UNPKG

iudex-web

Version:
1,435 lines (1,426 loc) 80.3 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Dispatch: () => Dispatch, EVENT_NAMES: () => EVENT_NAMES, FETCH_IGNORE_URLS: () => FETCH_IGNORE_URLS, XMLHttpRequest: () => XMLHttpRequest2, buildHeaders: () => buildHeaders, buildResource: () => buildResource, config: () => config, convertSeverityTextToNumber: () => convertSeverityTextToNumber, convertSeverityValuesToLevel: () => convertSeverityValuesToLevel, defaultInstrumentConfig: () => defaultInstrumentConfig, emitOtelLog: () => emitOtelLog, flattenObject: () => flattenObject, getCallerInfo: () => getCallerInfo, instrument: () => instrument, iudexCloudflare: () => cloudflare_worker_exports, iudexConsole: () => console_instrumentation_exports, nativeConsole: () => nativeConsole, registerOTelOptions: () => registerOTelOptions, setAttribute: () => setAttribute, setError: () => setError, setName: () => setName, setStatus: () => setStatus, trackAttribute: () => trackAttribute, trackGlobalAttribute: () => trackGlobalAttribute, useTracing: () => useTracing, withTracing: () => withTracing }); module.exports = __toCommonJS(src_exports); var import_api3 = require("@opentelemetry/api"); // src/utils.ts var import_semantic_conventions = require("@opentelemetry/semantic-conventions"); var import_api_logs = require("@opentelemetry/api-logs"); var import_lodash = __toESM(require("lodash")); var config = { isInstrumented: false, nativeConsole: { ...console }, nativeFetch: fetch.bind(globalThis) }; var nativeConsole = config.nativeConsole; function convertSeverityTextToNumber(severityText) { if (severityText == void 0) { return; } switch (severityText) { case "TRACE": return 1; case "DEBUG": return 5; case "INFO": return 9; case "WARN": return 13; case "ERROR": return 17; case "FATAL": return 21; default: return; } } __name(convertSeverityTextToNumber, "convertSeverityTextToNumber"); function convertSeverityValuesToLevel(severityNumber, severityText) { severityNumber ||= convertSeverityTextToNumber(severityText) || 0; if (severityNumber >= 1 && severityNumber <= 4) { return "TRACE"; } else if (severityNumber >= 5 && severityNumber <= 8) { return "DEBUG"; } else if (severityNumber >= 9 && severityNumber <= 12) { return "INFO"; } else if (severityNumber >= 13 && severityNumber <= 16) { return "WARN"; } else if (severityNumber >= 17 && severityNumber <= 20) { return "ERROR"; } else if (severityNumber >= 21 && severityNumber <= 24) { return "FATAL"; } else { return "INFO"; } } __name(convertSeverityValuesToLevel, "convertSeverityValuesToLevel"); function getCallerInfo(frameDepth) { const stack = new Error().stack; if (!stack) return {}; const stackLines = stack.split("\n"); const callerStackLine = stackLines[frameDepth + 1]; const callerAndPathRegex = /at (?<caller>.+?) \((?<filePath>[^:()]+(?::[^:()]+)*):(?<lineNum>\d+):\d+\)/; const capMatch = callerStackLine.match(callerAndPathRegex); if (capMatch) { const { filePath, lineNum, caller } = capMatch.groups; return { filePath, lineNum: Number(lineNum), caller }; } const pathOnlyRegex = /at (?<filePath>[^:()]+(?::[^:()]+)*):(?<lineNum>\d+):\d+/; const poMatch = callerStackLine.match(pathOnlyRegex); if (poMatch) { const { filePath, lineNum } = poMatch.groups; return { filePath, lineNum: Number(lineNum) }; } return {}; } __name(getCallerInfo, "getCallerInfo"); function flattenObject(obj, parentKey = "", result = {}, seen = /* @__PURE__ */ new Set(), maxObjectKeys = 30, maxDepth = 4) { if (!obj) return; if (maxDepth < 0) { result[parentKey] = "<max depth exceeded>"; return; } Object.entries(obj).forEach(([key, value], i) => { if (i === maxObjectKeys) { result[`${parentKey}.${key}._max_keys_exceeded`] = "<max keys exceeded>"; return; } if (i > maxObjectKeys) { return; } const newKey = parentKey ? `${parentKey}.${key}` : key; if (seen.has(value)) { result[newKey] = "<circular>"; return; } if (typeof value === "object" && value !== null && !Array.isArray(obj[key])) { seen.add(value); flattenObject(value, newKey, result, seen, maxObjectKeys, maxDepth - 1); } else { result[newKey] = value; } }); return result; } __name(flattenObject, "flattenObject"); function emitOtelLog({ level, body, severityNumber, attributes, stackDepth }) { if (!config.isInstrumented) return; const attrs = { ...attributes }; if (stackDepth != null) { const { filePath, lineNum, caller } = getCallerInfo(stackDepth + 1); Object.assign(attrs, { [import_semantic_conventions.SEMATTRS_CODE_FILEPATH]: filePath, [import_semantic_conventions.SEMATTRS_CODE_LINENO]: lineNum, [import_semantic_conventions.SEMATTRS_CODE_FUNCTION]: caller }); } if (config.sessionProvider) { attrs.session = config.sessionProvider.getActiveSession(); } const otelLogger = config.loggerProvider?.getLogger("default") || import_api_logs.logs.getLogger("default"); otelLogger.emit({ severityNumber: severityNumber || convertSeverityTextToNumber(level.toUpperCase()), severityText: level.toUpperCase(), body, attributes: import_lodash.default.omitBy(attrs, import_lodash.default.isNil) }); } __name(emitOtelLog, "emitOtelLog"); var Dispatch = class extends EventTarget { static { __name(this, "Dispatch"); } dispatch(eventName) { const ev = new Event(eventName); if ("on" + eventName in this) { this["on" + eventName](ev); } this.dispatchEvent(ev); } }; var XMLHttpRequest2 = class extends Dispatch { static { __name(this, "XMLHttpRequest"); } // readyState enumeration UNSENT = 0; OPENED = 1; HEADERS_RECEIVED = 2; LOADING = 3; DONE = 4; upload = new Dispatch(); // Request url; method; headers; readyState; _controller; withCredentials = false; // Response errored = false; responseHeaders = void 0; responseURL; responseValue; responseType; status; statusText; constructor() { super(); } open(method, url) { this.url = url; this.method = method; this.headers = new Headers(); this.readyState = this.UNSENT; this.responseType = ""; this._controller = new AbortController(); } setRequestHeader(key, value) { this.headers?.set(key, value); } abort() { this.upload.dispatch("abort"); this._controller?.abort(); } send(payload) { this.readyState = this.OPENED; this.status = 0; this.dispatch("readystatechange"); this.upload.dispatch("loadstart"); if (!this.url) { throw new DOMException( `Failed to execute 'send' on 'XMLHttpRequest': The object's state must be OPENED` ); } const fetchPromise = config.nativeFetch(this.url, { method: this.method, credentials: "credentials" in Request.prototype ? this.withCredentials ? "omit" : "include" : void 0, headers: this.headers, signal: this._controller?.signal, body: payload }).then((response) => { this.responseHeaders = response.headers; this.readyState = this.HEADERS_RECEIVED; this.responseURL = response.url; this.status = response.status; this.statusText = response.statusText; this.responseType = "text"; const contentType = response.headers.get("content-type"); if (!contentType) { } else if (contentType.includes("application/json")) { this.responseType = "json"; } else if (contentType.includes("text/html")) { this.responseType = "document"; } else if (contentType.includes("application/octet-stream")) { this.responseType = "blob"; } switch (this.responseType) { case void 0: return response.text(); case "blob": return response.blob(); case "document": return response.text(); case "json": return response.json(); } return response.text(); }).then((value) => { this.responseValue = value; this.readyState = this.DONE; this.dispatch("readystatechange"); this.upload.dispatch("progress"); this.dispatch("progress"); this.upload.dispatch("load"); this.dispatch("load"); }).catch((err) => { this.errored = true; this.dispatch("error"); this.upload.dispatch("error"); this.readyState = this.DONE; this.dispatch("readystatechange"); }).finally(() => { this.upload.dispatch("loadend"); this.dispatch("loadend"); }); config.workerEvent?.waitUntil(fetchPromise); } get responseText() { if (this.responseValue) { if (this.responseType === "arraybuffer") { return new TextDecoder().decode(this.responseValue); } if (this.responseType === "json") { return JSON.stringify(this.responseValue); } if (this.responseValue === "blob") { return new TextDecoder().decode(this.responseValue); } return this.responseValue; } return ""; } getAllResponseHeaders() { if (this.errored || this.readyState && this.readyState < this.HEADERS_RECEIVED || !this.responseHeaders) return ""; return Object.entries(this.responseHeaders).map(([header, value]) => `${header}: ${value}`).join("\r\n"); } getResponseHeader(headerName) { return this.responseHeaders?.get(headerName.toLowerCase()) || null; } }; // src/trace.ts var import_api = require("@opentelemetry/api"); function withTracing(fn, ctx = {}) { return function(...args) { const { name, trackArgs = true, maxArgKeys, maxArgDepth, attributes, setSpan, setArgs } = ctx; const tracer = import_api.trace.getTracer("default"); if (!config.isInstrumented) { return fn(...args); } if (ctx.beforeRun) { ctx.beforeRun(...args); } return tracer.startActiveSpan(name || fn.name || "<anonymous>", (span) => { try { if (attributes) { span.setAttributes(attributes); } if (trackArgs && !setArgs) { if (args.length === 1) { if (args[0] != null && typeof args[0] === "object") { const flatObj = flattenObject(args[0], "", {}, /* @__PURE__ */ new Set(), maxArgKeys, maxArgDepth); flatObj && Object.entries(flatObj).forEach(([key, value]) => { span.setAttribute(key, value); }); } else if (args[0] == null) { span.setAttribute("args.0", `<${args[0]}>`); } else { span.setAttribute("args.0", args[0]); } } else if (args.length > 1) { const flatObjs = flattenObject(args, "", {}, /* @__PURE__ */ new Set(), maxArgKeys, maxArgDepth); flatObjs && Object.entries(flatObjs).forEach(([key, value]) => { span.setAttribute(`args.${key}`, value); }); } } if (setArgs) { setArgs(span, args); } const ret = fn(...args); if (ret.then) { if (setSpan) { setSpan(span, ret); return ret; } return ret.then((res) => { return res; }).catch((err) => { span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: String(err) }); span.recordException(err); span.setAttribute("exception.message", err.message); span.setAttribute("exception.stacktrace", err.stack); emitOtelLog({ level: "ERROR", body: err }); throw err; }).finally(() => { span.end(); }); } if (setSpan) { setSpan(span, ret); return ret; } span.end(); return ret; } catch (err) { span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: err.message }); span.recordException(err); span.setAttribute("exception.message", err.message); err.stack && span.setAttribute("exception.stacktrace", err.stack || ""); emitOtelLog({ level: "ERROR", body: err }); span.end(); throw err; } }); }; } __name(withTracing, "withTracing"); function useTracing(fn, ctx = {}) { return withTracing(fn, ctx)(); } __name(useTracing, "useTracing"); // src/instrument.ts var import_exporter_logs_otlp_proto = require("@opentelemetry/exporter-logs-otlp-proto"); var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http"); var import_instrumentation3 = require("@opentelemetry/instrumentation"); var import_instrumentation_document_load = require("@opentelemetry/instrumentation-document-load"); // src/instrumentations/fetch-instrumentation.ts var api = __toESM(require("@opentelemetry/api")); var import_instrumentation = require("@opentelemetry/instrumentation"); var core = __toESM(require("@opentelemetry/core")); var web = __toESM(require("@opentelemetry/sdk-trace-web")); var import_AttributeNames = require("@opentelemetry/instrumentation-fetch/build/src/enums/AttributeNames"); var import_semantic_conventions2 = require("@opentelemetry/semantic-conventions"); var import_version = require("@opentelemetry/instrumentation-fetch/build/src/version"); var import_core = require("@opentelemetry/core"); var OBSERVER_WAIT_TIME_MS = 300; var isNode = typeof process === "object" && process.release?.name === "node"; var FetchInstrumentation = class extends import_instrumentation.InstrumentationBase { static { __name(this, "FetchInstrumentation"); } component = "fetch"; version = import_version.VERSION; moduleName = this.component; _usedResources = /* @__PURE__ */ new WeakSet(); _tasksCount = 0; constructor(config2 = {}) { super("@opentelemetry/instrumentation-fetch", import_version.VERSION, config2); } init() { } /** * Add cors pre flight child span * @param span * @param corsPreFlightRequest */ _addChildSpan(span, corsPreFlightRequest) { const childSpan = this.tracer.startSpan( "CORS Preflight", { startTime: corsPreFlightRequest[web.PerformanceTimingNames.FETCH_START] }, api.trace.setSpan(api.context.active(), span) ); if (!this.getConfig().ignoreNetworkEvents) { web.addSpanNetworkEvents(childSpan, corsPreFlightRequest); } childSpan.end( corsPreFlightRequest[web.PerformanceTimingNames.RESPONSE_END] ); } /** * Adds more attributes to span just before ending it * @param span * @param response */ _addFinalSpanAttributes(span, response) { const parsedUrl = web.parseUrl(response.url); span.setAttribute(import_semantic_conventions2.SEMATTRS_HTTP_STATUS_CODE, response.status); if (response.statusText != null) { span.setAttribute(import_AttributeNames.AttributeNames.HTTP_STATUS_TEXT, response.statusText); } span.setAttribute(import_semantic_conventions2.SEMATTRS_HTTP_HOST, parsedUrl.host); span.setAttribute( import_semantic_conventions2.SEMATTRS_HTTP_SCHEME, parsedUrl.protocol.replace(":", "") ); if (typeof navigator !== "undefined") { span.setAttribute(import_semantic_conventions2.SEMATTRS_HTTP_USER_AGENT, navigator.userAgent); } } /** * Add headers * @param options * @param spanUrl */ _addHeaders(options, spanUrl) { if (!web.shouldPropagateTraceHeaders( spanUrl, this.getConfig().propagateTraceHeaderCorsUrls )) { const headers = {}; api.propagation.inject(api.context.active(), headers); if (Object.keys(headers).length > 0) { this._diag.debug("headers inject skipped due to CORS policy"); } return; } const sessionId = config.sessionProvider?.getActiveSession()?.id; let baggage = api.propagation.getActiveBaggage() || api.propagation.createBaggage({}); if (sessionId) { baggage = baggage.setEntry("session.id", { value: sessionId }); } const ctx = api.propagation.setBaggage(api.context.active(), baggage); api.context.with(ctx, () => { if (options instanceof Request) { api.propagation.inject(api.context.active(), options.headers, { set: /* @__PURE__ */ __name((h, k, v) => h.set(k, typeof v === "string" ? v : String(v)), "set") }); } else if (options.headers instanceof Headers) { api.propagation.inject(api.context.active(), options.headers, { set: /* @__PURE__ */ __name((h, k, v) => h.set(k, typeof v === "string" ? v : String(v)), "set") }); } else if (options.headers instanceof Map) { api.propagation.inject(api.context.active(), options.headers, { set: /* @__PURE__ */ __name((h, k, v) => h.set(k, typeof v === "string" ? v : String(v)), "set") }); } else { const headers = {}; api.propagation.inject(api.context.active(), headers); options.headers = Object.assign({}, headers, options.headers || {}); } }); } /** * Clears the resource timings and all resources assigned with spans * when {@link FetchPluginConfig.clearTimingResources} is * set to true (default false) * @private */ _clearResources() { if (this._tasksCount === 0 && this.getConfig().clearTimingResources) { performance.clearResourceTimings(); this._usedResources = /* @__PURE__ */ new WeakSet(); } } /** * Creates a new span * @param url * @param options */ _createSpan(url, options = {}) { if (core.isUrlIgnored(url, this.getConfig().ignoreUrls)) { this._diag.debug("ignoring span as url matches ignored url"); return; } const method = (options.method || "GET").toUpperCase(); const spanName = `HTTP ${method}`; return this.tracer.startSpan(spanName, { kind: api.SpanKind.CLIENT, attributes: { [import_AttributeNames.AttributeNames.COMPONENT]: this.moduleName, [import_semantic_conventions2.SEMATTRS_HTTP_METHOD]: method, [import_semantic_conventions2.SEMATTRS_HTTP_URL]: url } }); } /** * Finds appropriate resource and add network events to the span * @param span * @param resourcesObserver * @param endTime */ _findResourceAndAddNetworkEvents(span, resourcesObserver, endTime) { let resources = resourcesObserver.entries; if (!resources.length) { if (!performance.getEntriesByType) { return; } resources = performance.getEntriesByType( "resource" ); } const resource = web.getResource( resourcesObserver.spanUrl, resourcesObserver.startTime, endTime, resources, this._usedResources, "fetch" ); if (resource.mainRequest) { const mainRequest = resource.mainRequest; this._markResourceAsUsed(mainRequest); const corsPreFlightRequest = resource.corsPreFlightRequest; if (corsPreFlightRequest) { this._addChildSpan(span, corsPreFlightRequest); this._markResourceAsUsed(corsPreFlightRequest); } if (!this.getConfig().ignoreNetworkEvents) { web.addSpanNetworkEvents(span, mainRequest); } } } /** * Marks certain [resource]{@link PerformanceResourceTiming} when information * from this is used to add events to span. * This is done to avoid reusing the same resource again for next span * @param resource */ _markResourceAsUsed(resource) { this._usedResources.add(resource); } /** * Finish span, add attributes, network events etc. * @param span * @param spanData * @param response */ _endSpan(span, spanData, response) { const endTime = core.millisToHrTime(Date.now()); const performanceEndTime = core.hrTime(); this._addFinalSpanAttributes(span, response); setTimeout(() => { spanData.observer?.disconnect(); this._findResourceAndAddNetworkEvents(span, spanData, performanceEndTime); this._tasksCount--; this._clearResources(); span.end(endTime); }, OBSERVER_WAIT_TIME_MS); } /** * Patches the constructor of fetch */ _patchConstructor() { return (original) => { const plugin = this; return /* @__PURE__ */ __name(function patchConstructor(...args) { const self = this; const url = web.parseUrl( args[0] instanceof Request ? args[0].url : String(args[0]) ).href; const options = args[0] instanceof Request ? args[0] : args[1] || {}; const createdSpan = plugin._createSpan(url, options); if (!createdSpan) { return original.apply(this, args); } const spanData = plugin._prepareSpanData(url); function endSpanOnError(span, error) { plugin._applyAttributesAfterFetch(span, options, error); plugin._endSpan(span, spanData, { status: error.status || 0, statusText: error.message, url }); } __name(endSpanOnError, "endSpanOnError"); function endSpanOnSuccess(span, response) { plugin._applyAttributesAfterFetch(span, options, response); if (response.status >= 200 && response.status < 400) { plugin._endSpan(span, spanData, response); } else { plugin._endSpan(span, spanData, { status: response.status, statusText: response.statusText, url }); } } __name(endSpanOnSuccess, "endSpanOnSuccess"); function onSuccess(span, resolve, response) { try { const resClone = response.clone(); const resClone4Hook = response.clone(); const body = resClone.body; if (body) { const reader = body.getReader(); const read = /* @__PURE__ */ __name(() => { reader.read().then( ({ done }) => { if (done) { endSpanOnSuccess(span, resClone4Hook); } else { read(); } }, (error) => { endSpanOnError(span, error); } ); }, "read"); read(); } else { endSpanOnSuccess(span, response); } } finally { resolve(response); } } __name(onSuccess, "onSuccess"); function onError(span, reject, error) { try { endSpanOnError(span, error); } finally { reject(error); } } __name(onError, "onError"); return new Promise((resolve, reject) => { return api.context.with( api.trace.setSpan(api.context.active(), createdSpan), () => { plugin._addHeaders(options, url); plugin._tasksCount++; return original.apply( self, options instanceof Request ? [options] : [url, options] ).then( onSuccess.bind(self, createdSpan, resolve), onError.bind(self, createdSpan, reject) ); } ); }); }, "patchConstructor"); }; } _applyAttributesAfterFetch(span, request, result) { const applyCustomAttributesOnSpan = this.getConfig().applyCustomAttributesOnSpan; if (applyCustomAttributesOnSpan) { (0, import_instrumentation.safeExecuteInTheMiddle)( () => applyCustomAttributesOnSpan(span, request, result), (error) => { if (!error) { return; } this._diag.error("applyCustomAttributesOnSpan", error); }, true ); } } /** * Prepares a span data - needed later for matching appropriate network * resources * @param spanUrl */ _prepareSpanData(spanUrl) { const startTime = core.hrTime(); const entries = []; if (typeof PerformanceObserver !== "function") { return { entries, startTime, spanUrl }; } const observer = new PerformanceObserver((list) => { const perfObsEntries = list.getEntries(); perfObsEntries.forEach((entry) => { if (entry.initiatorType === "fetch" && entry.name === spanUrl) { entries.push(entry); } }); }); observer.observe({ entryTypes: ["resource"] }); return { entries, observer, startTime, spanUrl }; } /** * implements enable function */ enable() { if (isNode) { this._diag.warn( "this instrumentation is intended for web usage only, it does not instrument Node.js's fetch()" ); return; } if ((0, import_instrumentation.isWrapped)(fetch)) { this._unwrap(import_core._globalThis, "fetch"); this._diag.debug("removing previous patch for constructor"); } this._wrap(import_core._globalThis, "fetch", this._patchConstructor()); } /** * implements unpatch function */ disable() { if (isNode) { return; } this._unwrap(import_core._globalThis, "fetch"); this._usedResources = /* @__PURE__ */ new WeakSet(); } }; // src/instrument.ts var import_instrumentation_xml_http_request = require("@opentelemetry/instrumentation-xml-http-request"); var import_resources2 = require("@opentelemetry/resources"); var import_sdk_logs2 = require("@opentelemetry/sdk-logs"); var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base"); var import_sdk_trace_web2 = require("@opentelemetry/sdk-trace-web"); var import_core4 = require("@opentelemetry/core"); var import_semantic_conventions3 = require("@opentelemetry/semantic-conventions"); var import_lodash2 = __toESM(require("lodash")); // src/instrumentations/console-instrumentation.ts var console_instrumentation_exports = {}; __export(console_instrumentation_exports, { instrumentConsole: () => instrumentConsole }); var R = __toESM(require("ramda")); var import_util = __toESM(require("util")); function instrumentConsole() { if (console._instrumented) return; const { log, error, warn, info, debug, timeLog, timeEnd } = console; [ { name: "log", logger: log, level: "INFO" }, { name: "error", logger: error, level: "ERROR" }, { name: "warn", logger: warn, level: "WARN" }, { name: "info", logger: info, level: "INFO" }, { name: "debug", logger: debug, level: "DEBUG" }, { name: "timeLog", logger: timeLog, level: "INFO" }, { name: "timeEnd", logger: timeEnd, level: "INFO" } ].forEach(({ name, logger, level }) => { console[name] = function(...content) { logger(...content); const contentWoCtx = content.filter((c) => !isObject(c) || !("ctx" in c || "authCtx" in c)); const contentCtx = R.mergeAll( content.filter((c) => isObject(c) && ("ctx" in c || "authCtx" in c)).map((c) => { if (c.ctx) return c.ctx; if (c.authCtx) return c.authCtx; return {}; }) ); const prettyContentWoCtx = contentWoCtx.map((c) => { if (typeof c === "object") { try { return import_util.default.inspect(c); } catch { } } return c; }); emitOtelLog({ level, body: prettyContentWoCtx.join(" "), attributes: contentCtx }); }; }); console._instrumented = true; } __name(instrumentConsole, "instrumentConsole"); function isObject(obj) { return typeof obj === "object" && !Array.isArray(obj) && obj !== null; } __name(isObject, "isObject"); // src/instrumentations/user-interaction-instrumentation.ts var import_instrumentation2 = require("@opentelemetry/instrumentation"); var api2 = __toESM(require("@opentelemetry/api")); var import_core2 = require("@opentelemetry/core"); var import_sdk_trace_web = require("@opentelemetry/sdk-trace-web"); var PACKAGE_NAME = "iudex-web/instrumentation-user-interaction"; var PACKAGE_VERSION = "0.39.0"; var ZONE_CONTEXT_KEY = "OT_ZONE_CONTEXT"; var EVENT_NAVIGATION_NAME = "Navigation:"; var DEFAULT_EVENT_NAMES = ["click"]; function defaultShouldPreventSpanCreation() { return false; } __name(defaultShouldPreventSpanCreation, "defaultShouldPreventSpanCreation"); var UserInteractionInstrumentation = class extends import_instrumentation2.InstrumentationBase { static { __name(this, "UserInteractionInstrumentation"); } version = PACKAGE_VERSION; moduleName = "user-interaction"; _spansData = /* @__PURE__ */ new WeakMap(); _zonePatched; // for addEventListener/removeEventListener state _wrappedListeners = /* @__PURE__ */ new WeakMap(); // for event bubbling _eventsSpanMap = /* @__PURE__ */ new WeakMap(); _eventNames; _shouldPreventSpanCreation; // How to connect event with when react handles it? spansByPendingEvent = /* @__PURE__ */ new Map(); constructor(config2 = {}) { super(PACKAGE_NAME, PACKAGE_VERSION, config2); this._eventNames = new Set(config2?.eventNames ?? DEFAULT_EVENT_NAMES); this._shouldPreventSpanCreation = typeof config2?.shouldPreventSpanCreation === "function" ? config2.shouldPreventSpanCreation : defaultShouldPreventSpanCreation; } init() { } /** * This will check if last task was timeout and will save the time to * fix the user interaction when nothing happens * This timeout comes from xhr plugin which is needed to collect information * about last xhr main request from observer * @param task * @param span */ _checkForTimeout(task, span) { const spanData = this._spansData.get(span); if (spanData) { if (task.source === "setTimeout") { spanData.hrTimeLastTimeout = (0, import_core2.hrTime)(); } else if (task.source !== "Promise.then" && task.source !== "setTimeout") { spanData.hrTimeLastTimeout = void 0; } } } /** * Controls whether or not to create a span, based on the event type. */ _allowEventName(eventName) { return this._eventNames.has(eventName); } /** * Creates a new span * @param element * @param eventName * @param parentSpan */ _createSpan(element, eventName, parentSpan) { if (!(element instanceof HTMLElement)) { return void 0; } if (!element.getAttribute) { return void 0; } if (element.hasAttribute("disabled")) { return void 0; } const isReactEvent = eventName.startsWith("react-"); if (isReactEvent) { eventName = eventName.split("react-", 2)[1]; } if (!this._allowEventName(eventName)) { return void 0; } const xpath = (0, import_sdk_trace_web.getElementXPath)(element, true); try { const span = this.tracer.startSpan( eventName, { attributes: { ["event_type" /* EVENT_TYPE */]: eventName, ["target_element" /* TARGET_ELEMENT */]: element.tagName, ["target_xpath" /* TARGET_XPATH */]: xpath, ["http.url" /* HTTP_URL */]: window.location.href } }, parentSpan ? api2.trace.setSpan(api2.context.active(), parentSpan) : void 0 ); if (this._shouldPreventSpanCreation(eventName, element, span) === true) { return void 0; } this._spansData.set(span, { taskCount: 0 }); return span; } catch (e) { this._diag.error("failed to start create new user interaction span", e); } return void 0; } /** * Decrement number of tasks that left in zone, * This is needed to be able to end span when no more tasks left * @param span */ _decrementTask(span) { const spanData = this._spansData.get(span); if (spanData) { spanData.taskCount--; if (spanData.taskCount === 0) { this._tryToEndSpan(span, spanData.hrTimeLastTimeout); } } } /** * Return the current span * @param zone * @private */ _getCurrentSpan(zone) { const context4 = zone.get(ZONE_CONTEXT_KEY); if (context4) { return api2.trace.getSpan(context4); } return context4; } /** * Increment number of tasks that are run within the same zone. * This is needed to be able to end span when no more tasks left * @param span */ _incrementTask(span) { const spanData = this._spansData.get(span); if (spanData) { spanData.taskCount++; } } /** * Returns true iff we should use the patched callback; false if it's already been patched */ addPatchedListener(on, type, listener, wrappedListener) { let listener2Type = this._wrappedListeners.get(listener); if (!listener2Type) { listener2Type = /* @__PURE__ */ new Map(); this._wrappedListeners.set(listener, listener2Type); } let element2patched = listener2Type.get(type); if (!element2patched) { element2patched = /* @__PURE__ */ new Map(); listener2Type.set(type, element2patched); } if (element2patched.has(on)) { return false; } element2patched.set(on, wrappedListener); return true; } /** * Returns the patched version of the callback (or undefined) */ removePatchedListener(on, type, listener) { const listener2Type = this._wrappedListeners.get(listener); if (!listener2Type) { return void 0; } const element2patched = listener2Type.get(type); if (!element2patched) { return void 0; } const patched = element2patched.get(on); if (patched) { element2patched.delete(on); if (element2patched.size === 0) { listener2Type.delete(type); if (listener2Type.size === 0) { this._wrappedListeners.delete(listener); } } } return patched; } // utility method to deal with the Function|EventListener nature of addEventListener _invokeListener(listener, target, args) { if (typeof listener === "function") { return listener.apply(target, args); } else { return listener.handleEvent(args[0]); } } /** * This patches the addEventListener of HTMLElement to be able to * auto instrument the click events * This is done when zone is not available */ _patchAddEventListener() { const plugin = this; return (original) => { return /* @__PURE__ */ __name(function addEventListenerPatched(type, listener, useCapture) { if (!listener) { return original.call(this, type, listener, useCapture); } const once = useCapture && typeof useCapture === "object" && useCapture.once; const patchedListener = /* @__PURE__ */ __name(function(...args) { let parentSpan; const event = args[0]; let target = event?.target; if (event) { parentSpan = plugin._eventsSpanMap.get(event); } if (once) { plugin.removePatchedListener(this, type, listener); } const isReactEvent = event?.currentTarget?.tagName === "REACT"; if (isReactEvent && event?.currentTarget?.ownerDocument?.activeElement) { target = event.currentTarget.ownerDocument.activeElement; } const span = plugin._createSpan(target, type, parentSpan); if (span) { if (event) { plugin._eventsSpanMap.set(event, span); } if (isReactEvent && target) { const fiberKey = Object.keys(target).find((k) => k.startsWith("__reactFiber$")); if (fiberKey) { const fiber = target[fiberKey]; if (fiber._debugSource) { const { fileName, lineNumber, columnNumber } = fiber._debugSource; columnNumber && span.setAttribute("code.column", columnNumber); fileName && span.setAttribute("code.filepath", fileName); lineNumber && span.setAttribute("code.lineno", lineNumber); } const fibs = getFiberTree(fiber); const lineage = getReactRenderLineage(fibs); const stackTrace = getReactRenderStackTrace(fibs); span.setAttribute("code.stacktrace", stackTrace.slice(0, 10).join("\n")); span.setAttribute("code.reacttrace", lineage.join("\n")); } } try { return api2.context.with( api2.trace.setSpan(api2.context.active(), span), () => { const result = plugin._invokeListener(listener, this, args); return result; } ); } catch (e) { const error = e; span.setStatus({ code: api2.SpanStatusCode.ERROR, message: error.message }); span.recordException(error); emitOtelLog({ level: "ERROR", body: error.stack }); throw e; } finally { span.end(); } } else { return plugin._invokeListener(listener, this, args); } }, "patchedListener"); if (plugin.addPatchedListener(this, type, listener, patchedListener)) { return original.call(this, type, patchedListener, useCapture); } }, "addEventListenerPatched"); }; } /** * This patches the removeEventListener of HTMLElement to handle the fact that * we patched the original callbacks * This is done when zone is not available */ _patchRemoveEventListener() { const plugin = this; return (original) => { return /* @__PURE__ */ __name(function removeEventListenerPatched(type, listener, useCapture) { const wrappedListener = plugin.removePatchedListener( this, type, listener ); if (wrappedListener) { return original.call(this, type, wrappedListener, useCapture); } else { return original.call(this, type, listener, useCapture); } }, "removeEventListenerPatched"); }; } /** * Most browser provide event listener api via EventTarget in prototype chain. * Exception to this is IE 11 which has it on the prototypes closest to EventTarget: * * * - has addEventListener in IE * ** - has addEventListener in all other browsers * ! - missing in IE * * HTMLElement -> Element -> Node * -> EventTarget **! -> Object * Document -> Node * -> EventTarget **! -> Object * Window * -> WindowProperties ! -> EventTarget **! -> Object */ _getPatchableEventTargets() { return window.EventTarget ? [EventTarget.prototype] : [Node.prototype, Window.prototype]; } /** * Patches the history api */ _patchHistoryApi() { this._unpatchHistoryApi(); this._wrap(history, "replaceState", this._patchHistoryMethod()); this._wrap(history, "pushState", this._patchHistoryMethod()); this._wrap(history, "back", this._patchHistoryMethod()); this._wrap(history, "forward", this._patchHistoryMethod()); this._wrap(history, "go", this._patchHistoryMethod()); } /** * Patches the certain history api method */ _patchHistoryMethod() { const plugin = this; return (original) => { return /* @__PURE__ */ __name(function patchHistoryMethod(...args) { const url = `${location.pathname}${location.hash}${location.search}`; const result = original.apply(this, args); const urlAfter = `${location.pathname}${location.hash}${location.search}`; if (url !== urlAfter) { plugin._updateInteractionName(urlAfter); } return result; }, "patchHistoryMethod"); }; } /** * unpatch the history api methods */ _unpatchHistoryApi() { if ((0, import_instrumentation2.isWrapped)(history.replaceState)) this._unwrap(history, "replaceState"); if ((0, import_instrumentation2.isWrapped)(history.pushState)) this._unwrap(history, "pushState"); if ((0, import_instrumentation2.isWrapped)(history.back)) this._unwrap(history, "back"); if ((0, import_instrumentation2.isWrapped)(history.forward)) this._unwrap(history, "forward"); if ((0, import_instrumentation2.isWrapped)(history.go)) this._unwrap(history, "go"); } /** * Updates interaction span name * @param url */ _updateInteractionName(url) { const span = api2.trace.getSpan(api2.context.active()); if (span && typeof span.updateName === "function") { span.updateName(`${EVENT_NAVIGATION_NAME} ${url}`); } } /** * Patches zone cancel task - this is done to be able to correctly * decrement the number of remaining tasks */ _patchZoneCancelTask() { const plugin = this; return (original) => { return /* @__PURE__ */ __name(function patchCancelTask(task) { const currentZone = Zone.current; const currentSpan = plugin._getCurrentSpan(currentZone); if (currentSpan && plugin._shouldCountTask(task, currentZone)) { plugin._decrementTask(currentSpan); } return original.call(this, task); }, "patchCancelTask"); }; } /** * Patches zone schedule task - this is done to be able to correctly * increment the number of tasks running within current zone but also to * save time in case of timeout running from xhr plugin when waiting for * main request from PerformanceResourceTiming */ _patchZoneScheduleTask() { const plugin = this; return (original) => { return /* @__PURE__ */ __name(function patchScheduleTask(task) { const currentZone = Zone.current; const currentSpan = plugin._getCurrentSpan(currentZone); if (currentSpan && plugin._shouldCountTask(task, currentZone)) { plugin._incrementTask(currentSpan); plugin._checkForTimeout(task, currentSpan); } return original.call(this, task); }, "patchScheduleTask"); }; } /** * Patches zone run task - this is done to be able to create a span when * user interaction starts * @private */ _patchZoneRunTask() { const plugin = this; return (original) => { return /* @__PURE__ */ __name(function patchRunTask(task, applyThis, applyArgs) { const event = Array.isArray(applyArgs) && applyArgs[0] instanceof Event ? applyArgs[0] : void 0; const target = event?.target; const activeZone = this; const span = target ? plugin._createSpan(target, task.eventName) : plugin._getCurrentSpan(this); if (target && span) { plugin._incrementTask(span); const isReactEvent = Object.keys(target).some((k) => k.startsWith("__reactFiber$")); if (isReactEvent) { plugin._incrementTask(span); const spans = plugin.spansByPendingEvent.get(task.eventName) || []; spans.push(span); plugin.spansByPendingEvent.set(task.eventName, spans); } return activeZone.run(() => { try { return api2.context.with( api2.trace.setSpan(api2.context.active(), span), () => { const currentZone = Zone.current; task._zone = currentZone; return original.call( currentZone, task, applyThis, applyArgs ); } ); } catch (e) { const error = e; span.setStatus({ code: api2.SpanStatusCode.ERROR, message: error.message }); span.recordException(error); emitOtelLog({ level: "ERROR", body: error.zoneAwareStack || error.stack }); throw e; } finally { plugin._decrementTask(span); } }); } if (event?.type?.startsWith("react-")) { const eventType = event.type.split("react-", 2)[1]; if (eventType) { const spans = plugin.spansByPendingEvent.get(eventType); if (spans?.length) { const span2 = spans.shift(); if (span2) { return activeZone.run(() => { try { return api2.context.with( api2.trace.setSpan(api2.context.active(), span2), () => { const currentZone = Zone.current; task._zone = currentZone; return original.call( currentZone, task, applyThis, applyArgs ); } ); } catch (e) { const error = e; span2.setStatus({ code: api2.SpanStatusCode.ERROR, message: error.message }); span2.recordException(error); emitOtelLog({ level: "ERROR", body: error.zoneAwareStack || error.stack }); throw e; } finally { plugin._decrementTask(span2); } }); } } } } try { return original.call(activeZone, task, applyThis, applyArgs); } catch (e) { const error = e; span?.setStatus({ code: api2.SpanStatusCode.ERROR, message: error.message }); span?.recordException(error); emitOtelLog({ level: "ERROR", body: error.zoneAwareStack || error.stack }); throw e; } finally { if (span && plugin._shouldCountTask(task, activeZone)) { plugin._decrementTask(span); } } }, "patchRunTask"); }; } /** * Decides if task should be counted. * @param task * @param currentZone * @private */ _shouldCountTask(task, currentZone) { if (task._zone) { currentZone = task._zone; } if (!currentZone || !task.data || task.data.isPeriodic) { return false; } const currentSpan = this._getCurrentSpan(currentZone); if (!currentSpan) { return false; } if (!this._spansData.get(currentSpan)) { return false; } return task.type === "macroTask" || task.type === "microTask"; } /** * Will try to end span when such span still exists. * @param span * @param endTime * @private */ _tryToEndSpan(span, endTime) { if (span) { const spanData = this._spansData.get(span); if (spanData) { span.end(endTime); this._spansData.delete(span); } } } /** * implements enable function */ enable() { const ZoneWithPrototype = this.getZoneWithPrototype(); this._diag.debug( "applying patch to", this.moduleName, this.version, "zone:", !!ZoneWithPrototype ); if (ZoneWithPrototype) { if ((0, import_instrumentation2.isWrapped)(ZoneWithPrototype.prototype.runTask)) { this._unwrap(ZoneWithPrototype.prototype, "runTask"); this._diag.debug("removing previous patch from method runTask"); } if ((0, import_instrumentation2.isWrapped)(ZoneWithPrototype.prototype.scheduleTask)) { this._unwrap(ZoneWithPrototype.prototype, "scheduleTask"); this._diag.debug("removing previous patch from method scheduleTask"); } if ((0, import_instrumentation2.isWrapped)(ZoneWithPrototype.prototype.cancelTask)) { this._unwrap(ZoneWithPrototype.prototype, "cancelTask"); this._diag.debug("removing previous patch from method cancelTask"); } this._zonePatched = true; this._wrap( ZoneWithPrototype.prototype, "runTask", this._patchZoneRunTask() ); this._wrap( ZoneWithPrototype.prototype, "scheduleTask", this._patchZoneScheduleTask() ); this._wrap( ZoneWithPrototype.prototype, "cancelTask", this._patchZoneCancelTask() ); } else { this._zonePatched = false; const targets = this._getPatchableEventTargets(); targets.forEach((target) => { if ((0, import_instrumentation2.isWrapped)(target.addEventListener)) { this._unwrap(target, "addEventListener"); this._diag.debug( "removing previous patch from method addEventListener" );