@opentelemetry/browser-instrumentation 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/console/instrumentation.d.ts +1 -2
  2. package/dist/console/instrumentation.js.map +1 -1
  3. package/dist/console/types.d.ts +2 -3
  4. package/dist/errors/instrumentation.d.ts +1 -2
  5. package/dist/errors/instrumentation.js.map +1 -1
  6. package/dist/errors/types.d.ts +2 -3
  7. package/dist/fetch/instrumentation.d.ts +3 -3
  8. package/dist/fetch/instrumentation.js +8 -3
  9. package/dist/fetch/instrumentation.js.map +1 -1
  10. package/dist/fetch/types.d.ts +5 -6
  11. package/dist/navigation/instrumentation.d.ts +1 -2
  12. package/dist/navigation/instrumentation.js.map +1 -1
  13. package/dist/navigation/types.d.ts +4 -5
  14. package/dist/navigation/utils.js +1 -2
  15. package/dist/navigation/utils.js.map +1 -1
  16. package/dist/navigation-timing/instrumentation.d.ts +1 -2
  17. package/dist/navigation-timing/instrumentation.js.map +1 -1
  18. package/dist/navigation-timing/types.d.ts +1 -2
  19. package/dist/package.js +1 -1
  20. package/dist/resource-timing/instrumentation.d.ts +1 -2
  21. package/dist/resource-timing/instrumentation.js.map +1 -1
  22. package/dist/resource-timing/types.d.ts +1 -2
  23. package/dist/user-action/instrumentation.d.ts +1 -2
  24. package/dist/user-action/instrumentation.js.map +1 -1
  25. package/dist/user-action/types.d.ts +3 -4
  26. package/dist/utils/getElementCSSSelector.js +3 -3
  27. package/dist/utils/getElementCSSSelector.js.map +1 -1
  28. package/dist/web-vitals/instrumentation.d.ts +1 -2
  29. package/dist/web-vitals/instrumentation.js.map +1 -1
  30. package/dist/web-vitals/types.d.ts +1 -2
  31. package/dist/xhr/instrumentation.d.ts +1 -2
  32. package/dist/xhr/instrumentation.js.map +1 -1
  33. package/dist/xhr/types.d.ts +1 -2
  34. package/package.json +15 -9
@@ -4,7 +4,7 @@ import { InstrumentationBase } from "@opentelemetry/instrumentation";
4
4
  /**
5
5
  * OpenTelemetry instrumentation that captures console calls and emits them as OpenTelemetry logs.
6
6
  */
7
- declare class ConsoleInstrumentation extends InstrumentationBase<ConsoleInstrumentationConfig> {
7
+ export declare class ConsoleInstrumentation extends InstrumentationBase<ConsoleInstrumentationConfig> {
8
8
  private _isPatched;
9
9
  private _active;
10
10
  constructor(config?: ConsoleInstrumentationConfig);
@@ -16,5 +16,4 @@ declare class ConsoleInstrumentation extends InstrumentationBase<ConsoleInstrume
16
16
  disable(): void;
17
17
  }
18
18
  //#endregion
19
- export { ConsoleInstrumentation };
20
19
  //# sourceMappingURL=instrumentation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/console/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { context } from '@opentelemetry/api';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { ATTR_CONSOLE_METHOD, CONSOLE_LOG_EVENT_NAME } from './semconv.ts';\nimport type { ConsoleInstrumentationConfig, ConsoleMethod } from './types.ts';\n\nconst DEFAULT_LOG_METHODS: ConsoleMethod[] = [\n 'log',\n 'warn',\n 'error',\n 'info',\n 'debug',\n];\n\nconst SEVERITY_MAP: Record<ConsoleMethod, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n log: SeverityNumber.INFO,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n};\n\n/**\n * Default serializer for console arguments.\n * Joins arguments as strings.\n */\nfunction defaultMessageSerializer(args: unknown[]): string {\n return args\n .map((arg) => {\n if (typeof arg === 'object' && arg !== null) {\n try {\n return JSON.stringify(arg);\n } catch {\n // Circular reference or other error, fallback to String\n return String(arg);\n }\n }\n return String(arg);\n })\n .join(' ');\n}\n\n/**\n * OpenTelemetry instrumentation that captures console calls and emits them as OpenTelemetry logs.\n */\nexport class ConsoleInstrumentation extends InstrumentationBase<ConsoleInstrumentationConfig> {\n private declare _isPatched: boolean;\n private declare _active: boolean;\n\n constructor(config: ConsoleInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/console', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n private _getMessageSerializer(): (args: unknown[]) => string {\n return this._config.messageSerializer ?? defaultMessageSerializer;\n }\n\n private _getLogMethods(): ConsoleMethod[] {\n return this._config.logMethods ?? DEFAULT_LOG_METHODS;\n }\n\n private _patchConsoleMethod(\n method: ConsoleMethod,\n ): (original: Console[ConsoleMethod]) => Console[ConsoleMethod] {\n const instrumentation = this;\n\n return function patchConsoleMethod(original: Console[ConsoleMethod]) {\n return function (this: Console, ...args: unknown[]) {\n if (\n instrumentation._active &&\n instrumentation._getLogMethods().includes(method)\n ) {\n const logContext = context.active();\n const body = instrumentation._getMessageSerializer()(args);\n\n instrumentation.logger.emit({\n body,\n eventName: CONSOLE_LOG_EVENT_NAME,\n severityNumber: SEVERITY_MAP[method],\n severityText: method,\n context: logContext,\n attributes: {\n [ATTR_CONSOLE_METHOD]: method,\n },\n });\n }\n\n return original.apply(this, args);\n } as Console[ConsoleMethod];\n };\n }\n\n override enable(): void {\n this._active = true;\n if (this._isPatched) {\n return;\n }\n this._isPatched = true;\n for (const method of DEFAULT_LOG_METHODS) {\n if (typeof console[method] === 'function') {\n this._wrap(console, method, this._patchConsoleMethod(method));\n }\n }\n }\n\n override disable(): void {\n this._active = false;\n }\n}\n"],"mappings":";;;;;;AAYA,MAAM,sBAAuC;CAC3C;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,eAAsD;CAC1D,OAAO,eAAe;CACtB,KAAK,eAAe;CACpB,MAAM,eAAe;CACrB,MAAM,eAAe;CACrB,OAAO,eAAe;AACxB;;;;;AAMA,SAAS,yBAAyB,MAAyB;CACzD,OAAO,KACJ,KAAK,QAAQ;EACZ,IAAI,OAAO,QAAQ,YAAY,QAAQ,MACrC,IAAI;GACF,OAAO,KAAK,UAAU,GAAG;EAC3B,QAAQ;GAEN,OAAO,OAAO,GAAG;EACnB;EAEF,OAAO,OAAO,GAAG;CACnB,CAAC,CAAC,CACD,KAAK,GAAG;AACb;;;;AAKA,IAAa,yBAAb,cAA4C,oBAAkD;CAI5F,YAAY,SAAuC,CAAC,GAAG;EACrD,MAAM,kDAAkD,SAAS,MAAM;CACzE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,wBAA6D;EAC3D,OAAO,KAAK,QAAQ,qBAAqB;CAC3C;CAEA,iBAA0C;EACxC,OAAO,KAAK,QAAQ,cAAc;CACpC;CAEA,oBACE,QAC8D;EAC9D,MAAM,kBAAkB;EAExB,OAAO,SAAS,mBAAmB,UAAkC;GACnE,OAAO,SAAyB,GAAG,MAAiB;IAClD,IACE,gBAAgB,WAChB,gBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,GAChD;KACA,MAAM,aAAa,QAAQ,OAAO;KAClC,MAAM,OAAO,gBAAgB,sBAAsB,CAAC,CAAC,IAAI;KAEzD,gBAAgB,OAAO,KAAK;MAC1B;MACA,WAAW;MACX,gBAAgB,aAAa;MAC7B,cAAc;MACd,SAAS;MACT,YAAY,GACT,sBAAsB,OACzB;KACF,CAAC;IACH;IAEA,OAAO,SAAS,MAAM,MAAM,IAAI;GAClC;EACF;CACF;CAEA,SAAwB;EACtB,KAAK,UAAU;EACf,IAAI,KAAK,YACP;EAEF,KAAK,aAAa;EAClB,KAAK,MAAM,UAAU,qBACnB,IAAI,OAAO,QAAQ,YAAY,YAC7B,KAAK,MAAM,SAAS,QAAQ,KAAK,oBAAoB,MAAM,CAAC;CAGlE;CAEA,UAAyB;EACvB,KAAK,UAAU;CACjB;AACF"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/console/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { context } from '@opentelemetry/api';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { ATTR_CONSOLE_METHOD, CONSOLE_LOG_EVENT_NAME } from './semconv.ts';\nimport type { ConsoleInstrumentationConfig, ConsoleMethod } from './types.ts';\n\nconst DEFAULT_LOG_METHODS: ConsoleMethod[] = [\n 'log',\n 'warn',\n 'error',\n 'info',\n 'debug',\n];\n\nconst SEVERITY_MAP: Record<ConsoleMethod, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n log: SeverityNumber.INFO,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n};\n\n/**\n * Default serializer for console arguments.\n * Joins arguments as strings.\n */\nfunction defaultMessageSerializer(args: unknown[]): string {\n return args\n .map((arg) => {\n if (typeof arg === 'object' && arg !== null) {\n try {\n return JSON.stringify(arg);\n } catch {\n // Circular reference or other error, fallback to String\n return String(arg);\n }\n }\n return String(arg);\n })\n .join(' ');\n}\n\n/**\n * OpenTelemetry instrumentation that captures console calls and emits them as OpenTelemetry logs.\n */\nexport class ConsoleInstrumentation extends InstrumentationBase<ConsoleInstrumentationConfig> {\n declare private _isPatched: boolean;\n declare private _active: boolean;\n\n constructor(config: ConsoleInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/console', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n private _getMessageSerializer(): (args: unknown[]) => string {\n return this._config.messageSerializer ?? defaultMessageSerializer;\n }\n\n private _getLogMethods(): ConsoleMethod[] {\n return this._config.logMethods ?? DEFAULT_LOG_METHODS;\n }\n\n private _patchConsoleMethod(\n method: ConsoleMethod,\n ): (original: Console[ConsoleMethod]) => Console[ConsoleMethod] {\n const instrumentation = this;\n\n return function patchConsoleMethod(original: Console[ConsoleMethod]) {\n return function (this: Console, ...args: unknown[]) {\n if (\n instrumentation._active &&\n instrumentation._getLogMethods().includes(method)\n ) {\n const logContext = context.active();\n const body = instrumentation._getMessageSerializer()(args);\n\n instrumentation.logger.emit({\n body,\n eventName: CONSOLE_LOG_EVENT_NAME,\n severityNumber: SEVERITY_MAP[method],\n severityText: method,\n context: logContext,\n attributes: {\n [ATTR_CONSOLE_METHOD]: method,\n },\n });\n }\n\n return original.apply(this, args);\n } as Console[ConsoleMethod];\n };\n }\n\n override enable(): void {\n this._active = true;\n if (this._isPatched) {\n return;\n }\n this._isPatched = true;\n for (const method of DEFAULT_LOG_METHODS) {\n if (typeof console[method] === 'function') {\n this._wrap(console, method, this._patchConsoleMethod(method));\n }\n }\n }\n\n override disable(): void {\n this._active = false;\n }\n}\n"],"mappings":";;;;;;AAYA,MAAM,sBAAuC;CAC3C;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,eAAsD;CAC1D,OAAO,eAAe;CACtB,KAAK,eAAe;CACpB,MAAM,eAAe;CACrB,MAAM,eAAe;CACrB,OAAO,eAAe;AACxB;;;;;AAMA,SAAS,yBAAyB,MAAyB;CACzD,OAAO,KACJ,KAAK,QAAQ;EACZ,IAAI,OAAO,QAAQ,YAAY,QAAQ,MACrC,IAAI;GACF,OAAO,KAAK,UAAU,GAAG;EAC3B,QAAQ;GAEN,OAAO,OAAO,GAAG;EACnB;EAEF,OAAO,OAAO,GAAG;CACnB,CAAC,CAAC,CACD,KAAK,GAAG;AACb;;;;AAKA,IAAa,yBAAb,cAA4C,oBAAkD;CAI5F,YAAY,SAAuC,CAAC,GAAG;EACrD,MAAM,kDAAkD,SAAS,MAAM;CACzE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,wBAA6D;EAC3D,OAAO,KAAK,QAAQ,qBAAqB;CAC3C;CAEA,iBAA0C;EACxC,OAAO,KAAK,QAAQ,cAAc;CACpC;CAEA,oBACE,QAC8D;EAC9D,MAAM,kBAAkB;EAExB,OAAO,SAAS,mBAAmB,UAAkC;GACnE,OAAO,SAAyB,GAAG,MAAiB;IAClD,IACE,gBAAgB,WAChB,gBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,GAChD;KACA,MAAM,aAAa,QAAQ,OAAO;KAClC,MAAM,OAAO,gBAAgB,sBAAsB,CAAC,CAAC,IAAI;KAEzD,gBAAgB,OAAO,KAAK;MAC1B;MACA,WAAW;MACX,gBAAgB,aAAa;MAC7B,cAAc;MACd,SAAS;MACT,YAAY,GACT,sBAAsB,OACzB;KACF,CAAC;IACH;IAEA,OAAO,SAAS,MAAM,MAAM,IAAI;GAClC;EACF;CACF;CAEA,SAAwB;EACtB,KAAK,UAAU;EACf,IAAI,KAAK,YACP;EAEF,KAAK,aAAa;EAClB,KAAK,MAAM,UAAU,qBACnB,IAAI,OAAO,QAAQ,YAAY,YAC7B,KAAK,MAAM,SAAS,QAAQ,KAAK,oBAAoB,MAAM,CAAC;CAGlE;CAEA,UAAyB;EACvB,KAAK,UAAU;CACjB;AACF"}
@@ -3,11 +3,11 @@ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
3
  /**
4
4
  * Console methods that can be instrumented.
5
5
  */
6
- type ConsoleMethod = 'log' | 'warn' | 'error' | 'info' | 'debug';
6
+ export type ConsoleMethod = 'log' | 'warn' | 'error' | 'info' | 'debug';
7
7
  /**
8
8
  * ConsoleInstrumentation Configuration
9
9
  */
10
- interface ConsoleInstrumentationConfig extends InstrumentationConfig {
10
+ export interface ConsoleInstrumentationConfig extends InstrumentationConfig {
11
11
  /**
12
12
  * Console methods to instrument.
13
13
  * @default ['log', 'warn', 'error', 'info', 'debug']
@@ -20,5 +20,4 @@ interface ConsoleInstrumentationConfig extends InstrumentationConfig {
20
20
  messageSerializer?: (args: unknown[]) => string;
21
21
  }
22
22
  //#endregion
23
- export { ConsoleInstrumentationConfig, ConsoleMethod };
24
23
  //# sourceMappingURL=types.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { ErrorsInstrumentationConfig } from "./types.js";
2
2
  import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
3
  //#region src/errors/instrumentation.d.ts
4
- declare class ErrorsInstrumentation extends InstrumentationBase<ErrorsInstrumentationConfig> {
4
+ export declare class ErrorsInstrumentation extends InstrumentationBase<ErrorsInstrumentationConfig> {
5
5
  private _isEnabled;
6
6
  private _onErrorHandler?;
7
7
  constructor(config?: ErrorsInstrumentationConfig);
@@ -12,5 +12,4 @@ declare class ErrorsInstrumentation extends InstrumentationBase<ErrorsInstrument
12
12
  private _applyCustomAttributes;
13
13
  }
14
14
  //#endregion
15
- export { ErrorsInstrumentation };
16
15
  //# sourceMappingURL=instrumentation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/errors/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes } from '@opentelemetry/api';\nimport type { AnyValueMap, LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport {\n ATTR_EXCEPTION_MESSAGE,\n ATTR_EXCEPTION_STACKTRACE,\n ATTR_EXCEPTION_TYPE,\n} from '@opentelemetry/semantic-conventions';\nimport { version } from '../../package.json' with { type: 'json' };\nimport type { ErrorsInstrumentationConfig } from './types.ts';\n\nconst EXCEPTION_EVENT_NAME = 'exception';\n\nexport class ErrorsInstrumentation extends InstrumentationBase<ErrorsInstrumentationConfig> {\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n private declare _isEnabled: boolean;\n private declare _onErrorHandler?: (\n event: ErrorEvent | PromiseRejectionEvent,\n ) => void;\n\n constructor(config: ErrorsInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/errors', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n this._isEnabled = true;\n\n this._onErrorHandler = (event) => this._onError(event);\n window.addEventListener('error', this._onErrorHandler);\n window.addEventListener('unhandledrejection', this._onErrorHandler);\n }\n\n override disable(): void {\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n\n if (this._onErrorHandler) {\n window.removeEventListener('error', this._onErrorHandler);\n window.removeEventListener('unhandledrejection', this._onErrorHandler);\n this._onErrorHandler = undefined;\n }\n }\n\n private _onError(event: ErrorEvent | PromiseRejectionEvent): void {\n const isRejection = 'reason' in event;\n let error: Error | string | null | undefined = isRejection\n ? event.reason\n : event.error;\n\n // Cross-origin scripts deliver an ErrorEvent with `event.error` null but\n // a populated `event.message` (the canonical \"Script error.\" sanitized\n // message). Treat that message as a string error so these events still\n // surface as an exception log instead of being silently dropped.\n // PromiseRejectionEvent has no analogous message field, so it stays a\n // no-op.\n if (\n error == null &&\n !isRejection &&\n typeof event.message === 'string' &&\n event.message.length > 0\n ) {\n error = event.message;\n }\n\n if (error == null) {\n // Emit a diag debug message so the drop is observable. Anything that\n // lands here had no usable signal (ErrorEvent with no error and no\n // message, or PromiseRejectionEvent rejected with null/undefined). Pass\n // the raw value too so the reader can tell null from undefined when\n // investigating a missing capture.\n this._diag.debug(\n isRejection\n ? 'ignored unhandledrejection event with no reason'\n : 'ignored error event with no error and no message',\n error,\n );\n return;\n }\n\n // Capture as a const so the type narrowing survives into the closure below.\n const capturedError = error;\n\n // This runs inside a global error listener, so an escaping throw would\n // re-trigger the listener and loop. Both attribute extraction (a rejection\n // can carry a value whose `stack` getter throws) and emit (a broken\n // LogRecordProcessor) can throw, so contain the whole path.\n safeExecuteInTheMiddle(\n () => {\n let errorAttributes: AnyValueMap;\n if (typeof capturedError === 'string') {\n errorAttributes = { [ATTR_EXCEPTION_MESSAGE]: capturedError };\n } else {\n errorAttributes = {\n [ATTR_EXCEPTION_TYPE]: capturedError.name,\n [ATTR_EXCEPTION_MESSAGE]: capturedError.message,\n [ATTR_EXCEPTION_STACKTRACE]: capturedError.stack,\n };\n }\n\n const customAttributes = this._applyCustomAttributes(capturedError);\n\n const logRecord: LogRecord = {\n eventName: EXCEPTION_EVENT_NAME,\n severityNumber: SeverityNumber.ERROR,\n attributes: { ...errorAttributes, ...customAttributes },\n };\n\n this.logger.emit(logRecord);\n },\n (err) => {\n if (err) {\n this._diag.error('failed to record exception', err);\n }\n },\n true,\n );\n }\n\n private _applyCustomAttributes(error: Error | string): Attributes {\n const hook = this.getConfig().applyCustomAttributes;\n if (!hook) {\n return {};\n }\n let result: Attributes = {};\n safeExecuteInTheMiddle(\n () => {\n result = hook(error);\n },\n (err) => {\n if (err) {\n this._diag.error('applyCustomAttributes hook failed', err);\n }\n },\n true,\n );\n return result;\n }\n}\n"],"mappings":";;;;;AAoBA,MAAM,uBAAuB;AAE7B,IAAa,wBAAb,cAA2C,oBAAiD;CAS1F,YAAY,SAAsC,CAAC,GAAG;EACpD,MAAM,iDAAiD,SAAS,MAAM;CACxE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,SAAwB;EACtB,IAAI,KAAK,YACP;EAEF,KAAK,aAAa;EAElB,KAAK,mBAAmB,UAAU,KAAK,SAAS,KAAK;EACrD,OAAO,iBAAiB,SAAS,KAAK,eAAe;EACrD,OAAO,iBAAiB,sBAAsB,KAAK,eAAe;CACpE;CAEA,UAAyB;EACvB,IAAI,CAAC,KAAK,YACR;EAEF,KAAK,aAAa;EAElB,IAAI,KAAK,iBAAiB;GACxB,OAAO,oBAAoB,SAAS,KAAK,eAAe;GACxD,OAAO,oBAAoB,sBAAsB,KAAK,eAAe;GACrE,KAAK,kBAAkB,KAAA;EACzB;CACF;CAEA,SAAiB,OAAiD;EAChE,MAAM,cAAc,YAAY;EAChC,IAAI,QAA2C,cAC3C,MAAM,SACN,MAAM;EAQV,IACE,SAAS,QACT,CAAC,eACD,OAAO,MAAM,YAAY,YACzB,MAAM,QAAQ,SAAS,GAEvB,QAAQ,MAAM;EAGhB,IAAI,SAAS,MAAM;GAMjB,KAAK,MAAM,MACT,cACI,oDACA,oDACJ,KACF;GACA;EACF;EAGA,MAAM,gBAAgB;EAMtB,6BACQ;GACJ,IAAI;GACJ,IAAI,OAAO,kBAAkB,UAC3B,kBAAkB,GAAG,yBAAyB,cAAc;QAE5D,kBAAkB;KACf,sBAAsB,cAAc;KACpC,yBAAyB,cAAc;KACvC,4BAA4B,cAAc;GAC7C;GAGF,MAAM,mBAAmB,KAAK,uBAAuB,aAAa;GAElE,MAAM,YAAuB;IAC3B,WAAW;IACX,gBAAgB,eAAe;IAC/B,YAAY;KAAE,GAAG;KAAiB,GAAG;IAAiB;GACxD;GAEA,KAAK,OAAO,KAAK,SAAS;EAC5B,IACC,QAAQ;GACP,IAAI,KACF,KAAK,MAAM,MAAM,8BAA8B,GAAG;EAEtD,GACA,IACF;CACF;CAEA,uBAA+B,OAAmC;EAChE,MAAM,OAAO,KAAK,UAAU,CAAC,CAAC;EAC9B,IAAI,CAAC,MACH,OAAO,CAAC;EAEV,IAAI,SAAqB,CAAC;EAC1B,6BACQ;GACJ,SAAS,KAAK,KAAK;EACrB,IACC,QAAQ;GACP,IAAI,KACF,KAAK,MAAM,MAAM,qCAAqC,GAAG;EAE7D,GACA,IACF;EACA,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/errors/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes } from '@opentelemetry/api';\nimport type { AnyValueMap, LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport {\n ATTR_EXCEPTION_MESSAGE,\n ATTR_EXCEPTION_STACKTRACE,\n ATTR_EXCEPTION_TYPE,\n} from '@opentelemetry/semantic-conventions';\nimport { version } from '../../package.json' with { type: 'json' };\nimport type { ErrorsInstrumentationConfig } from './types.ts';\n\nconst EXCEPTION_EVENT_NAME = 'exception';\n\nexport class ErrorsInstrumentation extends InstrumentationBase<ErrorsInstrumentationConfig> {\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n declare private _isEnabled: boolean;\n declare private _onErrorHandler?: (\n event: ErrorEvent | PromiseRejectionEvent,\n ) => void;\n\n constructor(config: ErrorsInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/errors', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n this._isEnabled = true;\n\n this._onErrorHandler = (event) => this._onError(event);\n window.addEventListener('error', this._onErrorHandler);\n window.addEventListener('unhandledrejection', this._onErrorHandler);\n }\n\n override disable(): void {\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n\n if (this._onErrorHandler) {\n window.removeEventListener('error', this._onErrorHandler);\n window.removeEventListener('unhandledrejection', this._onErrorHandler);\n this._onErrorHandler = undefined;\n }\n }\n\n private _onError(event: ErrorEvent | PromiseRejectionEvent): void {\n const isRejection = 'reason' in event;\n let error: Error | string | null | undefined = isRejection\n ? event.reason\n : event.error;\n\n // Cross-origin scripts deliver an ErrorEvent with `event.error` null but\n // a populated `event.message` (the canonical \"Script error.\" sanitized\n // message). Treat that message as a string error so these events still\n // surface as an exception log instead of being silently dropped.\n // PromiseRejectionEvent has no analogous message field, so it stays a\n // no-op.\n if (\n error == null &&\n !isRejection &&\n typeof event.message === 'string' &&\n event.message.length > 0\n ) {\n error = event.message;\n }\n\n if (error == null) {\n // Emit a diag debug message so the drop is observable. Anything that\n // lands here had no usable signal (ErrorEvent with no error and no\n // message, or PromiseRejectionEvent rejected with null/undefined). Pass\n // the raw value too so the reader can tell null from undefined when\n // investigating a missing capture.\n this._diag.debug(\n isRejection\n ? 'ignored unhandledrejection event with no reason'\n : 'ignored error event with no error and no message',\n error,\n );\n return;\n }\n\n // Capture as a const so the type narrowing survives into the closure below.\n const capturedError = error;\n\n // This runs inside a global error listener, so an escaping throw would\n // re-trigger the listener and loop. Both attribute extraction (a rejection\n // can carry a value whose `stack` getter throws) and emit (a broken\n // LogRecordProcessor) can throw, so contain the whole path.\n safeExecuteInTheMiddle(\n () => {\n let errorAttributes: AnyValueMap;\n if (typeof capturedError === 'string') {\n errorAttributes = { [ATTR_EXCEPTION_MESSAGE]: capturedError };\n } else {\n errorAttributes = {\n [ATTR_EXCEPTION_TYPE]: capturedError.name,\n [ATTR_EXCEPTION_MESSAGE]: capturedError.message,\n [ATTR_EXCEPTION_STACKTRACE]: capturedError.stack,\n };\n }\n\n const customAttributes = this._applyCustomAttributes(capturedError);\n\n const logRecord: LogRecord = {\n eventName: EXCEPTION_EVENT_NAME,\n severityNumber: SeverityNumber.ERROR,\n attributes: { ...errorAttributes, ...customAttributes },\n };\n\n this.logger.emit(logRecord);\n },\n (err) => {\n if (err) {\n this._diag.error('failed to record exception', err);\n }\n },\n true,\n );\n }\n\n private _applyCustomAttributes(error: Error | string): Attributes {\n const hook = this.getConfig().applyCustomAttributes;\n if (!hook) {\n return {};\n }\n let result: Attributes = {};\n safeExecuteInTheMiddle(\n () => {\n result = hook(error);\n },\n (err) => {\n if (err) {\n this._diag.error('applyCustomAttributes hook failed', err);\n }\n },\n true,\n );\n return result;\n }\n}\n"],"mappings":";;;;;AAoBA,MAAM,uBAAuB;AAE7B,IAAa,wBAAb,cAA2C,oBAAiD;CAS1F,YAAY,SAAsC,CAAC,GAAG;EACpD,MAAM,iDAAiD,SAAS,MAAM;CACxE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,SAAwB;EACtB,IAAI,KAAK,YACP;EAEF,KAAK,aAAa;EAElB,KAAK,mBAAmB,UAAU,KAAK,SAAS,KAAK;EACrD,OAAO,iBAAiB,SAAS,KAAK,eAAe;EACrD,OAAO,iBAAiB,sBAAsB,KAAK,eAAe;CACpE;CAEA,UAAyB;EACvB,IAAI,CAAC,KAAK,YACR;EAEF,KAAK,aAAa;EAElB,IAAI,KAAK,iBAAiB;GACxB,OAAO,oBAAoB,SAAS,KAAK,eAAe;GACxD,OAAO,oBAAoB,sBAAsB,KAAK,eAAe;GACrE,KAAK,kBAAkB,KAAA;EACzB;CACF;CAEA,SAAiB,OAAiD;EAChE,MAAM,cAAc,YAAY;EAChC,IAAI,QAA2C,cAC3C,MAAM,SACN,MAAM;EAQV,IACE,SAAS,QACT,CAAC,eACD,OAAO,MAAM,YAAY,YACzB,MAAM,QAAQ,SAAS,GAEvB,QAAQ,MAAM;EAGhB,IAAI,SAAS,MAAM;GAMjB,KAAK,MAAM,MACT,cACI,oDACA,oDACJ,KACF;GACA;EACF;EAGA,MAAM,gBAAgB;EAMtB,6BACQ;GACJ,IAAI;GACJ,IAAI,OAAO,kBAAkB,UAC3B,kBAAkB,GAAG,yBAAyB,cAAc;QAE5D,kBAAkB;KACf,sBAAsB,cAAc;KACpC,yBAAyB,cAAc;KACvC,4BAA4B,cAAc;GAC7C;GAGF,MAAM,mBAAmB,KAAK,uBAAuB,aAAa;GAElE,MAAM,YAAuB;IAC3B,WAAW;IACX,gBAAgB,eAAe;IAC/B,YAAY;KAAE,GAAG;KAAiB,GAAG;IAAiB;GACxD;GAEA,KAAK,OAAO,KAAK,SAAS;EAC5B,IACC,QAAQ;GACP,IAAI,KACF,KAAK,MAAM,MAAM,8BAA8B,GAAG;EAEtD,GACA,IACF;CACF;CAEA,uBAA+B,OAAmC;EAChE,MAAM,OAAO,KAAK,UAAU,CAAC,CAAC;EAC9B,IAAI,CAAC,MACH,OAAO,CAAC;EAEV,IAAI,SAAqB,CAAC;EAC1B,6BACQ;GACJ,SAAS,KAAK,KAAK;EACrB,IACC,QAAQ;GACP,IAAI,KACF,KAAK,MAAM,MAAM,qCAAqC,GAAG;EAE7D,GACA,IACF;EACA,OAAO;CACT;AACF"}
@@ -1,11 +1,11 @@
1
1
  import { Attributes } from "@opentelemetry/api";
2
2
  import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
3
  //#region src/errors/types.d.ts
4
- type ApplyCustomAttributesFunction = (error: Error | string) => Attributes;
4
+ export type ApplyCustomAttributesFunction = (error: Error | string) => Attributes;
5
5
  /**
6
6
  * ErrorsInstrumentation Configuration
7
7
  */
8
- interface ErrorsInstrumentationConfig extends InstrumentationConfig {
8
+ export interface ErrorsInstrumentationConfig extends InstrumentationConfig {
9
9
  /**
10
10
  * Optional callback invoked for each captured error or unhandled rejection.
11
11
  * Returned attributes are merged onto the emitted log record (after the
@@ -14,5 +14,4 @@ interface ErrorsInstrumentationConfig extends InstrumentationConfig {
14
14
  applyCustomAttributes?: ApplyCustomAttributesFunction;
15
15
  }
16
16
  //#endregion
17
- export { ApplyCustomAttributesFunction, ErrorsInstrumentationConfig };
18
17
  //# sourceMappingURL=types.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { FetchInstrumentationConfig } from "./types.js";
2
2
  import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
3
  //#region src/fetch/instrumentation.d.ts
4
- declare class FetchInstrumentation extends InstrumentationBase<FetchInstrumentationConfig> {
4
+ export declare class FetchInstrumentation extends InstrumentationBase<FetchInstrumentationConfig> {
5
5
  private _isEnabled;
6
6
  private _isFetchPatched;
7
7
  private _spanResources;
@@ -27,10 +27,10 @@ declare class FetchInstrumentation extends InstrumentationBase<FetchInstrumentat
27
27
  */
28
28
  private _callRequestHook;
29
29
  /**
30
- * Add headers for the request and the given context if apply
30
+ * Add headers for the request and the given context if apply.
31
+ * Returns the options to send, a copy when the caller passed an init object.
31
32
  */
32
33
  private _addHeaders;
33
34
  }
34
35
  //#endregion
35
- export { FetchInstrumentation };
36
36
  //# sourceMappingURL=instrumentation.d.ts.map
@@ -126,7 +126,7 @@ var FetchInstrumentation = class extends InstrumentationBase {
126
126
  fetchStart: performance.now()
127
127
  });
128
128
  instrumentation._callRequestHook(createdSpan, options);
129
- instrumentation._addHeaders(options, url, fetchContext);
129
+ options = instrumentation._addHeaders(options, url, fetchContext);
130
130
  return original.apply(globalThis, options instanceof Request ? [options] : [url, options]).then(onSuccess.bind(globalThis, createdSpan), onError.bind(globalThis, createdSpan));
131
131
  });
132
132
  } catch (e) {
@@ -198,7 +198,8 @@ var FetchInstrumentation = class extends InstrumentationBase {
198
198
  }, true);
199
199
  }
200
200
  /**
201
- * Add headers for the request and the given context if apply
201
+ * Add headers for the request and the given context if apply.
202
+ * Returns the options to send, a copy when the caller passed an init object.
202
203
  */
203
204
  _addHeaders(options, url, ctx) {
204
205
  const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;
@@ -208,13 +209,17 @@ var FetchInstrumentation = class extends InstrumentationBase {
208
209
  else {
209
210
  const headers = new Headers(options.headers);
210
211
  propagation.inject(ctx, headers, { set: (h, k, v) => h.set(k, typeof v === "string" ? v : String(v)) });
211
- options.headers = headers;
212
+ return {
213
+ ...options,
214
+ headers
215
+ };
212
216
  }
213
217
  } else {
214
218
  const headers = {};
215
219
  propagation.inject(ctx, headers);
216
220
  if (Object.keys(headers).length > 0) this._diag.debug("headers inject skipped due to CORS policy");
217
221
  }
222
+ return options;
218
223
  }
219
224
  };
220
225
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/fetch/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes, Context, Span } from '@opentelemetry/api';\nimport {\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n} from '@opentelemetry/api';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport {\n ATTR_ERROR_TYPE,\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_REQUEST_METHOD_ORIGINAL,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n} from '@opentelemetry/semantic-conventions';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { getNetworkContextRegistry } from '../utils/NetworkContextRegistry.ts';\nimport {\n getFetchBodyLength,\n normalizeHttpRequestMethod,\n} from '../utils/request.ts';\nimport { matchesUrl, parseUrl, serverPortFromUrl } from '../utils/url.ts';\nimport { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv.ts';\nimport type {\n FetchError,\n FetchInstrumentationConfig,\n FetchResponse,\n} from './types.ts';\n\nconst hasBrowserPerformanceAPI = typeof PerformanceObserver !== 'undefined';\n\nexport class FetchInstrumentation extends InstrumentationBase<FetchInstrumentationConfig> {\n // Note: Intentionally *not* using `_enabled` as the field name to avoid\n // any possible confusion with the `_enabled` field used on the *Node.js*\n // InstrumentationBase class.\n // Also not initializing the fields to `false` because the base class\n // constructor already call `enable` modifying their values and it will\n // set the instrumentations in a base state (enabled, patched but with flags set to false)\n private declare _isEnabled: boolean;\n private declare _isFetchPatched: boolean;\n\n // To keep track of the resources\n private _spanResources: Map<Span, PerformanceResourceTiming> = new Map();\n\n constructor(config: FetchInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/fetch', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (!hasBrowserPerformanceAPI) {\n this._diag.warn(\n 'this instrumentation is intended for web usage only, it does not instrument server-side fetch()',\n );\n return;\n }\n\n if (this._isEnabled) {\n return;\n }\n\n if (this._isFetchPatched) {\n this._diag.debug('fetch constructor already patched');\n this._isEnabled = true;\n return;\n }\n\n try {\n // `_wrap` throws if a third-party script has locked globalThis.fetch via\n // Object.defineProperty(window, 'fetch', { writable: false, ... }).\n this._wrap(globalThis, 'fetch', this._patchConstructor());\n this._isFetchPatched = true;\n this._isEnabled = true;\n } catch (err) {\n this._diag.warn(\n 'Failed to patch globalThis.fetch; instrumentation will not be enabled. ' +\n 'Another script may have locked globalThis.fetch via Object.defineProperty.',\n err,\n );\n }\n }\n\n override disable(): void {\n if (!hasBrowserPerformanceAPI) {\n return;\n }\n\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n }\n\n /**\n * Patches the constructor of fetch\n */\n private _patchConstructor(): (original: typeof fetch) => typeof fetch {\n return (original) => {\n const instrumentation = this;\n\n return function patchConstructor(\n this: typeof globalThis,\n ...args: Parameters<typeof fetch>\n ): Promise<Response> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n\n // Reference to the Span in case there is an exception after creating it (wil not end)\n // It will be used to keep the Map only with the Spans that will be ended\n let trucatedSpan: Span | undefined;\n try {\n const instrConfig = instrumentation.getConfig();\n const url = parseUrl(\n args[0] instanceof Request ? args[0].url : String(args[0]),\n ).href;\n\n const shouldIgnoreUrl = matchesUrl(url, instrConfig.ignoreUrls);\n if (shouldIgnoreUrl) {\n return original.apply(this, args);\n }\n\n // Per the Fetch spec, when fetch() is called with a Request object\n // and a separate init object, the init properties override the\n // Request's properties. Merge them into a new Request so that\n // downstream consumers (hooks, header injection, the actual fetch\n // call) see the correct final values.\n // See: https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#parameters\n let options: Request | RequestInit;\n if (args[0] instanceof Request) {\n options = args[1] != null ? new Request(args[0], args[1]) : args[0];\n } else {\n options = args[1] || {};\n }\n\n const createdSpan = instrumentation._createSpan(url, options);\n trucatedSpan = createdSpan;\n if (instrConfig.measureRequestSize) {\n getFetchBodyLength(...args)\n .then((bodyLength) => {\n if (typeof bodyLength === 'number') {\n createdSpan.setAttribute(\n ATTR_HTTP_REQUEST_BODY_SIZE,\n bodyLength,\n );\n }\n })\n .catch((error) => {\n instrumentation._diag.warn('getFetchBodyLength', error);\n });\n }\n\n function endSpanOnError(span: Span, error: FetchError) {\n instrumentation._applyAttributesAfterFetch(span, options, error);\n instrumentation._endSpan(span, {\n status: error.status,\n error: error.name || error.message,\n aborted: error.name === 'AbortError',\n });\n }\n\n function endSpanOnSuccess(span: Span, response: Response) {\n instrumentation._applyAttributesAfterFetch(span, options, response);\n instrumentation._endSpan(span, response);\n }\n\n function onSuccess(span: Span, response: Response): Response {\n try {\n // Clone the response and eagerly consume the clone to detect\n // when the body transfer completes. The original response is\n // returned to the caller untouched so that it passes internal\n // brand-checks required by APIs such as\n // WebAssembly.compileStreaming.\n // It consumes the entire body even if the user cancels reading it\n // from the original response. But it does work with `AbortController.abort()`\n // because it aborts the underlying fetch cancelling the original and clone streams\n // ref: https://github.com/open-telemetry/opentelemetry-js/pull/6521\n const resClone = response.clone();\n const body = resClone.body;\n if (body) {\n const reader = body.getReader();\n\n const read = (): void => {\n reader.read().then(\n ({ done }) => {\n try {\n if (done) {\n endSpanOnSuccess(span, response);\n } else {\n read();\n }\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to finalize span after body read',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n (error) => {\n try {\n endSpanOnError(span, {\n name: error?.name,\n message: error?.message,\n status: response.status,\n });\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to end span after body read rejected',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n );\n };\n read();\n } else {\n // some older browsers don't have .body implemented\n endSpanOnSuccess(span, response);\n }\n } catch (error) {\n instrumentation._diag.error(\n 'Failed to read fetch response body',\n error,\n );\n instrumentation._endSpan(span, { status: response.status });\n }\n return response;\n }\n\n function onError(span: Span, error: FetchError): never {\n try {\n endSpanOnError(span, error);\n } catch (e: unknown) {\n // endSpanOnError failed — fall back to ending the span directly.\n // No `status`: no response was ever received for this request.\n instrumentation._diag.error(\n 'Failed to end span on fetch error',\n e,\n );\n instrumentation._endSpan(span, {});\n }\n throw error;\n }\n\n const fetchContext = trace.setSpan(context.active(), createdSpan);\n return context.with(fetchContext, () => {\n instrumentation._spanResources.set(createdSpan, {\n name: url,\n fetchStart: performance.now(),\n } as PerformanceResourceTiming);\n\n // Call request hook before injection so hooks cannot tamper with propagation headers.\n // Also, this means the hook will see `options.headers` in the same type as passed in,\n // rather than as a `Headers` instance set by `_addHeaders()`.\n instrumentation._callRequestHook(createdSpan, options);\n instrumentation._addHeaders(options, url, fetchContext);\n\n return original\n .apply(\n globalThis,\n options instanceof Request ? [options] : [url, options],\n )\n .then(\n onSuccess.bind(globalThis, createdSpan),\n onError.bind(globalThis, createdSpan),\n );\n });\n } catch (e: unknown) {\n instrumentation._diag.error('Failed to instrument fetch request', e);\n }\n // Remove the span if it was created and stored before the exception\n if (trucatedSpan) {\n instrumentation._spanResources.delete(trucatedSpan);\n }\n return original.apply(this, args);\n };\n };\n }\n\n /**\n * Creates a new span\n */\n private _createSpan(\n url: string,\n options: Partial<Request | RequestInit> = {},\n ): Span {\n const attributes = {} as Attributes;\n const parsedUrl = parseUrl(url);\n const origMethod = options.method;\n const normMethod = normalizeHttpRequestMethod(options.method || 'GET');\n const serverPort = serverPortFromUrl(parsedUrl);\n const name = normMethod;\n const { sanitizeUrl } = this.getConfig();\n\n attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;\n attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url) : url;\n attributes[ATTR_SERVER_ADDRESS] = parsedUrl.hostname;\n if (serverPort) {\n attributes[ATTR_SERVER_PORT] = serverPort;\n }\n if (origMethod && origMethod !== normMethod) {\n attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;\n }\n\n return this.tracer.startSpan(name, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n }\n\n /**\n * Finish span, add attributes, etc.\n */\n private _endSpan(span: Span, response: FetchResponse) {\n if (response.status !== undefined) {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);\n }\n\n // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status\n // 1xx/2xx/3xx MUST be left unset UNLESS there was another error (e.g. a\n // network error receiving the response body)\n if (!response.aborted) {\n const isErrorStatus =\n response.status === undefined ||\n response.status >= 400 ||\n response.error !== undefined;\n if (isErrorStatus) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n const errorType =\n response.error ??\n (response.status !== undefined ? String(response.status) : undefined);\n if (typeof errorType === 'string') {\n span.setAttribute(ATTR_ERROR_TYPE, errorType);\n }\n }\n }\n\n span.end();\n\n // Register the resource context for other instrumentations\n const resource = this._spanResources.get(span);\n if (resource) {\n this._spanResources.delete(span);\n getNetworkContextRegistry().register(span, {\n key: resource.name,\n startPerfNow: resource.fetchStart,\n endPerfNow: performance.now(),\n });\n }\n }\n\n private _applyAttributesAfterFetch(\n span: Span,\n request: Request | RequestInit,\n result: Response | FetchError,\n ) {\n const applyCustomAttributesOnSpan =\n this.getConfig().applyCustomAttributesOnSpan;\n if (applyCustomAttributesOnSpan) {\n safeExecuteInTheMiddle(\n () => applyCustomAttributesOnSpan(span, request, result),\n (error) => {\n if (!error) {\n return;\n }\n this._diag.error('applyCustomAttributesOnSpan', error);\n },\n true,\n );\n }\n }\n\n /**\n * Calls the request hook if defined\n */\n private _callRequestHook(span: Span, request: Request | RequestInit) {\n const requestHook = this.getConfig().requestHook;\n\n if (requestHook) {\n safeExecuteInTheMiddle(\n () => requestHook(span, request),\n (error) => {\n if (error) {\n this._diag.error('requestHook', error);\n }\n },\n true,\n );\n }\n }\n\n /**\n * Add headers for the request and the given context if apply\n */\n private _addHeaders(\n options: Request | RequestInit,\n url: string,\n ctx: Context,\n ): void {\n // Propagate only if in request goes to same origin or is in the allow list\n const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;\n const urlOrigin = parseUrl(url).origin;\n const sameOrigin = location.origin === urlOrigin;\n const shouldPropagate = sameOrigin || matchesUrl(url, urlsToPropagate);\n\n if (shouldPropagate) {\n if (options instanceof Request) {\n // This mutates `Request.headers` in-place, because it is read-only\n // (per https://developer.mozilla.org/en-US/docs/Web/API/Request/headers),\n // so we cannot (easily) replace it.\n propagation.inject(ctx, options.headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n } else {\n // Otherwise, create a new Headers to avoid mutating the caller's\n // possibly re-used headers.\n const headers = new Headers(options.headers);\n propagation.inject(ctx, headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n options.headers = headers;\n }\n } else {\n const headers: Partial<Record<string, unknown>> = {};\n propagation.inject(ctx, headers);\n if (Object.keys(headers).length > 0) {\n this._diag.debug('headers inject skipped due to CORS policy');\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/fetch/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes, Context, Span } from '@opentelemetry/api';\nimport {\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n} from '@opentelemetry/api';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport {\n ATTR_ERROR_TYPE,\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_REQUEST_METHOD_ORIGINAL,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n} from '@opentelemetry/semantic-conventions';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { getNetworkContextRegistry } from '../utils/NetworkContextRegistry.ts';\nimport {\n getFetchBodyLength,\n normalizeHttpRequestMethod,\n} from '../utils/request.ts';\nimport { matchesUrl, parseUrl, serverPortFromUrl } from '../utils/url.ts';\nimport { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv.ts';\nimport type {\n FetchError,\n FetchInstrumentationConfig,\n FetchResponse,\n} from './types.ts';\n\nconst hasBrowserPerformanceAPI = typeof PerformanceObserver !== 'undefined';\n\nexport class FetchInstrumentation extends InstrumentationBase<FetchInstrumentationConfig> {\n // Note: Intentionally *not* using `_enabled` as the field name to avoid\n // any possible confusion with the `_enabled` field used on the *Node.js*\n // InstrumentationBase class.\n // Also not initializing the fields to `false` because the base class\n // constructor already call `enable` modifying their values and it will\n // set the instrumentations in a base state (enabled, patched but with flags set to false)\n declare private _isEnabled: boolean;\n declare private _isFetchPatched: boolean;\n\n // To keep track of the resources\n private _spanResources: Map<Span, PerformanceResourceTiming> = new Map();\n\n constructor(config: FetchInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/fetch', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (!hasBrowserPerformanceAPI) {\n this._diag.warn(\n 'this instrumentation is intended for web usage only, it does not instrument server-side fetch()',\n );\n return;\n }\n\n if (this._isEnabled) {\n return;\n }\n\n if (this._isFetchPatched) {\n this._diag.debug('fetch constructor already patched');\n this._isEnabled = true;\n return;\n }\n\n try {\n // `_wrap` throws if a third-party script has locked globalThis.fetch via\n // Object.defineProperty(window, 'fetch', { writable: false, ... }).\n this._wrap(globalThis, 'fetch', this._patchConstructor());\n this._isFetchPatched = true;\n this._isEnabled = true;\n } catch (err) {\n this._diag.warn(\n 'Failed to patch globalThis.fetch; instrumentation will not be enabled. ' +\n 'Another script may have locked globalThis.fetch via Object.defineProperty.',\n err,\n );\n }\n }\n\n override disable(): void {\n if (!hasBrowserPerformanceAPI) {\n return;\n }\n\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n }\n\n /**\n * Patches the constructor of fetch\n */\n private _patchConstructor(): (original: typeof fetch) => typeof fetch {\n return (original) => {\n const instrumentation = this;\n\n return function patchConstructor(\n this: typeof globalThis,\n ...args: Parameters<typeof fetch>\n ): Promise<Response> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n\n // Reference to the Span in case there is an exception after creating it (wil not end)\n // It will be used to keep the Map only with the Spans that will be ended\n let trucatedSpan: Span | undefined;\n try {\n const instrConfig = instrumentation.getConfig();\n const url = parseUrl(\n args[0] instanceof Request ? args[0].url : String(args[0]),\n ).href;\n\n const shouldIgnoreUrl = matchesUrl(url, instrConfig.ignoreUrls);\n if (shouldIgnoreUrl) {\n return original.apply(this, args);\n }\n\n // Per the Fetch spec, when fetch() is called with a Request object\n // and a separate init object, the init properties override the\n // Request's properties. Merge them into a new Request so that\n // downstream consumers (hooks, header injection, the actual fetch\n // call) see the correct final values.\n // See: https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#parameters\n let options: Request | RequestInit;\n if (args[0] instanceof Request) {\n options = args[1] != null ? new Request(args[0], args[1]) : args[0];\n } else {\n options = args[1] || {};\n }\n\n const createdSpan = instrumentation._createSpan(url, options);\n trucatedSpan = createdSpan;\n if (instrConfig.measureRequestSize) {\n getFetchBodyLength(...args)\n .then((bodyLength) => {\n if (typeof bodyLength === 'number') {\n createdSpan.setAttribute(\n ATTR_HTTP_REQUEST_BODY_SIZE,\n bodyLength,\n );\n }\n })\n .catch((error) => {\n instrumentation._diag.warn('getFetchBodyLength', error);\n });\n }\n\n function endSpanOnError(span: Span, error: FetchError) {\n instrumentation._applyAttributesAfterFetch(span, options, error);\n instrumentation._endSpan(span, {\n status: error.status,\n error: error.name || error.message,\n aborted: error.name === 'AbortError',\n });\n }\n\n function endSpanOnSuccess(span: Span, response: Response) {\n instrumentation._applyAttributesAfterFetch(span, options, response);\n instrumentation._endSpan(span, response);\n }\n\n function onSuccess(span: Span, response: Response): Response {\n try {\n // Clone the response and eagerly consume the clone to detect\n // when the body transfer completes. The original response is\n // returned to the caller untouched so that it passes internal\n // brand-checks required by APIs such as\n // WebAssembly.compileStreaming.\n // It consumes the entire body even if the user cancels reading it\n // from the original response. But it does work with `AbortController.abort()`\n // because it aborts the underlying fetch cancelling the original and clone streams\n // ref: https://github.com/open-telemetry/opentelemetry-js/pull/6521\n const resClone = response.clone();\n const body = resClone.body;\n if (body) {\n const reader = body.getReader();\n\n const read = (): void => {\n reader.read().then(\n ({ done }) => {\n try {\n if (done) {\n endSpanOnSuccess(span, response);\n } else {\n read();\n }\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to finalize span after body read',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n (error) => {\n try {\n endSpanOnError(span, {\n name: error?.name,\n message: error?.message,\n status: response.status,\n });\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to end span after body read rejected',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n );\n };\n read();\n } else {\n // some older browsers don't have .body implemented\n endSpanOnSuccess(span, response);\n }\n } catch (error) {\n instrumentation._diag.error(\n 'Failed to read fetch response body',\n error,\n );\n instrumentation._endSpan(span, { status: response.status });\n }\n return response;\n }\n\n function onError(span: Span, error: FetchError): never {\n try {\n endSpanOnError(span, error);\n } catch (e: unknown) {\n // endSpanOnError failed — fall back to ending the span directly.\n // No `status`: no response was ever received for this request.\n instrumentation._diag.error(\n 'Failed to end span on fetch error',\n e,\n );\n instrumentation._endSpan(span, {});\n }\n throw error;\n }\n\n const fetchContext = trace.setSpan(context.active(), createdSpan);\n return context.with(fetchContext, () => {\n instrumentation._spanResources.set(createdSpan, {\n name: url,\n fetchStart: performance.now(),\n } as PerformanceResourceTiming);\n\n // Call request hook before injection so hooks cannot tamper with propagation headers.\n // Also, this means the hook will see `options.headers` in the same type as passed in,\n // rather than as a `Headers` instance set by `_addHeaders()`.\n instrumentation._callRequestHook(createdSpan, options);\n options = instrumentation._addHeaders(options, url, fetchContext);\n\n return original\n .apply(\n globalThis,\n options instanceof Request ? [options] : [url, options],\n )\n .then(\n onSuccess.bind(globalThis, createdSpan),\n onError.bind(globalThis, createdSpan),\n );\n });\n } catch (e: unknown) {\n instrumentation._diag.error('Failed to instrument fetch request', e);\n }\n // Remove the span if it was created and stored before the exception\n if (trucatedSpan) {\n instrumentation._spanResources.delete(trucatedSpan);\n }\n return original.apply(this, args);\n };\n };\n }\n\n /**\n * Creates a new span\n */\n private _createSpan(\n url: string,\n options: Partial<Request | RequestInit> = {},\n ): Span {\n const attributes = {} as Attributes;\n const parsedUrl = parseUrl(url);\n const origMethod = options.method;\n const normMethod = normalizeHttpRequestMethod(options.method || 'GET');\n const serverPort = serverPortFromUrl(parsedUrl);\n const name = normMethod;\n const { sanitizeUrl } = this.getConfig();\n\n attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;\n attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url) : url;\n attributes[ATTR_SERVER_ADDRESS] = parsedUrl.hostname;\n if (serverPort) {\n attributes[ATTR_SERVER_PORT] = serverPort;\n }\n if (origMethod && origMethod !== normMethod) {\n attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;\n }\n\n return this.tracer.startSpan(name, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n }\n\n /**\n * Finish span, add attributes, etc.\n */\n private _endSpan(span: Span, response: FetchResponse) {\n if (response.status !== undefined) {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);\n }\n\n // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status\n // 1xx/2xx/3xx MUST be left unset UNLESS there was another error (e.g. a\n // network error receiving the response body)\n if (!response.aborted) {\n const isErrorStatus =\n response.status === undefined ||\n response.status >= 400 ||\n response.error !== undefined;\n if (isErrorStatus) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n const errorType =\n response.error ??\n (response.status !== undefined ? String(response.status) : undefined);\n if (typeof errorType === 'string') {\n span.setAttribute(ATTR_ERROR_TYPE, errorType);\n }\n }\n }\n\n span.end();\n\n // Register the resource context for other instrumentations\n const resource = this._spanResources.get(span);\n if (resource) {\n this._spanResources.delete(span);\n getNetworkContextRegistry().register(span, {\n key: resource.name,\n startPerfNow: resource.fetchStart,\n endPerfNow: performance.now(),\n });\n }\n }\n\n private _applyAttributesAfterFetch(\n span: Span,\n request: Request | RequestInit,\n result: Response | FetchError,\n ) {\n const applyCustomAttributesOnSpan =\n this.getConfig().applyCustomAttributesOnSpan;\n if (applyCustomAttributesOnSpan) {\n safeExecuteInTheMiddle(\n () => applyCustomAttributesOnSpan(span, request, result),\n (error) => {\n if (!error) {\n return;\n }\n this._diag.error('applyCustomAttributesOnSpan', error);\n },\n true,\n );\n }\n }\n\n /**\n * Calls the request hook if defined\n */\n private _callRequestHook(span: Span, request: Request | RequestInit) {\n const requestHook = this.getConfig().requestHook;\n\n if (requestHook) {\n safeExecuteInTheMiddle(\n () => requestHook(span, request),\n (error) => {\n if (error) {\n this._diag.error('requestHook', error);\n }\n },\n true,\n );\n }\n }\n\n /**\n * Add headers for the request and the given context if apply.\n * Returns the options to send, a copy when the caller passed an init object.\n */\n private _addHeaders(\n options: Request | RequestInit,\n url: string,\n ctx: Context,\n ): Request | RequestInit {\n // Propagate only if in request goes to same origin or is in the allow list\n const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;\n const urlOrigin = parseUrl(url).origin;\n const sameOrigin = location.origin === urlOrigin;\n const shouldPropagate = sameOrigin || matchesUrl(url, urlsToPropagate);\n\n if (shouldPropagate) {\n if (options instanceof Request) {\n // This mutates `Request.headers` in-place, because it is read-only\n // (per https://developer.mozilla.org/en-US/docs/Web/API/Request/headers),\n // so we cannot (easily) replace it.\n propagation.inject(ctx, options.headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n } else {\n // Otherwise, create a new Headers and a new init to avoid mutating\n // the caller's possibly re-used init and headers.\n const headers = new Headers(options.headers);\n propagation.inject(ctx, headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n return { ...options, headers };\n }\n } else {\n const headers: Partial<Record<string, unknown>> = {};\n propagation.inject(ctx, headers);\n if (Object.keys(headers).length > 0) {\n this._diag.debug('headers inject skipped due to CORS policy');\n }\n }\n return options;\n }\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
7
7
  * network error), per
8
8
  * https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status
9
9
  */
10
- interface FetchResponse {
10
+ export interface FetchResponse {
11
11
  status?: number;
12
12
  /**
13
13
  * Error identifier used as `error.type` (e.g. the exception's `name`).
@@ -21,15 +21,15 @@ interface FetchResponse {
21
21
  /**
22
22
  * Interface used to provide information to finish span on fetch error
23
23
  */
24
- interface FetchError {
24
+ export interface FetchError {
25
25
  status?: number;
26
26
  message: string;
27
27
  /** The error's exception type (e.g. `TypeError`, `AbortError`), if available. */
28
28
  name?: string;
29
29
  }
30
- type FetchCustomAttributeFunction = (span: Span, request: Request | RequestInit, result: Response | FetchError) => void;
31
- type FetchRequestHookFunction = (span: Span, request: Request | RequestInit) => void;
32
- interface FetchInstrumentationConfig extends InstrumentationConfig {
30
+ export type FetchCustomAttributeFunction = (span: Span, request: Request | RequestInit, result: Response | FetchError) => void;
31
+ export type FetchRequestHookFunction = (span: Span, request: Request | RequestInit) => void;
32
+ export interface FetchInstrumentationConfig extends InstrumentationConfig {
33
33
  /** urls which should include trace headers when origin doesn't match */
34
34
  propagateTraceHeaderCorsUrls?: Array<string | RegExp>;
35
35
  /**
@@ -48,5 +48,4 @@ interface FetchInstrumentationConfig extends InstrumentationConfig {
48
48
  sanitizeUrl?: (url: string) => string;
49
49
  }
50
50
  //#endregion
51
- export { FetchCustomAttributeFunction, FetchError, FetchInstrumentationConfig, FetchRequestHookFunction, FetchResponse };
52
51
  //# sourceMappingURL=types.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { NavigationInstrumentationConfig } from "./types.js";
2
2
  import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
3
  //#region src/navigation/instrumentation.d.ts
4
- declare class NavigationInstrumentation extends InstrumentationBase<NavigationInstrumentationConfig> {
4
+ export declare class NavigationInstrumentation extends InstrumentationBase<NavigationInstrumentationConfig> {
5
5
  private _isEnabled;
6
6
  private _isHistoryPatched;
7
7
  private _hasProcessedInitialLoad;
@@ -24,5 +24,4 @@ declare class NavigationInstrumentation extends InstrumentationBase<NavigationIn
24
24
  private _mapChangeStateToType;
25
25
  }
26
26
  //#endregion
27
- export { NavigationInstrumentation };
28
27
  //# sourceMappingURL=instrumentation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/navigation/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport { version } from '../../package.json' with { type: 'json' };\nimport {\n ATTR_BROWSER_NAVIGATION_HASH_CHANGE,\n ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT,\n ATTR_BROWSER_NAVIGATION_TYPE,\n ATTR_URL_FULL,\n BROWSER_NAVIGATION_EVENT_NAME,\n} from './semconv.ts';\nimport type {\n NavigationInstrumentationConfig,\n NavigationType,\n} from './types.ts';\nimport { isHashChange } from './utils.ts';\n\ntype ChangeState =\n | 'pushState'\n | 'replaceState'\n | 'popstate'\n | 'currententrychange';\n\ninterface NavigationApiEntry {\n url?: string;\n}\n\ninterface NavigationApiTarget {\n currentEntry?: NavigationApiEntry;\n}\n\ninterface NavigationApiEvent extends Event {\n navigationType?: NavigationType;\n target: NavigationApiTarget & EventTarget;\n}\n\ninterface NavigationApi {\n addEventListener(\n type: 'currententrychange',\n listener: (event: NavigationApiEvent) => void,\n ): void;\n removeEventListener(\n type: 'currententrychange',\n listener: (event: NavigationApiEvent) => void,\n ): void;\n}\n\nexport class NavigationInstrumentation extends InstrumentationBase<NavigationInstrumentationConfig> {\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n private declare _isEnabled: boolean;\n private declare _isHistoryPatched: boolean;\n private declare _hasProcessedInitialLoad: boolean;\n private declare _lastUrl: string;\n private declare _onDOMContentLoaded?: () => void;\n private declare _onPopState?: (event: PopStateEvent) => void;\n private declare _onCurrentEntryChange?: (event: NavigationApiEvent) => void;\n\n constructor(config: NavigationInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/navigation', version, config);\n this._lastUrl = location.href;\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n this._isEnabled = true;\n\n const navigationApi = this._getNavigationApi();\n\n // Only patch history API if Navigation API is not being used.\n if (!navigationApi && !this._isHistoryPatched) {\n this._patchHistoryApi();\n this._isHistoryPatched = true;\n }\n\n this._waitForPageLoad();\n\n if (navigationApi) {\n this._onCurrentEntryChange = (event) => {\n this._onSoftNavigation('currententrychange', event);\n };\n navigationApi.addEventListener(\n 'currententrychange',\n this._onCurrentEntryChange,\n );\n } else {\n this._onPopState = () => {\n this._onSoftNavigation('popstate');\n };\n window.addEventListener('popstate', this._onPopState);\n }\n }\n\n override disable(): void {\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n\n if (this._onDOMContentLoaded) {\n document.removeEventListener(\n 'DOMContentLoaded',\n this._onDOMContentLoaded,\n );\n this._onDOMContentLoaded = undefined;\n }\n if (this._onPopState) {\n window.removeEventListener('popstate', this._onPopState);\n this._onPopState = undefined;\n }\n if (this._onCurrentEntryChange) {\n const navigationApi = this._getNavigationApi();\n navigationApi?.removeEventListener(\n 'currententrychange',\n this._onCurrentEntryChange,\n );\n this._onCurrentEntryChange = undefined;\n }\n // Reset the initial-load flag so it can be processed again if re-enabled.\n this._hasProcessedInitialLoad = false;\n }\n\n private _getNavigationApi(): NavigationApi | undefined {\n const cfg = this.getConfig();\n if (!cfg.useNavigationApiIfAvailable) {\n return undefined;\n }\n return (window as unknown as { navigation?: NavigationApi }).navigation;\n }\n\n private _onHardNavigation(): void {\n const cfg = this.getConfig();\n const logRecord: LogRecord = {\n eventName: BROWSER_NAVIGATION_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_URL_FULL]: cfg.sanitizeUrl\n ? cfg.sanitizeUrl(document.documentURI)\n : document.documentURI,\n [ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: false,\n [ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: false,\n },\n };\n this._applyCustomLogRecordData(logRecord);\n this.logger.emit(logRecord);\n }\n\n private _onSoftNavigation(\n changeState: ChangeState,\n navigationEvent?: NavigationApiEvent,\n ): void {\n const referrerUrl = this._lastUrl;\n const currentUrl =\n changeState === 'currententrychange' &&\n navigationEvent?.target?.currentEntry?.url\n ? navigationEvent.target.currentEntry.url\n : location.href;\n\n if (referrerUrl === currentUrl) {\n return;\n }\n\n const navType = this._mapChangeStateToType(changeState, navigationEvent);\n const sameDocument = this._determineSameDocument(referrerUrl, currentUrl);\n const hashChange = isHashChange(referrerUrl, currentUrl);\n const cfg = this.getConfig();\n\n const logRecord: LogRecord = {\n eventName: BROWSER_NAVIGATION_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_URL_FULL]: cfg.sanitizeUrl\n ? cfg.sanitizeUrl(currentUrl)\n : currentUrl,\n [ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: sameDocument,\n [ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: hashChange,\n ...(navType ? { [ATTR_BROWSER_NAVIGATION_TYPE]: navType } : {}),\n },\n };\n this._applyCustomLogRecordData(logRecord);\n this.logger.emit(logRecord);\n\n this._lastUrl = currentUrl;\n }\n\n private _waitForPageLoad(): void {\n if (document.readyState === 'complete' && !this._hasProcessedInitialLoad) {\n this._hasProcessedInitialLoad = true;\n this._onHardNavigation();\n return;\n }\n\n this._onDOMContentLoaded = () => {\n if (!this._hasProcessedInitialLoad) {\n this._hasProcessedInitialLoad = true;\n this._onHardNavigation();\n }\n };\n document.addEventListener('DOMContentLoaded', this._onDOMContentLoaded);\n }\n\n private _patchHistoryApi(): void {\n this._wrap(\n history,\n 'replaceState',\n this._patchHistoryMethod('replaceState'),\n );\n this._wrap(history, 'pushState', this._patchHistoryMethod('pushState'));\n }\n\n private _patchHistoryMethod(changeState: 'pushState' | 'replaceState') {\n const plugin = this;\n return (original: History['pushState' | 'replaceState']) => {\n return function patchedHistoryMethod(\n this: History,\n ...args: Parameters<History['pushState' | 'replaceState']>\n ) {\n if (!plugin._isEnabled) {\n return original.apply(this, args);\n }\n const result = original.apply(this, args);\n if (location.href !== plugin._lastUrl) {\n plugin._onSoftNavigation(changeState);\n }\n return result;\n };\n };\n }\n\n private _applyCustomLogRecordData(logRecord: LogRecord): void {\n const cfg = this.getConfig();\n const hook = cfg.applyCustomLogRecordData;\n if (!hook) {\n return;\n }\n safeExecuteInTheMiddle(\n () => hook(logRecord),\n (error) => {\n if (error) {\n this._diag.error('applyCustomLogRecordData hook failed', error);\n }\n },\n true,\n );\n }\n\n private _determineSameDocument(fromUrl: string, toUrl: string): boolean {\n try {\n const fromURL = new URL(fromUrl);\n const toURL = new URL(toUrl);\n return fromURL.origin === toURL.origin;\n } catch {\n // Fallback: assume same document for relative URLs or parsing errors.\n // In SPAs, route changes via pushState/replaceState are same-document.\n return true;\n }\n }\n\n private _mapChangeStateToType(\n changeState: ChangeState,\n navigationEvent?: NavigationApiEvent,\n ): NavigationType | undefined {\n if (changeState === 'currententrychange') {\n const navType = navigationEvent?.navigationType;\n switch (navType) {\n case 'traverse':\n return 'traverse';\n case 'replace':\n return 'replace';\n case 'reload':\n return 'reload';\n default:\n // Default to 'push' for programmatic navigations (history.pushState,\n // link clicks) when no explicit type info is available.\n return 'push';\n }\n }\n\n switch (changeState) {\n case 'pushState':\n return 'push';\n case 'replaceState':\n return 'replace';\n case 'popstate':\n return 'traverse';\n default:\n return undefined;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/navigation/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport { version } from '../../package.json' with { type: 'json' };\nimport {\n ATTR_BROWSER_NAVIGATION_HASH_CHANGE,\n ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT,\n ATTR_BROWSER_NAVIGATION_TYPE,\n ATTR_URL_FULL,\n BROWSER_NAVIGATION_EVENT_NAME,\n} from './semconv.ts';\nimport type {\n NavigationInstrumentationConfig,\n NavigationType,\n} from './types.ts';\nimport { isHashChange } from './utils.ts';\n\ntype ChangeState =\n | 'pushState'\n | 'replaceState'\n | 'popstate'\n | 'currententrychange';\n\ninterface NavigationApiEntry {\n url?: string;\n}\n\ninterface NavigationApiTarget {\n currentEntry?: NavigationApiEntry;\n}\n\ninterface NavigationApiEvent extends Event {\n navigationType?: NavigationType;\n target: NavigationApiTarget & EventTarget;\n}\n\ninterface NavigationApi {\n addEventListener(\n type: 'currententrychange',\n listener: (event: NavigationApiEvent) => void,\n ): void;\n removeEventListener(\n type: 'currententrychange',\n listener: (event: NavigationApiEvent) => void,\n ): void;\n}\n\nexport class NavigationInstrumentation extends InstrumentationBase<NavigationInstrumentationConfig> {\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n declare private _isEnabled: boolean;\n declare private _isHistoryPatched: boolean;\n declare private _hasProcessedInitialLoad: boolean;\n declare private _lastUrl: string;\n declare private _onDOMContentLoaded?: () => void;\n declare private _onPopState?: (event: PopStateEvent) => void;\n declare private _onCurrentEntryChange?: (event: NavigationApiEvent) => void;\n\n constructor(config: NavigationInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/navigation', version, config);\n this._lastUrl = location.href;\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n this._isEnabled = true;\n\n const navigationApi = this._getNavigationApi();\n\n // Only patch history API if Navigation API is not being used.\n if (!navigationApi && !this._isHistoryPatched) {\n this._patchHistoryApi();\n this._isHistoryPatched = true;\n }\n\n this._waitForPageLoad();\n\n if (navigationApi) {\n this._onCurrentEntryChange = (event) => {\n this._onSoftNavigation('currententrychange', event);\n };\n navigationApi.addEventListener(\n 'currententrychange',\n this._onCurrentEntryChange,\n );\n } else {\n this._onPopState = () => {\n this._onSoftNavigation('popstate');\n };\n window.addEventListener('popstate', this._onPopState);\n }\n }\n\n override disable(): void {\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n\n if (this._onDOMContentLoaded) {\n document.removeEventListener(\n 'DOMContentLoaded',\n this._onDOMContentLoaded,\n );\n this._onDOMContentLoaded = undefined;\n }\n if (this._onPopState) {\n window.removeEventListener('popstate', this._onPopState);\n this._onPopState = undefined;\n }\n if (this._onCurrentEntryChange) {\n const navigationApi = this._getNavigationApi();\n navigationApi?.removeEventListener(\n 'currententrychange',\n this._onCurrentEntryChange,\n );\n this._onCurrentEntryChange = undefined;\n }\n // Reset the initial-load flag so it can be processed again if re-enabled.\n this._hasProcessedInitialLoad = false;\n }\n\n private _getNavigationApi(): NavigationApi | undefined {\n const cfg = this.getConfig();\n if (!cfg.useNavigationApiIfAvailable) {\n return undefined;\n }\n return (window as unknown as { navigation?: NavigationApi }).navigation;\n }\n\n private _onHardNavigation(): void {\n const cfg = this.getConfig();\n const logRecord: LogRecord = {\n eventName: BROWSER_NAVIGATION_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_URL_FULL]: cfg.sanitizeUrl\n ? cfg.sanitizeUrl(document.documentURI)\n : document.documentURI,\n [ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: false,\n [ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: false,\n },\n };\n this._applyCustomLogRecordData(logRecord);\n this.logger.emit(logRecord);\n }\n\n private _onSoftNavigation(\n changeState: ChangeState,\n navigationEvent?: NavigationApiEvent,\n ): void {\n const referrerUrl = this._lastUrl;\n const currentUrl =\n changeState === 'currententrychange' &&\n navigationEvent?.target?.currentEntry?.url\n ? navigationEvent.target.currentEntry.url\n : location.href;\n\n if (referrerUrl === currentUrl) {\n return;\n }\n\n const navType = this._mapChangeStateToType(changeState, navigationEvent);\n const sameDocument = this._determineSameDocument(referrerUrl, currentUrl);\n const hashChange = isHashChange(referrerUrl, currentUrl);\n const cfg = this.getConfig();\n\n const logRecord: LogRecord = {\n eventName: BROWSER_NAVIGATION_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_URL_FULL]: cfg.sanitizeUrl\n ? cfg.sanitizeUrl(currentUrl)\n : currentUrl,\n [ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: sameDocument,\n [ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: hashChange,\n ...(navType ? { [ATTR_BROWSER_NAVIGATION_TYPE]: navType } : {}),\n },\n };\n this._applyCustomLogRecordData(logRecord);\n this.logger.emit(logRecord);\n\n this._lastUrl = currentUrl;\n }\n\n private _waitForPageLoad(): void {\n if (document.readyState === 'complete' && !this._hasProcessedInitialLoad) {\n this._hasProcessedInitialLoad = true;\n this._onHardNavigation();\n return;\n }\n\n this._onDOMContentLoaded = () => {\n if (!this._hasProcessedInitialLoad) {\n this._hasProcessedInitialLoad = true;\n this._onHardNavigation();\n }\n };\n document.addEventListener('DOMContentLoaded', this._onDOMContentLoaded);\n }\n\n private _patchHistoryApi(): void {\n this._wrap(\n history,\n 'replaceState',\n this._patchHistoryMethod('replaceState'),\n );\n this._wrap(history, 'pushState', this._patchHistoryMethod('pushState'));\n }\n\n private _patchHistoryMethod(changeState: 'pushState' | 'replaceState') {\n const plugin = this;\n return (original: History['pushState' | 'replaceState']) => {\n return function patchedHistoryMethod(\n this: History,\n ...args: Parameters<History['pushState' | 'replaceState']>\n ) {\n if (!plugin._isEnabled) {\n return original.apply(this, args);\n }\n const result = original.apply(this, args);\n if (location.href !== plugin._lastUrl) {\n plugin._onSoftNavigation(changeState);\n }\n return result;\n };\n };\n }\n\n private _applyCustomLogRecordData(logRecord: LogRecord): void {\n const cfg = this.getConfig();\n const hook = cfg.applyCustomLogRecordData;\n if (!hook) {\n return;\n }\n safeExecuteInTheMiddle(\n () => hook(logRecord),\n (error) => {\n if (error) {\n this._diag.error('applyCustomLogRecordData hook failed', error);\n }\n },\n true,\n );\n }\n\n private _determineSameDocument(fromUrl: string, toUrl: string): boolean {\n try {\n const fromURL = new URL(fromUrl);\n const toURL = new URL(toUrl);\n return fromURL.origin === toURL.origin;\n } catch {\n // Fallback: assume same document for relative URLs or parsing errors.\n // In SPAs, route changes via pushState/replaceState are same-document.\n return true;\n }\n }\n\n private _mapChangeStateToType(\n changeState: ChangeState,\n navigationEvent?: NavigationApiEvent,\n ): NavigationType | undefined {\n if (changeState === 'currententrychange') {\n const navType = navigationEvent?.navigationType;\n switch (navType) {\n case 'traverse':\n return 'traverse';\n case 'replace':\n return 'replace';\n case 'reload':\n return 'reload';\n default:\n // Default to 'push' for programmatic navigations (history.pushState,\n // link clicks) when no explicit type info is available.\n return 'push';\n }\n }\n\n switch (changeState) {\n case 'pushState':\n return 'push';\n case 'replaceState':\n return 'replace';\n case 'popstate':\n return 'traverse';\n default:\n return undefined;\n }\n }\n}\n"],"mappings":"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"}
@@ -1,13 +1,13 @@
1
1
  import { LogRecord } from "@opentelemetry/api-logs";
2
2
  import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
3
  //#region src/navigation/types.d.ts
4
- type ApplyCustomLogRecordDataFunction = (logRecord: LogRecord) => void;
5
- type SanitizeUrlFunction = (url: string) => string;
6
- type NavigationType = 'push' | 'replace' | 'reload' | 'traverse';
4
+ export type ApplyCustomLogRecordDataFunction = (logRecord: LogRecord) => void;
5
+ export type SanitizeUrlFunction = (url: string) => string;
6
+ export type NavigationType = 'push' | 'replace' | 'reload' | 'traverse';
7
7
  /**
8
8
  * NavigationInstrumentation Configuration
9
9
  */
10
- interface NavigationInstrumentationConfig extends InstrumentationConfig {
10
+ export interface NavigationInstrumentationConfig extends InstrumentationConfig {
11
11
  /** Hook to modify log records before they are emitted. */
12
12
  applyCustomLogRecordData?: ApplyCustomLogRecordDataFunction;
13
13
  /** Use the Navigation API `currententrychange` event if available (experimental). Defaults to false. */
@@ -16,5 +16,4 @@ interface NavigationInstrumentationConfig extends InstrumentationConfig {
16
16
  sanitizeUrl?: SanitizeUrlFunction;
17
17
  }
18
18
  //#endregion
19
- export { ApplyCustomLogRecordDataFunction, NavigationInstrumentationConfig, NavigationType, SanitizeUrlFunction };
20
19
  //# sourceMappingURL=types.d.ts.map
@@ -13,10 +13,9 @@ function isHashChange(fromUrl, toUrl) {
13
13
  const a = new URL(fromUrl, window.location.origin);
14
14
  const b = new URL(toUrl, window.location.origin);
15
15
  const sameBase = a.origin === b.origin && a.pathname === b.pathname && a.search === b.search;
16
- const fromHasHash = a.hash !== "";
17
16
  const toHasHash = b.hash !== "";
18
17
  const hashesAreDifferent = a.hash !== b.hash;
19
- return sameBase && hashesAreDifferent && (fromHasHash && toHasHash || !fromHasHash && toHasHash);
18
+ return sameBase && hashesAreDifferent && toHasHash;
20
19
  } catch {
21
20
  const fromBase = fromUrl.split("#")[0];
22
21
  const toBase = toUrl.split("#")[0];
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":[],"sources":["../../src/navigation/utils.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Determines if navigation between two URLs represents a hash change.\n * A hash change is true if the URLs are the same except for the hash part,\n * AND the hash is being added or changed (not removed).\n *\n * @param fromUrl - The source URL\n * @param toUrl - The destination URL\n * @returns true if this represents a hash change navigation\n */\nexport function isHashChange(fromUrl: string, toUrl: string): boolean {\n try {\n const a = new URL(fromUrl, window.location.origin);\n const b = new URL(toUrl, window.location.origin);\n const sameBase =\n a.origin === b.origin &&\n a.pathname === b.pathname &&\n a.search === b.search;\n const fromHasHash = a.hash !== '';\n const toHasHash = b.hash !== '';\n const hashesAreDifferent = a.hash !== b.hash;\n\n return (\n sameBase &&\n hashesAreDifferent &&\n ((fromHasHash && toHasHash) || (!fromHasHash && toHasHash))\n );\n } catch {\n const fromBase = fromUrl.split('#')[0];\n const toBase = toUrl.split('#')[0];\n const fromHash = fromUrl.split('#')[1] ?? '';\n const toHash = toUrl.split('#')[1] ?? '';\n\n const sameBase = fromBase === toBase;\n const hashesAreDifferent = fromHash !== toHash;\n const notRemovingHash = toHash !== '';\n\n return sameBase && hashesAreDifferent && notRemovingHash;\n }\n}\n"],"mappings":";;;;;;;;;;AAcA,SAAgB,aAAa,SAAiB,OAAwB;CACpE,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,SAAS,OAAO,SAAS,MAAM;EACjD,MAAM,IAAI,IAAI,IAAI,OAAO,OAAO,SAAS,MAAM;EAC/C,MAAM,WACJ,EAAE,WAAW,EAAE,UACf,EAAE,aAAa,EAAE,YACjB,EAAE,WAAW,EAAE;EACjB,MAAM,cAAc,EAAE,SAAS;EAC/B,MAAM,YAAY,EAAE,SAAS;EAC7B,MAAM,qBAAqB,EAAE,SAAS,EAAE;EAExC,OACE,YACA,uBACE,eAAe,aAAe,CAAC,eAAe;CAEpD,QAAQ;EACN,MAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC;EACpC,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC;EAChC,MAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM;EAC1C,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;EAMtC,OAJiB,aAAa,UACH,aAAa,UAChB,WAAW;CAGrC;AACF"}
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../../src/navigation/utils.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Determines if navigation between two URLs represents a hash change.\n * A hash change is true if the URLs are the same except for the hash part,\n * AND the hash is being added or changed (not removed).\n *\n * @param fromUrl - The source URL\n * @param toUrl - The destination URL\n * @returns true if this represents a hash change navigation\n */\nexport function isHashChange(fromUrl: string, toUrl: string): boolean {\n try {\n const a = new URL(fromUrl, window.location.origin);\n const b = new URL(toUrl, window.location.origin);\n const sameBase =\n a.origin === b.origin &&\n a.pathname === b.pathname &&\n a.search === b.search;\n const toHasHash = b.hash !== '';\n const hashesAreDifferent = a.hash !== b.hash;\n\n return sameBase && hashesAreDifferent && toHasHash;\n } catch {\n const fromBase = fromUrl.split('#')[0];\n const toBase = toUrl.split('#')[0];\n const fromHash = fromUrl.split('#')[1] ?? '';\n const toHash = toUrl.split('#')[1] ?? '';\n\n const sameBase = fromBase === toBase;\n const hashesAreDifferent = fromHash !== toHash;\n const notRemovingHash = toHash !== '';\n\n return sameBase && hashesAreDifferent && notRemovingHash;\n }\n}\n"],"mappings":";;;;;;;;;;AAcA,SAAgB,aAAa,SAAiB,OAAwB;CACpE,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,SAAS,OAAO,SAAS,MAAM;EACjD,MAAM,IAAI,IAAI,IAAI,OAAO,OAAO,SAAS,MAAM;EAC/C,MAAM,WACJ,EAAE,WAAW,EAAE,UACf,EAAE,aAAa,EAAE,YACjB,EAAE,WAAW,EAAE;EACjB,MAAM,YAAY,EAAE,SAAS;EAC7B,MAAM,qBAAqB,EAAE,SAAS,EAAE;EAExC,OAAO,YAAY,sBAAsB;CAC3C,QAAQ;EACN,MAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC;EACpC,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC;EAChC,MAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM;EAC1C,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;EAMtC,OAJiB,aAAa,UACH,aAAa,UAChB,WAAW;CAGrC;AACF"}
@@ -4,7 +4,7 @@ import { InstrumentationBase } from "@opentelemetry/instrumentation";
4
4
  /**
5
5
  * This class automatically instruments navigation timing within the browser.
6
6
  */
7
- declare class NavigationTimingInstrumentation extends InstrumentationBase<NavigationTimingInstrumentationConfig> {
7
+ export declare class NavigationTimingInstrumentation extends InstrumentationBase<NavigationTimingInstrumentationConfig> {
8
8
  private _lastEntry?;
9
9
  private _completeDelayTimeoutId?;
10
10
  private _retryCount;
@@ -36,5 +36,4 @@ declare class NavigationTimingInstrumentation extends InstrumentationBase<Naviga
36
36
  private _unsubscribeAll;
37
37
  }
38
38
  //#endregion
39
- export { NavigationTimingInstrumentation };
40
39
  //# sourceMappingURL=instrumentation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/navigation-timing/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { version } from '../../package.json' with { type: 'json' };\nimport {\n ATTR_NAVIGATION_CONNECT_END,\n ATTR_NAVIGATION_CONNECT_START,\n ATTR_NAVIGATION_DECODED_BODY_SIZE,\n ATTR_NAVIGATION_DOM_COMPLETE,\n ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END,\n ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START,\n ATTR_NAVIGATION_DOM_INTERACTIVE,\n ATTR_NAVIGATION_DOMAIN_LOOKUP_END,\n ATTR_NAVIGATION_DOMAIN_LOOKUP_START,\n ATTR_NAVIGATION_DURATION,\n ATTR_NAVIGATION_ENCODED_BODY_SIZE,\n ATTR_NAVIGATION_FETCH_START,\n ATTR_NAVIGATION_LOAD_EVENT_END,\n ATTR_NAVIGATION_LOAD_EVENT_START,\n ATTR_NAVIGATION_REDIRECT_COUNT,\n ATTR_NAVIGATION_REQUEST_START,\n ATTR_NAVIGATION_RESPONSE_END,\n ATTR_NAVIGATION_RESPONSE_START,\n ATTR_NAVIGATION_SECURE_CONNECTION_START,\n ATTR_NAVIGATION_TRANSFER_SIZE,\n ATTR_NAVIGATION_TYPE,\n ATTR_NAVIGATION_UNLOAD_EVENT_END,\n ATTR_NAVIGATION_UNLOAD_EVENT_START,\n ATTR_NAVIGATION_URL,\n NAVIGATION_TIMING_EVENT_NAME,\n} from './semconv.ts';\nimport type { NavigationTimingInstrumentationConfig } from './types.ts';\n\nconst BASE_DELAY_MS = 50;\nconst MAX_RETRIES = 5;\n\n/**\n * This class automatically instruments navigation timing within the browser.\n */\nexport class NavigationTimingInstrumentation extends InstrumentationBase<NavigationTimingInstrumentationConfig> {\n private _lastEntry?: PerformanceNavigationTiming;\n private _completeDelayTimeoutId?: number;\n private _retryCount = 0;\n private _didEmit = false;\n\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n private declare _isEnabled: boolean;\n private declare _onLoad: () => void;\n private declare _onPageHide: () => void;\n\n constructor(config: NavigationTimingInstrumentationConfig = {}) {\n super(\n '@opentelemetry/browser-instrumentation/navigation-timing',\n version,\n config,\n );\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n this._isEnabled = true;\n this._onLoad = () => this._tryEmitOrSchedule();\n this._onPageHide = () => this._handleUnload();\n\n // Try emitting immediately (e.g. when enabled after load),\n // otherwise schedule for `load` or fall back to unload.\n this._tryEmitOrSchedule();\n if (this._didEmit) {\n return;\n }\n\n window.addEventListener('pagehide', this._onPageHide);\n }\n\n override disable(): void {\n this._isEnabled = false;\n this._unsubscribeAll();\n this._lastEntry = undefined;\n this._didEmit = false;\n this._retryCount = 0;\n }\n\n private _getLatestNavigationEntry(): PerformanceNavigationTiming | undefined {\n const entries = performance?.getEntriesByType?.('navigation') as\n | PerformanceNavigationTiming[]\n | undefined;\n if (!entries || entries.length === 0) {\n return;\n }\n\n return entries[entries.length - 1];\n }\n\n private _calculateBackoffDelay(): number {\n return this._retryCount * BASE_DELAY_MS;\n }\n\n /**\n * Attempts to emit the navigation timing event.\n *\n * - Emits immediately if a complete `PerformanceNavigationTiming` entry is available.\n * - If the page is still loading, waits for `window.load` and retries.\n * - If the page is already loaded but the entry is not finalized yet, schedules one\n * deferred re-check (to allow the browser to populate the timing fields).\n *\n * This method can be called multiple times (from `enable()`, the load handler, or the\n * deferred timeout), so it must be safe to re-enter.\n */\n private _tryEmitOrSchedule(): void {\n if (this._didEmit) {\n return;\n }\n\n const entry = this._getLatestNavigationEntry();\n if (entry) {\n this._lastEntry = entry;\n }\n\n // Prefer emitting a \"complete\" navigation entry.\n if (entry && entry.loadEventEnd > 0) {\n this._emitAndCleanup(entry);\n return;\n }\n\n // If the document is still loading, wait for `load` and try again.\n if (document.readyState !== 'complete') {\n window.addEventListener('load', this._onLoad, { once: true });\n return;\n }\n\n // If the document is already complete but navigation timings are not finalized yet,\n // schedule a deferred re-check with linear backoff to allow the browser to finish\n // populating the entry.\n if (this._completeDelayTimeoutId !== undefined) {\n return;\n }\n if (this._retryCount > MAX_RETRIES) {\n if (this._lastEntry) {\n this._diag.warn(\n 'Navigation timing: retries exhausted, emitting incomplete entry',\n );\n this._emitAndCleanup(this._lastEntry);\n } else {\n this._diag.warn(\n 'Navigation timing: retries exhausted with no entry available',\n );\n this._unsubscribeAll();\n }\n return;\n }\n\n const delay = this._calculateBackoffDelay();\n this._retryCount++;\n\n this._completeDelayTimeoutId = window.setTimeout(() => {\n this._completeDelayTimeoutId = undefined;\n this._tryEmitOrSchedule();\n }, delay);\n }\n\n private _handleUnload(): void {\n if (this._didEmit) {\n return;\n }\n\n const entry = this._getLatestNavigationEntry() ?? this._lastEntry;\n if (entry) {\n this._emitAndCleanup(entry);\n } else {\n this._diag.warn('Navigation timing: no entry available at unload');\n this._unsubscribeAll();\n }\n }\n\n private _emitAndCleanup(entry: PerformanceNavigationTiming): void {\n if (this._didEmit) {\n return;\n }\n\n this._didEmit = true;\n\n this._emitNavigationTiming(entry);\n this._lastEntry = undefined;\n this._unsubscribeAll();\n }\n\n private _emitNavigationTiming(entry: PerformanceNavigationTiming) {\n if (!entry) {\n return;\n }\n\n this.logger.emit({\n eventName: NAVIGATION_TIMING_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_NAVIGATION_TYPE]: entry.type,\n [ATTR_NAVIGATION_URL]: entry.name,\n [ATTR_NAVIGATION_DURATION]: entry.duration,\n [ATTR_NAVIGATION_DOM_COMPLETE]: entry.domComplete,\n [ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END]:\n entry.domContentLoadedEventEnd,\n [ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START]:\n entry.domContentLoadedEventStart,\n [ATTR_NAVIGATION_DOM_INTERACTIVE]: entry.domInteractive,\n [ATTR_NAVIGATION_LOAD_EVENT_END]: entry.loadEventEnd,\n [ATTR_NAVIGATION_LOAD_EVENT_START]: entry.loadEventStart,\n [ATTR_NAVIGATION_REDIRECT_COUNT]: entry.redirectCount,\n [ATTR_NAVIGATION_UNLOAD_EVENT_END]: entry.unloadEventEnd,\n [ATTR_NAVIGATION_UNLOAD_EVENT_START]: entry.unloadEventStart,\n [ATTR_NAVIGATION_FETCH_START]: entry.fetchStart,\n [ATTR_NAVIGATION_DOMAIN_LOOKUP_START]: entry.domainLookupStart,\n [ATTR_NAVIGATION_DOMAIN_LOOKUP_END]: entry.domainLookupEnd,\n [ATTR_NAVIGATION_CONNECT_START]: entry.connectStart,\n [ATTR_NAVIGATION_CONNECT_END]: entry.connectEnd,\n [ATTR_NAVIGATION_SECURE_CONNECTION_START]: entry.secureConnectionStart,\n [ATTR_NAVIGATION_REQUEST_START]: entry.requestStart,\n [ATTR_NAVIGATION_RESPONSE_START]: entry.responseStart,\n [ATTR_NAVIGATION_RESPONSE_END]: entry.responseEnd,\n [ATTR_NAVIGATION_TRANSFER_SIZE]: entry.transferSize,\n [ATTR_NAVIGATION_ENCODED_BODY_SIZE]: entry.encodedBodySize,\n [ATTR_NAVIGATION_DECODED_BODY_SIZE]: entry.decodedBodySize,\n },\n });\n }\n\n private _unsubscribeAll(): void {\n if (this._completeDelayTimeoutId) {\n clearTimeout(this._completeDelayTimeoutId);\n this._completeDelayTimeoutId = undefined;\n }\n\n if (this._onLoad) {\n window.removeEventListener('load', this._onLoad);\n }\n if (this._onPageHide) {\n window.removeEventListener('pagehide', this._onPageHide);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/navigation-timing/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { version } from '../../package.json' with { type: 'json' };\nimport {\n ATTR_NAVIGATION_CONNECT_END,\n ATTR_NAVIGATION_CONNECT_START,\n ATTR_NAVIGATION_DECODED_BODY_SIZE,\n ATTR_NAVIGATION_DOM_COMPLETE,\n ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END,\n ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START,\n ATTR_NAVIGATION_DOM_INTERACTIVE,\n ATTR_NAVIGATION_DOMAIN_LOOKUP_END,\n ATTR_NAVIGATION_DOMAIN_LOOKUP_START,\n ATTR_NAVIGATION_DURATION,\n ATTR_NAVIGATION_ENCODED_BODY_SIZE,\n ATTR_NAVIGATION_FETCH_START,\n ATTR_NAVIGATION_LOAD_EVENT_END,\n ATTR_NAVIGATION_LOAD_EVENT_START,\n ATTR_NAVIGATION_REDIRECT_COUNT,\n ATTR_NAVIGATION_REQUEST_START,\n ATTR_NAVIGATION_RESPONSE_END,\n ATTR_NAVIGATION_RESPONSE_START,\n ATTR_NAVIGATION_SECURE_CONNECTION_START,\n ATTR_NAVIGATION_TRANSFER_SIZE,\n ATTR_NAVIGATION_TYPE,\n ATTR_NAVIGATION_UNLOAD_EVENT_END,\n ATTR_NAVIGATION_UNLOAD_EVENT_START,\n ATTR_NAVIGATION_URL,\n NAVIGATION_TIMING_EVENT_NAME,\n} from './semconv.ts';\nimport type { NavigationTimingInstrumentationConfig } from './types.ts';\n\nconst BASE_DELAY_MS = 50;\nconst MAX_RETRIES = 5;\n\n/**\n * This class automatically instruments navigation timing within the browser.\n */\nexport class NavigationTimingInstrumentation extends InstrumentationBase<NavigationTimingInstrumentationConfig> {\n private _lastEntry?: PerformanceNavigationTiming;\n private _completeDelayTimeoutId?: number;\n private _retryCount = 0;\n private _didEmit = false;\n\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n declare private _isEnabled: boolean;\n declare private _onLoad: () => void;\n declare private _onPageHide: () => void;\n\n constructor(config: NavigationTimingInstrumentationConfig = {}) {\n super(\n '@opentelemetry/browser-instrumentation/navigation-timing',\n version,\n config,\n );\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n this._isEnabled = true;\n this._onLoad = () => this._tryEmitOrSchedule();\n this._onPageHide = () => this._handleUnload();\n\n // Try emitting immediately (e.g. when enabled after load),\n // otherwise schedule for `load` or fall back to unload.\n this._tryEmitOrSchedule();\n if (this._didEmit) {\n return;\n }\n\n window.addEventListener('pagehide', this._onPageHide);\n }\n\n override disable(): void {\n this._isEnabled = false;\n this._unsubscribeAll();\n this._lastEntry = undefined;\n this._didEmit = false;\n this._retryCount = 0;\n }\n\n private _getLatestNavigationEntry(): PerformanceNavigationTiming | undefined {\n const entries = performance?.getEntriesByType?.('navigation') as\n | PerformanceNavigationTiming[]\n | undefined;\n if (!entries || entries.length === 0) {\n return;\n }\n\n return entries[entries.length - 1];\n }\n\n private _calculateBackoffDelay(): number {\n return this._retryCount * BASE_DELAY_MS;\n }\n\n /**\n * Attempts to emit the navigation timing event.\n *\n * - Emits immediately if a complete `PerformanceNavigationTiming` entry is available.\n * - If the page is still loading, waits for `window.load` and retries.\n * - If the page is already loaded but the entry is not finalized yet, schedules one\n * deferred re-check (to allow the browser to populate the timing fields).\n *\n * This method can be called multiple times (from `enable()`, the load handler, or the\n * deferred timeout), so it must be safe to re-enter.\n */\n private _tryEmitOrSchedule(): void {\n if (this._didEmit) {\n return;\n }\n\n const entry = this._getLatestNavigationEntry();\n if (entry) {\n this._lastEntry = entry;\n }\n\n // Prefer emitting a \"complete\" navigation entry.\n if (entry && entry.loadEventEnd > 0) {\n this._emitAndCleanup(entry);\n return;\n }\n\n // If the document is still loading, wait for `load` and try again.\n if (document.readyState !== 'complete') {\n window.addEventListener('load', this._onLoad, { once: true });\n return;\n }\n\n // If the document is already complete but navigation timings are not finalized yet,\n // schedule a deferred re-check with linear backoff to allow the browser to finish\n // populating the entry.\n if (this._completeDelayTimeoutId !== undefined) {\n return;\n }\n if (this._retryCount > MAX_RETRIES) {\n if (this._lastEntry) {\n this._diag.warn(\n 'Navigation timing: retries exhausted, emitting incomplete entry',\n );\n this._emitAndCleanup(this._lastEntry);\n } else {\n this._diag.warn(\n 'Navigation timing: retries exhausted with no entry available',\n );\n this._unsubscribeAll();\n }\n return;\n }\n\n const delay = this._calculateBackoffDelay();\n this._retryCount++;\n\n this._completeDelayTimeoutId = window.setTimeout(() => {\n this._completeDelayTimeoutId = undefined;\n this._tryEmitOrSchedule();\n }, delay);\n }\n\n private _handleUnload(): void {\n if (this._didEmit) {\n return;\n }\n\n const entry = this._getLatestNavigationEntry() ?? this._lastEntry;\n if (entry) {\n this._emitAndCleanup(entry);\n } else {\n this._diag.warn('Navigation timing: no entry available at unload');\n this._unsubscribeAll();\n }\n }\n\n private _emitAndCleanup(entry: PerformanceNavigationTiming): void {\n if (this._didEmit) {\n return;\n }\n\n this._didEmit = true;\n\n this._emitNavigationTiming(entry);\n this._lastEntry = undefined;\n this._unsubscribeAll();\n }\n\n private _emitNavigationTiming(entry: PerformanceNavigationTiming) {\n if (!entry) {\n return;\n }\n\n this.logger.emit({\n eventName: NAVIGATION_TIMING_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_NAVIGATION_TYPE]: entry.type,\n [ATTR_NAVIGATION_URL]: entry.name,\n [ATTR_NAVIGATION_DURATION]: entry.duration,\n [ATTR_NAVIGATION_DOM_COMPLETE]: entry.domComplete,\n [ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END]:\n entry.domContentLoadedEventEnd,\n [ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START]:\n entry.domContentLoadedEventStart,\n [ATTR_NAVIGATION_DOM_INTERACTIVE]: entry.domInteractive,\n [ATTR_NAVIGATION_LOAD_EVENT_END]: entry.loadEventEnd,\n [ATTR_NAVIGATION_LOAD_EVENT_START]: entry.loadEventStart,\n [ATTR_NAVIGATION_REDIRECT_COUNT]: entry.redirectCount,\n [ATTR_NAVIGATION_UNLOAD_EVENT_END]: entry.unloadEventEnd,\n [ATTR_NAVIGATION_UNLOAD_EVENT_START]: entry.unloadEventStart,\n [ATTR_NAVIGATION_FETCH_START]: entry.fetchStart,\n [ATTR_NAVIGATION_DOMAIN_LOOKUP_START]: entry.domainLookupStart,\n [ATTR_NAVIGATION_DOMAIN_LOOKUP_END]: entry.domainLookupEnd,\n [ATTR_NAVIGATION_CONNECT_START]: entry.connectStart,\n [ATTR_NAVIGATION_CONNECT_END]: entry.connectEnd,\n [ATTR_NAVIGATION_SECURE_CONNECTION_START]: entry.secureConnectionStart,\n [ATTR_NAVIGATION_REQUEST_START]: entry.requestStart,\n [ATTR_NAVIGATION_RESPONSE_START]: entry.responseStart,\n [ATTR_NAVIGATION_RESPONSE_END]: entry.responseEnd,\n [ATTR_NAVIGATION_TRANSFER_SIZE]: entry.transferSize,\n [ATTR_NAVIGATION_ENCODED_BODY_SIZE]: entry.encodedBodySize,\n [ATTR_NAVIGATION_DECODED_BODY_SIZE]: entry.decodedBodySize,\n },\n });\n }\n\n private _unsubscribeAll(): void {\n if (this._completeDelayTimeoutId) {\n clearTimeout(this._completeDelayTimeoutId);\n this._completeDelayTimeoutId = undefined;\n }\n\n if (this._onLoad) {\n window.removeEventListener('load', this._onLoad);\n }\n if (this._onPageHide) {\n window.removeEventListener('pagehide', this._onPageHide);\n }\n }\n}\n"],"mappings":"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"}
@@ -3,7 +3,6 @@ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
3
  /**
4
4
  * NavigationTimingInstrumentation Configuration
5
5
  */
6
- interface NavigationTimingInstrumentationConfig extends InstrumentationConfig {}
6
+ export interface NavigationTimingInstrumentationConfig extends InstrumentationConfig {}
7
7
  //#endregion
8
- export { NavigationTimingInstrumentationConfig };
9
8
  //# sourceMappingURL=types.d.ts.map
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.8.0";
2
+ var version = "0.8.2";
3
3
  //#endregion
4
4
  export { version };
5
5
 
@@ -9,7 +9,7 @@ import { InstrumentationBase } from "@opentelemetry/instrumentation";
9
9
  * requestIdleCallback when available (with fallback for Safari) to ensure
10
10
  * processing happens during idle periods.
11
11
  */
12
- declare class ResourceTimingInstrumentation extends InstrumentationBase<ResourceTimingInstrumentationConfig> {
12
+ export declare class ResourceTimingInstrumentation extends InstrumentationBase<ResourceTimingInstrumentationConfig> {
13
13
  private _observer?;
14
14
  private _pendingEntries;
15
15
  private _idleHandle?;
@@ -28,5 +28,4 @@ declare class ResourceTimingInstrumentation extends InstrumentationBase<Resource
28
28
  private _cancelScheduledProcessing;
29
29
  }
30
30
  //#endregion
31
- export { ResourceTimingInstrumentation };
32
31
  //# sourceMappingURL=instrumentation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/resource-timing/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { getNetworkContextRegistry } from '#utils';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { matchesUrl } from '../utils/url.ts';\nimport type { IdleCallbackHandle } from './idle-callback-shim.ts';\nimport {\n cancelIdleCallbackShim,\n requestIdleCallbackShim,\n} from './idle-callback-shim.ts';\nimport {\n ATTR_RESOURCE_CONNECT_END,\n ATTR_RESOURCE_CONNECT_START,\n ATTR_RESOURCE_DECODED_BODY_SIZE,\n ATTR_RESOURCE_DOMAIN_LOOKUP_END,\n ATTR_RESOURCE_DOMAIN_LOOKUP_START,\n ATTR_RESOURCE_DURATION,\n ATTR_RESOURCE_ENCODED_BODY_SIZE,\n ATTR_RESOURCE_FETCH_START,\n ATTR_RESOURCE_INITIATOR_TYPE,\n ATTR_RESOURCE_NEXT_HOP_PROTOCOL,\n ATTR_RESOURCE_REDIRECT_END,\n ATTR_RESOURCE_REDIRECT_START,\n ATTR_RESOURCE_RENDER_BLOCKING_STATUS,\n ATTR_RESOURCE_REQUEST_START,\n ATTR_RESOURCE_RESPONSE_END,\n ATTR_RESOURCE_RESPONSE_START,\n ATTR_RESOURCE_SECURE_CONNECTION_START,\n ATTR_RESOURCE_TRANSFER_SIZE,\n ATTR_RESOURCE_URL,\n ATTR_RESOURCE_WORKER_START,\n RESOURCE_TIMING_EVENT_NAME,\n} from './semconv.ts';\nimport type { ResourceTimingInstrumentationConfig } from './types.ts';\n\nconst DEFAULT_BATCH_SIZE = 50;\nconst DEFAULT_FORCE_PROCESSING_AFTER = 1000;\nconst DEFAULT_MAX_PROCESSING_TIME = 50;\nconst DEFAULT_MAX_QUEUE_SIZE = 1000;\n\nconst MIN_BATCH_SIZE = 1;\nconst MIN_FORCE_PROCESSING_AFTER = 0;\nconst MIN_PROCESSING_TIME = 0;\nconst MIN_QUEUE_SIZE = 1;\n\n/**\n * OpenTelemetry instrumentation for resource timing for browser applications.\n *\n * This instrumentation captures resource timing data using PerformanceObserver\n * and batches emissions to avoid overwhelming the main thread. It uses\n * requestIdleCallback when available (with fallback for Safari) to ensure\n * processing happens during idle periods.\n */\nexport class ResourceTimingInstrumentation extends InstrumentationBase<ResourceTimingInstrumentationConfig> {\n private _observer?: PerformanceObserver;\n private _pendingEntries: PerformanceResourceTiming[] = [];\n private _idleHandle?: IdleCallbackHandle;\n\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n private declare _isEnabled: boolean;\n private declare _loadHandler: (() => void) | undefined;\n private declare _visibilityChangeHandler: (() => void) | undefined;\n\n constructor(config: ResourceTimingInstrumentationConfig = {}) {\n super(\n '@opentelemetry/browser-instrumentation/resource-timing',\n version,\n config,\n );\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n\n if (!('PerformanceObserver' in window)) {\n this._diag.debug(\n 'PerformanceObserver is not supported, resource timings will not be collected',\n );\n return;\n }\n\n this._isEnabled = true;\n\n if (document.readyState === 'complete') {\n this._setupObserver();\n } else {\n this._loadHandler = () => this._setupObserver();\n window.addEventListener('load', this._loadHandler, { once: true });\n }\n\n this._visibilityChangeHandler = () => {\n if (document.hidden) {\n this._flush();\n }\n };\n document.addEventListener(\n 'visibilitychange',\n this._visibilityChangeHandler,\n );\n }\n\n override disable(): void {\n this._isEnabled = false;\n this._flush();\n this._observer?.disconnect();\n this._observer = undefined;\n if (this._loadHandler) {\n window.removeEventListener('load', this._loadHandler);\n this._loadHandler = undefined;\n }\n if (this._visibilityChangeHandler) {\n document.removeEventListener(\n 'visibilitychange',\n this._visibilityChangeHandler,\n );\n this._visibilityChangeHandler = undefined;\n }\n }\n\n private _setupObserver(): void {\n if (!this._isEnabled) {\n return;\n }\n\n const observer = new PerformanceObserver((list) => {\n if (!this._isEnabled) {\n return;\n }\n\n const maxQueueSize = Math.max(\n this._config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,\n MIN_QUEUE_SIZE,\n );\n const entries = list.getEntries() as PerformanceResourceTiming[];\n const initiatorTypes = this._config.initiatorTypes;\n\n for (const entry of entries) {\n if (\n initiatorTypes !== undefined &&\n !initiatorTypes.includes(entry.initiatorType)\n ) {\n continue;\n }\n\n const shouldIgnoreUrl = matchesUrl(entry.name, this._config.ignoreUrls);\n if (shouldIgnoreUrl) {\n continue;\n }\n\n if (this._pendingEntries.length >= maxQueueSize) {\n this._flush();\n }\n this._pendingEntries.push(entry);\n }\n\n if (this._pendingEntries.length > 0) {\n this._scheduleProcessing();\n }\n });\n\n this._observer = observer;\n\n try {\n observer.observe({ type: 'resource', buffered: true });\n } catch (e) {\n this._diag.error('Failed to start resource PerformanceObserver', e);\n this._observer = undefined;\n }\n }\n\n private _scheduleProcessing(): void {\n if (this._idleHandle !== undefined) {\n return;\n }\n\n const timeout = Math.max(\n this._config.forceProcessingAfter ?? DEFAULT_FORCE_PROCESSING_AFTER,\n MIN_FORCE_PROCESSING_AFTER,\n );\n this._idleHandle = requestIdleCallbackShim(\n (deadline) => this._processChunk(deadline),\n { timeout },\n );\n }\n\n private _processChunk(deadline: IdleDeadline): void {\n this._idleHandle = undefined;\n if (!this._isEnabled || this._pendingEntries.length === 0) {\n return;\n }\n\n const maxTime = Math.max(\n this._config.maxProcessingTime ?? DEFAULT_MAX_PROCESSING_TIME,\n MIN_PROCESSING_TIME,\n );\n const batchSize = Math.max(\n this._config.batchSize ?? DEFAULT_BATCH_SIZE,\n MIN_BATCH_SIZE,\n );\n const startTime = performance.now();\n\n let cursor = 0;\n try {\n for (let i = 0; i < batchSize; i++) {\n const entry = this._pendingEntries[cursor];\n if (entry === undefined) {\n break;\n }\n\n const elapsed = performance.now() - startTime;\n // timeRemaining() is not Baseline widely available (no Safari support),\n // compatibility is handled by idle-callback-shim.ts.\n // eslint-disable-next-line baseline-js/use-baseline\n if (elapsed >= maxTime || deadline.timeRemaining() < 1) {\n break;\n }\n\n cursor++;\n this._emitResource(entry);\n }\n } finally {\n this._pendingEntries.splice(0, cursor);\n if (this._pendingEntries.length > 0) {\n this._scheduleProcessing();\n }\n }\n }\n\n private _emitResource(entry: PerformanceResourceTiming): void {\n try {\n const record: LogRecord = {\n eventName: RESOURCE_TIMING_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_RESOURCE_URL]: entry.name,\n [ATTR_RESOURCE_INITIATOR_TYPE]: entry.initiatorType,\n [ATTR_RESOURCE_DURATION]: entry.duration,\n [ATTR_RESOURCE_FETCH_START]: entry.fetchStart,\n [ATTR_RESOURCE_DOMAIN_LOOKUP_START]: entry.domainLookupStart,\n [ATTR_RESOURCE_DOMAIN_LOOKUP_END]: entry.domainLookupEnd,\n [ATTR_RESOURCE_CONNECT_START]: entry.connectStart,\n [ATTR_RESOURCE_CONNECT_END]: entry.connectEnd,\n [ATTR_RESOURCE_SECURE_CONNECTION_START]: entry.secureConnectionStart,\n [ATTR_RESOURCE_REQUEST_START]: entry.requestStart,\n [ATTR_RESOURCE_RESPONSE_START]: entry.responseStart,\n [ATTR_RESOURCE_RESPONSE_END]: entry.responseEnd,\n [ATTR_RESOURCE_TRANSFER_SIZE]: entry.transferSize,\n [ATTR_RESOURCE_ENCODED_BODY_SIZE]: entry.encodedBodySize,\n [ATTR_RESOURCE_DECODED_BODY_SIZE]: entry.decodedBodySize,\n [ATTR_RESOURCE_REDIRECT_START]: entry.redirectStart,\n [ATTR_RESOURCE_REDIRECT_END]: entry.redirectEnd,\n [ATTR_RESOURCE_WORKER_START]: entry.workerStart,\n [ATTR_RESOURCE_NEXT_HOP_PROTOCOL]: entry.nextHopProtocol,\n // @ts-expect-error renderBlockingStatus is only available in Chromium as of March 2026\n [ATTR_RESOURCE_RENDER_BLOCKING_STATUS]: entry.renderBlockingStatus,\n },\n };\n const registry = getNetworkContextRegistry();\n const ctx = registry.getContext(entry);\n // Attach the context to the LogRecord and remove it from the registry\n if (ctx) {\n record.context = ctx;\n registry.unregister(entry);\n }\n this.logger.emit(record);\n } catch (error) {\n this._diag.error(\n `Failed to emit resource timing entry for \"${entry.name}\"`,\n error,\n );\n }\n }\n\n private _flush(): void {\n this._cancelScheduledProcessing();\n\n for (const entry of this._pendingEntries) {\n this._emitResource(entry);\n }\n this._pendingEntries = [];\n }\n\n private _cancelScheduledProcessing(): void {\n if (this._idleHandle !== undefined) {\n cancelIdleCallbackShim(this._idleHandle);\n this._idleHandle = undefined;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/resource-timing/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { getNetworkContextRegistry } from '#utils';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { matchesUrl } from '../utils/url.ts';\nimport type { IdleCallbackHandle } from './idle-callback-shim.ts';\nimport {\n cancelIdleCallbackShim,\n requestIdleCallbackShim,\n} from './idle-callback-shim.ts';\nimport {\n ATTR_RESOURCE_CONNECT_END,\n ATTR_RESOURCE_CONNECT_START,\n ATTR_RESOURCE_DECODED_BODY_SIZE,\n ATTR_RESOURCE_DOMAIN_LOOKUP_END,\n ATTR_RESOURCE_DOMAIN_LOOKUP_START,\n ATTR_RESOURCE_DURATION,\n ATTR_RESOURCE_ENCODED_BODY_SIZE,\n ATTR_RESOURCE_FETCH_START,\n ATTR_RESOURCE_INITIATOR_TYPE,\n ATTR_RESOURCE_NEXT_HOP_PROTOCOL,\n ATTR_RESOURCE_REDIRECT_END,\n ATTR_RESOURCE_REDIRECT_START,\n ATTR_RESOURCE_RENDER_BLOCKING_STATUS,\n ATTR_RESOURCE_REQUEST_START,\n ATTR_RESOURCE_RESPONSE_END,\n ATTR_RESOURCE_RESPONSE_START,\n ATTR_RESOURCE_SECURE_CONNECTION_START,\n ATTR_RESOURCE_TRANSFER_SIZE,\n ATTR_RESOURCE_URL,\n ATTR_RESOURCE_WORKER_START,\n RESOURCE_TIMING_EVENT_NAME,\n} from './semconv.ts';\nimport type { ResourceTimingInstrumentationConfig } from './types.ts';\n\nconst DEFAULT_BATCH_SIZE = 50;\nconst DEFAULT_FORCE_PROCESSING_AFTER = 1000;\nconst DEFAULT_MAX_PROCESSING_TIME = 50;\nconst DEFAULT_MAX_QUEUE_SIZE = 1000;\n\nconst MIN_BATCH_SIZE = 1;\nconst MIN_FORCE_PROCESSING_AFTER = 0;\nconst MIN_PROCESSING_TIME = 0;\nconst MIN_QUEUE_SIZE = 1;\n\n/**\n * OpenTelemetry instrumentation for resource timing for browser applications.\n *\n * This instrumentation captures resource timing data using PerformanceObserver\n * and batches emissions to avoid overwhelming the main thread. It uses\n * requestIdleCallback when available (with fallback for Safari) to ensure\n * processing happens during idle periods.\n */\nexport class ResourceTimingInstrumentation extends InstrumentationBase<ResourceTimingInstrumentationConfig> {\n private _observer?: PerformanceObserver;\n private _pendingEntries: PerformanceResourceTiming[] = [];\n private _idleHandle?: IdleCallbackHandle;\n\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n declare private _isEnabled: boolean;\n declare private _loadHandler: (() => void) | undefined;\n declare private _visibilityChangeHandler: (() => void) | undefined;\n\n constructor(config: ResourceTimingInstrumentationConfig = {}) {\n super(\n '@opentelemetry/browser-instrumentation/resource-timing',\n version,\n config,\n );\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n\n if (!('PerformanceObserver' in window)) {\n this._diag.debug(\n 'PerformanceObserver is not supported, resource timings will not be collected',\n );\n return;\n }\n\n this._isEnabled = true;\n\n if (document.readyState === 'complete') {\n this._setupObserver();\n } else {\n this._loadHandler = () => this._setupObserver();\n window.addEventListener('load', this._loadHandler, { once: true });\n }\n\n this._visibilityChangeHandler = () => {\n if (document.hidden) {\n this._flush();\n }\n };\n document.addEventListener(\n 'visibilitychange',\n this._visibilityChangeHandler,\n );\n }\n\n override disable(): void {\n this._isEnabled = false;\n this._flush();\n this._observer?.disconnect();\n this._observer = undefined;\n if (this._loadHandler) {\n window.removeEventListener('load', this._loadHandler);\n this._loadHandler = undefined;\n }\n if (this._visibilityChangeHandler) {\n document.removeEventListener(\n 'visibilitychange',\n this._visibilityChangeHandler,\n );\n this._visibilityChangeHandler = undefined;\n }\n }\n\n private _setupObserver(): void {\n if (!this._isEnabled) {\n return;\n }\n\n const observer = new PerformanceObserver((list) => {\n if (!this._isEnabled) {\n return;\n }\n\n const maxQueueSize = Math.max(\n this._config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,\n MIN_QUEUE_SIZE,\n );\n const entries = list.getEntries() as PerformanceResourceTiming[];\n const initiatorTypes = this._config.initiatorTypes;\n\n for (const entry of entries) {\n if (\n initiatorTypes !== undefined &&\n !initiatorTypes.includes(entry.initiatorType)\n ) {\n continue;\n }\n\n const shouldIgnoreUrl = matchesUrl(entry.name, this._config.ignoreUrls);\n if (shouldIgnoreUrl) {\n continue;\n }\n\n if (this._pendingEntries.length >= maxQueueSize) {\n this._flush();\n }\n this._pendingEntries.push(entry);\n }\n\n if (this._pendingEntries.length > 0) {\n this._scheduleProcessing();\n }\n });\n\n this._observer = observer;\n\n try {\n observer.observe({ type: 'resource', buffered: true });\n } catch (e) {\n this._diag.error('Failed to start resource PerformanceObserver', e);\n this._observer = undefined;\n }\n }\n\n private _scheduleProcessing(): void {\n if (this._idleHandle !== undefined) {\n return;\n }\n\n const timeout = Math.max(\n this._config.forceProcessingAfter ?? DEFAULT_FORCE_PROCESSING_AFTER,\n MIN_FORCE_PROCESSING_AFTER,\n );\n this._idleHandle = requestIdleCallbackShim(\n (deadline) => this._processChunk(deadline),\n { timeout },\n );\n }\n\n private _processChunk(deadline: IdleDeadline): void {\n this._idleHandle = undefined;\n if (!this._isEnabled || this._pendingEntries.length === 0) {\n return;\n }\n\n const maxTime = Math.max(\n this._config.maxProcessingTime ?? DEFAULT_MAX_PROCESSING_TIME,\n MIN_PROCESSING_TIME,\n );\n const batchSize = Math.max(\n this._config.batchSize ?? DEFAULT_BATCH_SIZE,\n MIN_BATCH_SIZE,\n );\n const startTime = performance.now();\n\n let cursor = 0;\n try {\n for (let i = 0; i < batchSize; i++) {\n const entry = this._pendingEntries[cursor];\n if (entry === undefined) {\n break;\n }\n\n const elapsed = performance.now() - startTime;\n // timeRemaining() is not Baseline widely available (no Safari support),\n // compatibility is handled by idle-callback-shim.ts.\n // eslint-disable-next-line baseline-js/use-baseline\n if (elapsed >= maxTime || deadline.timeRemaining() < 1) {\n break;\n }\n\n cursor++;\n this._emitResource(entry);\n }\n } finally {\n this._pendingEntries.splice(0, cursor);\n if (this._pendingEntries.length > 0) {\n this._scheduleProcessing();\n }\n }\n }\n\n private _emitResource(entry: PerformanceResourceTiming): void {\n try {\n const record: LogRecord = {\n eventName: RESOURCE_TIMING_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_RESOURCE_URL]: entry.name,\n [ATTR_RESOURCE_INITIATOR_TYPE]: entry.initiatorType,\n [ATTR_RESOURCE_DURATION]: entry.duration,\n [ATTR_RESOURCE_FETCH_START]: entry.fetchStart,\n [ATTR_RESOURCE_DOMAIN_LOOKUP_START]: entry.domainLookupStart,\n [ATTR_RESOURCE_DOMAIN_LOOKUP_END]: entry.domainLookupEnd,\n [ATTR_RESOURCE_CONNECT_START]: entry.connectStart,\n [ATTR_RESOURCE_CONNECT_END]: entry.connectEnd,\n [ATTR_RESOURCE_SECURE_CONNECTION_START]: entry.secureConnectionStart,\n [ATTR_RESOURCE_REQUEST_START]: entry.requestStart,\n [ATTR_RESOURCE_RESPONSE_START]: entry.responseStart,\n [ATTR_RESOURCE_RESPONSE_END]: entry.responseEnd,\n [ATTR_RESOURCE_TRANSFER_SIZE]: entry.transferSize,\n [ATTR_RESOURCE_ENCODED_BODY_SIZE]: entry.encodedBodySize,\n [ATTR_RESOURCE_DECODED_BODY_SIZE]: entry.decodedBodySize,\n [ATTR_RESOURCE_REDIRECT_START]: entry.redirectStart,\n [ATTR_RESOURCE_REDIRECT_END]: entry.redirectEnd,\n [ATTR_RESOURCE_WORKER_START]: entry.workerStart,\n [ATTR_RESOURCE_NEXT_HOP_PROTOCOL]: entry.nextHopProtocol,\n // @ts-expect-error renderBlockingStatus is only available in Chromium as of March 2026\n [ATTR_RESOURCE_RENDER_BLOCKING_STATUS]: entry.renderBlockingStatus,\n },\n };\n const registry = getNetworkContextRegistry();\n const ctx = registry.getContext(entry);\n // Attach the context to the LogRecord and remove it from the registry\n if (ctx) {\n record.context = ctx;\n registry.unregister(entry);\n }\n this.logger.emit(record);\n } catch (error) {\n this._diag.error(\n `Failed to emit resource timing entry for \"${entry.name}\"`,\n error,\n );\n }\n }\n\n private _flush(): void {\n this._cancelScheduledProcessing();\n\n for (const entry of this._pendingEntries) {\n this._emitResource(entry);\n }\n this._pendingEntries = [];\n }\n\n private _cancelScheduledProcessing(): void {\n if (this._idleHandle !== undefined) {\n cancelIdleCallbackShim(this._idleHandle);\n this._idleHandle = undefined;\n }\n }\n}\n"],"mappings":";;;;;;;;AAyCA,MAAM,qBAAqB;AAC3B,MAAM,iCAAiC;AACvC,MAAM,8BAA8B;AACpC,MAAM,yBAAyB;AAE/B,MAAM,iBAAiB;AACvB,MAAM,6BAA6B;AACnC,MAAM,sBAAsB;AAC5B,MAAM,iBAAiB;;;;;;;;;AAUvB,IAAa,gCAAb,cAAmD,oBAAyD;CAC1G;CACA,kBAAuD,CAAC;CACxD;CASA,YAAY,SAA8C,CAAC,GAAG;EAC5D,MACE,0DACA,SACA,MACF;CACF;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,SAAwB;EACtB,IAAI,KAAK,YACP;EAGF,IAAI,EAAE,yBAAyB,SAAS;GACtC,KAAK,MAAM,MACT,8EACF;GACA;EACF;EAEA,KAAK,aAAa;EAElB,IAAI,SAAS,eAAe,YAC1B,KAAK,eAAe;OACf;GACL,KAAK,qBAAqB,KAAK,eAAe;GAC9C,OAAO,iBAAiB,QAAQ,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;EACnE;EAEA,KAAK,iCAAiC;GACpC,IAAI,SAAS,QACX,KAAK,OAAO;EAEhB;EACA,SAAS,iBACP,oBACA,KAAK,wBACP;CACF;CAEA,UAAyB;EACvB,KAAK,aAAa;EAClB,KAAK,OAAO;EACZ,KAAK,WAAW,WAAW;EAC3B,KAAK,YAAY,KAAA;EACjB,IAAI,KAAK,cAAc;GACrB,OAAO,oBAAoB,QAAQ,KAAK,YAAY;GACpD,KAAK,eAAe,KAAA;EACtB;EACA,IAAI,KAAK,0BAA0B;GACjC,SAAS,oBACP,oBACA,KAAK,wBACP;GACA,KAAK,2BAA2B,KAAA;EAClC;CACF;CAEA,iBAA+B;EAC7B,IAAI,CAAC,KAAK,YACR;EAGF,MAAM,WAAW,IAAI,qBAAqB,SAAS;GACjD,IAAI,CAAC,KAAK,YACR;GAGF,MAAM,eAAe,KAAK,IACxB,KAAK,QAAQ,gBAAgB,wBAC7B,cACF;GACA,MAAM,UAAU,KAAK,WAAW;GAChC,MAAM,iBAAiB,KAAK,QAAQ;GAEpC,KAAK,MAAM,SAAS,SAAS;IAC3B,IACE,mBAAmB,KAAA,KACnB,CAAC,eAAe,SAAS,MAAM,aAAa,GAE5C;IAIF,IADwB,WAAW,MAAM,MAAM,KAAK,QAAQ,UAC1C,GAChB;IAGF,IAAI,KAAK,gBAAgB,UAAU,cACjC,KAAK,OAAO;IAEd,KAAK,gBAAgB,KAAK,KAAK;GACjC;GAEA,IAAI,KAAK,gBAAgB,SAAS,GAChC,KAAK,oBAAoB;EAE7B,CAAC;EAED,KAAK,YAAY;EAEjB,IAAI;GACF,SAAS,QAAQ;IAAE,MAAM;IAAY,UAAU;GAAK,CAAC;EACvD,SAAS,GAAG;GACV,KAAK,MAAM,MAAM,gDAAgD,CAAC;GAClE,KAAK,YAAY,KAAA;EACnB;CACF;CAEA,sBAAoC;EAClC,IAAI,KAAK,gBAAgB,KAAA,GACvB;EAGF,MAAM,UAAU,KAAK,IACnB,KAAK,QAAQ,wBAAwB,gCACrC,0BACF;EACA,KAAK,cAAc,yBAChB,aAAa,KAAK,cAAc,QAAQ,GACzC,EAAE,QAAQ,CACZ;CACF;CAEA,cAAsB,UAA8B;EAClD,KAAK,cAAc,KAAA;EACnB,IAAI,CAAC,KAAK,cAAc,KAAK,gBAAgB,WAAW,GACtD;EAGF,MAAM,UAAU,KAAK,IACnB,KAAK,QAAQ,qBAAqB,6BAClC,mBACF;EACA,MAAM,YAAY,KAAK,IACrB,KAAK,QAAQ,aAAa,oBAC1B,cACF;EACA,MAAM,YAAY,YAAY,IAAI;EAElC,IAAI,SAAS;EACb,IAAI;GACF,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,KAAK;IAClC,MAAM,QAAQ,KAAK,gBAAgB;IACnC,IAAI,UAAU,KAAA,GACZ;IAOF,IAJgB,YAAY,IAAI,IAAI,aAIrB,WAAW,SAAS,cAAc,IAAI,GACnD;IAGF;IACA,KAAK,cAAc,KAAK;GAC1B;EACF,UAAU;GACR,KAAK,gBAAgB,OAAO,GAAG,MAAM;GACrC,IAAI,KAAK,gBAAgB,SAAS,GAChC,KAAK,oBAAoB;EAE7B;CACF;CAEA,cAAsB,OAAwC;EAC5D,IAAI;GACF,MAAM,SAAoB;IACxB,WAAW;IACX,gBAAgB,eAAe;IAC/B,YAAY;MACT,oBAAoB,MAAM;MAC1B,+BAA+B,MAAM;MACrC,yBAAyB,MAAM;MAC/B,4BAA4B,MAAM;MAClC,oCAAoC,MAAM;MAC1C,kCAAkC,MAAM;MACxC,8BAA8B,MAAM;MACpC,4BAA4B,MAAM;MAClC,wCAAwC,MAAM;MAC9C,8BAA8B,MAAM;MACpC,+BAA+B,MAAM;MACrC,6BAA6B,MAAM;MACnC,8BAA8B,MAAM;MACpC,kCAAkC,MAAM;MACxC,kCAAkC,MAAM;MACxC,+BAA+B,MAAM;MACrC,6BAA6B,MAAM;MACnC,6BAA6B,MAAM;MACnC,kCAAkC,MAAM;MAExC,uCAAuC,MAAM;IAChD;GACF;GACA,MAAM,WAAW,0BAA0B;GAC3C,MAAM,MAAM,SAAS,WAAW,KAAK;GAErC,IAAI,KAAK;IACP,OAAO,UAAU;IACjB,SAAS,WAAW,KAAK;GAC3B;GACA,KAAK,OAAO,KAAK,MAAM;EACzB,SAAS,OAAO;GACd,KAAK,MAAM,MACT,6CAA6C,MAAM,KAAK,IACxD,KACF;EACF;CACF;CAEA,SAAuB;EACrB,KAAK,2BAA2B;EAEhC,KAAK,MAAM,SAAS,KAAK,iBACvB,KAAK,cAAc,KAAK;EAE1B,KAAK,kBAAkB,CAAC;CAC1B;CAEA,6BAA2C;EACzC,IAAI,KAAK,gBAAgB,KAAA,GAAW;GAClC,uBAAuB,KAAK,WAAW;GACvC,KAAK,cAAc,KAAA;EACrB;CACF;AACF"}
@@ -3,7 +3,7 @@ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
3
  /**
4
4
  * ResourceTimingInstrumentation Configuration
5
5
  */
6
- interface ResourceTimingInstrumentationConfig extends InstrumentationConfig {
6
+ export interface ResourceTimingInstrumentationConfig extends InstrumentationConfig {
7
7
  /**
8
8
  * Number of resources to process per batch.
9
9
  * Default: 50
@@ -43,5 +43,4 @@ interface ResourceTimingInstrumentationConfig extends InstrumentationConfig {
43
43
  ignoreUrls?: (string | RegExp)[];
44
44
  }
45
45
  //#endregion
46
- export { ResourceTimingInstrumentationConfig };
47
46
  //# sourceMappingURL=types.d.ts.map
@@ -4,7 +4,7 @@ import { InstrumentationBase } from "@opentelemetry/instrumentation";
4
4
  /**
5
5
  * This class automatically instruments different User Actions within the browser.
6
6
  */
7
- declare class UserActionInstrumentation extends InstrumentationBase<UserActionInstrumentationConfig> {
7
+ export declare class UserActionInstrumentation extends InstrumentationBase<UserActionInstrumentationConfig> {
8
8
  private _onClickHandler?;
9
9
  constructor(config?: UserActionInstrumentationConfig);
10
10
  protected init(): never[];
@@ -15,5 +15,4 @@ declare class UserActionInstrumentation extends InstrumentationBase<UserActionIn
15
15
  disable(): void;
16
16
  }
17
17
  //#endregion
18
- export { UserActionInstrumentation };
19
18
  //# sourceMappingURL=instrumentation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/user-action/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport { getElementCSSSelector } from '#utils';\nimport { version } from '../../package.json' with { type: 'json' };\nimport {\n ATTR_CSS_SELECTOR,\n ATTR_MOUSE_EVENT_BUTTON,\n ATTR_PAGE_X,\n ATTR_PAGE_Y,\n ATTR_TAG_NAME,\n ATTR_TAGS,\n CLICK_EVENT_NAME,\n} from './semconv.ts';\nimport type {\n AutoCapturedUserAction,\n MouseButton,\n UserActionInstrumentationConfig,\n} from './types.ts';\n\nconst DEFAULT_AUTO_CAPTURED_ACTIONS: AutoCapturedUserAction[] = ['click'];\nconst OTEL_ELEMENT_ATTRIBUTE_PREFIX = 'data-otel-';\n\n/**\n * This class automatically instruments different User Actions within the browser.\n */\nexport class UserActionInstrumentation extends InstrumentationBase<UserActionInstrumentationConfig> {\n private declare _onClickHandler?: (event: MouseEvent) => void;\n\n constructor(config: UserActionInstrumentationConfig = {}) {\n super(\n '@opentelemetry/browser-instrumentation/user-action',\n version,\n config,\n );\n }\n\n protected override init() {\n return [];\n }\n\n private _getMouseButtonFromMouseEvent(event: MouseEvent): MouseButton {\n switch (event.button) {\n case 0:\n return 'left';\n case 1:\n return 'middle';\n case 2:\n return 'right';\n default:\n return 'left';\n }\n }\n\n private onClick(event: MouseEvent) {\n const element = event.target;\n\n if (!(element instanceof HTMLElement)) {\n return;\n }\n\n if (element.hasAttribute('disabled')) {\n return;\n }\n\n const cssSelector = getElementCSSSelector(element, {\n useIdForTargetElement: true,\n useIdForAncestors: true,\n });\n const otelPrefixedAttributes: Record<string, string> = {};\n\n // Grab all the attributes in the element that start with data-otel-*\n for (const attr of element.attributes) {\n if (attr.name.startsWith(OTEL_ELEMENT_ATTRIBUTE_PREFIX)) {\n otelPrefixedAttributes[\n attr.name.slice(OTEL_ELEMENT_ATTRIBUTE_PREFIX.length)\n ] = attr.value;\n }\n }\n\n const logRecord: LogRecord = {\n severityNumber: SeverityNumber.INFO,\n eventName: CLICK_EVENT_NAME,\n attributes: {\n [ATTR_PAGE_X]: event.pageX,\n [ATTR_PAGE_Y]: event.pageY,\n [ATTR_TAG_NAME]: element.tagName,\n [ATTR_TAGS]: otelPrefixedAttributes,\n [ATTR_MOUSE_EVENT_BUTTON]: this._getMouseButtonFromMouseEvent(event),\n [ATTR_CSS_SELECTOR]: cssSelector,\n },\n };\n\n this._applyCustomLogRecordData(logRecord);\n\n this.logger.emit(logRecord);\n }\n\n private _applyCustomLogRecordData(logRecord: LogRecord) {\n const applyCustomLogRecordData = this.getConfig().applyCustomLogRecordData;\n if (applyCustomLogRecordData) {\n safeExecuteInTheMiddle(\n () => applyCustomLogRecordData(logRecord),\n (error) => {\n if (error) {\n this._diag.error('applyCustomLogRecordData hook failed', error);\n }\n },\n true,\n );\n }\n }\n\n override enable(): void {\n const autoCapturedActions =\n this._config.autoCapturedActions ?? DEFAULT_AUTO_CAPTURED_ACTIONS;\n if (!this._onClickHandler) {\n this._onClickHandler = this.onClick.bind(this);\n }\n\n if (autoCapturedActions.includes('click')) {\n document.addEventListener('click', this._onClickHandler, true);\n }\n }\n\n override disable(): void {\n if (this._onClickHandler) {\n document.removeEventListener('click', this._onClickHandler, true);\n }\n }\n}\n"],"mappings":";;;;;;AA2BA,MAAM,gCAA0D,CAAC,OAAO;AACxE,MAAM,gCAAgC;;;;AAKtC,IAAa,4BAAb,cAA+C,oBAAqD;CAGlG,YAAY,SAA0C,CAAC,GAAG;EACxD,MACE,sDACA,SACA,MACF;CACF;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,8BAAsC,OAAgC;EACpE,QAAQ,MAAM,QAAd;GACE,KAAK,GACH,OAAO;GACT,KAAK,GACH,OAAO;GACT,KAAK,GACH,OAAO;GACT,SACE,OAAO;EACX;CACF;CAEA,QAAgB,OAAmB;EACjC,MAAM,UAAU,MAAM;EAEtB,IAAI,EAAE,mBAAmB,cACvB;EAGF,IAAI,QAAQ,aAAa,UAAU,GACjC;EAGF,MAAM,cAAc,sBAAsB,SAAS;GACjD,uBAAuB;GACvB,mBAAmB;EACrB,CAAC;EACD,MAAM,yBAAiD,CAAC;EAGxD,KAAK,MAAM,QAAQ,QAAQ,YACzB,IAAI,KAAK,KAAK,WAAW,6BAA6B,GACpD,uBACE,KAAK,KAAK,MAAM,EAAoC,KAClD,KAAK;EAIb,MAAM,YAAuB;GAC3B,gBAAgB,eAAe;GAC/B,WAAW;GACX,YAAY;KACT,cAAc,MAAM;KACpB,cAAc,MAAM;KACpB,gBAAgB,QAAQ;KACxB,YAAY;KACZ,0BAA0B,KAAK,8BAA8B,KAAK;KAClE,oBAAoB;GACvB;EACF;EAEA,KAAK,0BAA0B,SAAS;EAExC,KAAK,OAAO,KAAK,SAAS;CAC5B;CAEA,0BAAkC,WAAsB;EACtD,MAAM,2BAA2B,KAAK,UAAU,CAAC,CAAC;EAClD,IAAI,0BACF,6BACQ,yBAAyB,SAAS,IACvC,UAAU;GACT,IAAI,OACF,KAAK,MAAM,MAAM,wCAAwC,KAAK;EAElE,GACA,IACF;CAEJ;CAEA,SAAwB;EACtB,MAAM,sBACJ,KAAK,QAAQ,uBAAuB;EACtC,IAAI,CAAC,KAAK,iBACR,KAAK,kBAAkB,KAAK,QAAQ,KAAK,IAAI;EAG/C,IAAI,oBAAoB,SAAS,OAAO,GACtC,SAAS,iBAAiB,SAAS,KAAK,iBAAiB,IAAI;CAEjE;CAEA,UAAyB;EACvB,IAAI,KAAK,iBACP,SAAS,oBAAoB,SAAS,KAAK,iBAAiB,IAAI;CAEpE;AACF"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/user-action/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport { getElementCSSSelector } from '#utils';\nimport { version } from '../../package.json' with { type: 'json' };\nimport {\n ATTR_CSS_SELECTOR,\n ATTR_MOUSE_EVENT_BUTTON,\n ATTR_PAGE_X,\n ATTR_PAGE_Y,\n ATTR_TAG_NAME,\n ATTR_TAGS,\n CLICK_EVENT_NAME,\n} from './semconv.ts';\nimport type {\n AutoCapturedUserAction,\n MouseButton,\n UserActionInstrumentationConfig,\n} from './types.ts';\n\nconst DEFAULT_AUTO_CAPTURED_ACTIONS: AutoCapturedUserAction[] = ['click'];\nconst OTEL_ELEMENT_ATTRIBUTE_PREFIX = 'data-otel-';\n\n/**\n * This class automatically instruments different User Actions within the browser.\n */\nexport class UserActionInstrumentation extends InstrumentationBase<UserActionInstrumentationConfig> {\n declare private _onClickHandler?: (event: MouseEvent) => void;\n\n constructor(config: UserActionInstrumentationConfig = {}) {\n super(\n '@opentelemetry/browser-instrumentation/user-action',\n version,\n config,\n );\n }\n\n protected override init() {\n return [];\n }\n\n private _getMouseButtonFromMouseEvent(event: MouseEvent): MouseButton {\n switch (event.button) {\n case 0:\n return 'left';\n case 1:\n return 'middle';\n case 2:\n return 'right';\n default:\n return 'left';\n }\n }\n\n private onClick(event: MouseEvent) {\n const element = event.target;\n\n if (!(element instanceof HTMLElement)) {\n return;\n }\n\n if (element.hasAttribute('disabled')) {\n return;\n }\n\n const cssSelector = getElementCSSSelector(element, {\n useIdForTargetElement: true,\n useIdForAncestors: true,\n });\n const otelPrefixedAttributes: Record<string, string> = {};\n\n // Grab all the attributes in the element that start with data-otel-*\n for (const attr of element.attributes) {\n if (attr.name.startsWith(OTEL_ELEMENT_ATTRIBUTE_PREFIX)) {\n otelPrefixedAttributes[\n attr.name.slice(OTEL_ELEMENT_ATTRIBUTE_PREFIX.length)\n ] = attr.value;\n }\n }\n\n const logRecord: LogRecord = {\n severityNumber: SeverityNumber.INFO,\n eventName: CLICK_EVENT_NAME,\n attributes: {\n [ATTR_PAGE_X]: event.pageX,\n [ATTR_PAGE_Y]: event.pageY,\n [ATTR_TAG_NAME]: element.tagName,\n [ATTR_TAGS]: otelPrefixedAttributes,\n [ATTR_MOUSE_EVENT_BUTTON]: this._getMouseButtonFromMouseEvent(event),\n [ATTR_CSS_SELECTOR]: cssSelector,\n },\n };\n\n this._applyCustomLogRecordData(logRecord);\n\n this.logger.emit(logRecord);\n }\n\n private _applyCustomLogRecordData(logRecord: LogRecord) {\n const applyCustomLogRecordData = this.getConfig().applyCustomLogRecordData;\n if (applyCustomLogRecordData) {\n safeExecuteInTheMiddle(\n () => applyCustomLogRecordData(logRecord),\n (error) => {\n if (error) {\n this._diag.error('applyCustomLogRecordData hook failed', error);\n }\n },\n true,\n );\n }\n }\n\n override enable(): void {\n const autoCapturedActions =\n this._config.autoCapturedActions ?? DEFAULT_AUTO_CAPTURED_ACTIONS;\n if (!this._onClickHandler) {\n this._onClickHandler = this.onClick.bind(this);\n }\n\n if (autoCapturedActions.includes('click')) {\n document.addEventListener('click', this._onClickHandler, true);\n }\n }\n\n override disable(): void {\n if (this._onClickHandler) {\n document.removeEventListener('click', this._onClickHandler, true);\n }\n }\n}\n"],"mappings":";;;;;;AA2BA,MAAM,gCAA0D,CAAC,OAAO;AACxE,MAAM,gCAAgC;;;;AAKtC,IAAa,4BAAb,cAA+C,oBAAqD;CAGlG,YAAY,SAA0C,CAAC,GAAG;EACxD,MACE,sDACA,SACA,MACF;CACF;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,8BAAsC,OAAgC;EACpE,QAAQ,MAAM,QAAd;GACE,KAAK,GACH,OAAO;GACT,KAAK,GACH,OAAO;GACT,KAAK,GACH,OAAO;GACT,SACE,OAAO;EACX;CACF;CAEA,QAAgB,OAAmB;EACjC,MAAM,UAAU,MAAM;EAEtB,IAAI,EAAE,mBAAmB,cACvB;EAGF,IAAI,QAAQ,aAAa,UAAU,GACjC;EAGF,MAAM,cAAc,sBAAsB,SAAS;GACjD,uBAAuB;GACvB,mBAAmB;EACrB,CAAC;EACD,MAAM,yBAAiD,CAAC;EAGxD,KAAK,MAAM,QAAQ,QAAQ,YACzB,IAAI,KAAK,KAAK,WAAW,6BAA6B,GACpD,uBACE,KAAK,KAAK,MAAM,EAAoC,KAClD,KAAK;EAIb,MAAM,YAAuB;GAC3B,gBAAgB,eAAe;GAC/B,WAAW;GACX,YAAY;KACT,cAAc,MAAM;KACpB,cAAc,MAAM;KACpB,gBAAgB,QAAQ;KACxB,YAAY;KACZ,0BAA0B,KAAK,8BAA8B,KAAK;KAClE,oBAAoB;GACvB;EACF;EAEA,KAAK,0BAA0B,SAAS;EAExC,KAAK,OAAO,KAAK,SAAS;CAC5B;CAEA,0BAAkC,WAAsB;EACtD,MAAM,2BAA2B,KAAK,UAAU,CAAC,CAAC;EAClD,IAAI,0BACF,6BACQ,yBAAyB,SAAS,IACvC,UAAU;GACT,IAAI,OACF,KAAK,MAAM,MAAM,wCAAwC,KAAK;EAElE,GACA,IACF;CAEJ;CAEA,SAAwB;EACtB,MAAM,sBACJ,KAAK,QAAQ,uBAAuB;EACtC,IAAI,CAAC,KAAK,iBACR,KAAK,kBAAkB,KAAK,QAAQ,KAAK,IAAI;EAG/C,IAAI,oBAAoB,SAAS,OAAO,GACtC,SAAS,iBAAiB,SAAS,KAAK,iBAAiB,IAAI;CAEjE;CAEA,UAAyB;EACvB,IAAI,KAAK,iBACP,SAAS,oBAAoB,SAAS,KAAK,iBAAiB,IAAI;CAEpE;AACF"}
@@ -1,16 +1,15 @@
1
1
  import { LogRecord } from "@opentelemetry/api-logs";
2
2
  import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
3
  //#region src/user-action/types.d.ts
4
- type AutoCapturedUserAction = 'click';
5
- type ApplyCustomLogRecordDataFunction = (logRecord: LogRecord) => void;
4
+ export type AutoCapturedUserAction = 'click';
5
+ export type ApplyCustomLogRecordDataFunction = (logRecord: LogRecord) => void;
6
6
  /**
7
7
  * UserActionInstrumentation Configuration
8
8
  */
9
- interface UserActionInstrumentationConfig extends InstrumentationConfig {
9
+ export interface UserActionInstrumentationConfig extends InstrumentationConfig {
10
10
  autoCapturedActions?: AutoCapturedUserAction[];
11
11
  /** Hook to modify log records before they are emitted. */
12
12
  applyCustomLogRecordData?: ApplyCustomLogRecordDataFunction;
13
13
  }
14
14
  //#endregion
15
- export { ApplyCustomLogRecordDataFunction, AutoCapturedUserAction, UserActionInstrumentationConfig };
16
15
  //# sourceMappingURL=types.d.ts.map
@@ -31,9 +31,9 @@ const getNodeSelector = (element, useElementId = false) => {
31
31
  };
32
32
  const getNthChild = (element) => {
33
33
  if (!element.parentElement) return 0;
34
- const selector = getFullClassSelector(element);
35
- const siblings = Array.from(element.parentElement.children).filter((sibling) => getFullClassSelector(sibling) === selector);
36
- if (siblings.length > 1) return siblings.indexOf(element) + 1;
34
+ const classes = Array.from(element.classList);
35
+ const children = Array.from(element.parentElement.children);
36
+ if (children.some((sibling) => sibling !== element && sibling.localName === element.localName && classes.every((cls) => sibling.classList.contains(cls)))) return children.indexOf(element) + 1;
37
37
  return 0;
38
38
  };
39
39
  const getFullClassSelector = (element) => element.localName + (element.classList.length > 0 ? Array.from(element.classList).map((cls) => `.${CSS.escape(cls)}`).join("") : "");
@@ -1 +1 @@
1
- {"version":3,"file":"getElementCSSSelector.js","names":[],"sources":["../../src/utils/getElementCSSSelector.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\ntype GetElementCSSSelectorOptions = {\n /**\n * If true, the function will attempt to use element ID to create condensed CSS selector.\n */\n useIdForTargetElement?: boolean;\n /**\n * If true, the function will attempt to use element ID for all ancestor elements to create condensed CSS selector.\n */\n useIdForAncestors?: boolean;\n};\n\n/**\n * Generates the CSS selector of a given element in the DOM tree.\n *\n * @example #main > div:nth-child(2) > button.submit\n * @example #unique-id\n */\nexport const getElementCSSSelector = (\n element: Node,\n {\n useIdForTargetElement = false,\n useIdForAncestors = false,\n }: GetElementCSSSelectorOptions = {},\n): string => {\n // Handle document node\n if (element.nodeType === Node.DOCUMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const nodeValue = getNodeSelector(\n htmlElement,\n useIdForTargetElement || useIdForAncestors,\n );\n\n // If optimized and found an ID selector, stop recursion early\n if (nodeValue.startsWith('#')) {\n return nodeValue;\n }\n\n const parent = htmlElement.parentElement;\n const parentSelector = parent\n ? getElementCSSSelector(parent, {\n useIdForAncestors,\n useIdForTargetElement: false,\n })\n : '';\n\n return parentSelector ? `${parentSelector} > ${nodeValue}` : nodeValue;\n};\n\nconst getNodeSelector = (element: Node, useElementId = false): string => {\n if (element.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const id = htmlElement.getAttribute('id');\n\n // Use ID if requested and it's unique\n if (useElementId && id) {\n // Check if ID is unique in the document\n const elementsWithSameId = htmlElement.ownerDocument.querySelectorAll(\n `#${CSS.escape(id)}`,\n );\n\n if (elementsWithSameId.length === 1) {\n return `#${CSS.escape(id)}`;\n }\n }\n\n let selector = getFullClassSelector(htmlElement);\n // Add nth-child if there are siblings with the same tag and classes\n const index = getNthChild(htmlElement);\n\n if (index > 0) {\n selector += `:nth-child(${index})`;\n }\n\n return selector;\n};\n\nconst getNthChild = (element: HTMLElement): number => {\n // parentElement is needed to access children\n if (!element.parentElement) {\n return 0;\n }\n\n const selector = getFullClassSelector(element);\n\n // Get all siblings that match the same selector\n const siblings = Array.from(element.parentElement.children).filter(\n (sibling) => getFullClassSelector(sibling) === selector,\n );\n\n // Only add nth-child if there are multiple matching siblings\n if (siblings.length > 1) {\n return siblings.indexOf(element) + 1;\n }\n\n return 0;\n};\n\nconst getFullClassSelector = (element: Element): string =>\n element.localName +\n (element.classList.length > 0\n ? Array.from(element.classList)\n .map((cls) => `.${CSS.escape(cls)}`)\n .join('')\n : '');\n"],"mappings":";;;;;;;AAsBA,MAAa,yBACX,SACA,EACE,wBAAwB,OACxB,oBAAoB,UACY,CAAC,MACxB;CAEX,IAAI,QAAQ,aAAa,KAAK,eAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,YAAY,gBAChB,aACA,yBAAyB,iBAC3B;CAGA,IAAI,UAAU,WAAW,GAAG,GAC1B,OAAO;CAGT,MAAM,SAAS,YAAY;CAC3B,MAAM,iBAAiB,SACnB,sBAAsB,QAAQ;EAC5B;EACA,uBAAuB;CACzB,CAAC,IACD;CAEJ,OAAO,iBAAiB,GAAG,eAAe,KAAK,cAAc;AAC/D;AAEA,MAAM,mBAAmB,SAAe,eAAe,UAAkB;CACvE,IAAI,QAAQ,aAAa,KAAK,cAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,KAAK,YAAY,aAAa,IAAI;CAGxC,IAAI,gBAAgB,IAES;MAAA,YAAY,cAAc,iBACnD,IAAI,IAAI,OAAO,EAAE,GAGE,CAAC,CAAC,WAAW,GAChC,OAAO,IAAI,IAAI,OAAO,EAAE;CAAA;CAI5B,IAAI,WAAW,qBAAqB,WAAW;CAE/C,MAAM,QAAQ,YAAY,WAAW;CAErC,IAAI,QAAQ,GACV,YAAY,cAAc,MAAM;CAGlC,OAAO;AACT;AAEA,MAAM,eAAe,YAAiC;CAEpD,IAAI,CAAC,QAAQ,eACX,OAAO;CAGT,MAAM,WAAW,qBAAqB,OAAO;CAG7C,MAAM,WAAW,MAAM,KAAK,QAAQ,cAAc,QAAQ,CAAC,CAAC,QACzD,YAAY,qBAAqB,OAAO,MAAM,QACjD;CAGA,IAAI,SAAS,SAAS,GACpB,OAAO,SAAS,QAAQ,OAAO,IAAI;CAGrC,OAAO;AACT;AAEA,MAAM,wBAAwB,YAC5B,QAAQ,aACP,QAAQ,UAAU,SAAS,IACxB,MAAM,KAAK,QAAQ,SAAS,CAAC,CAC1B,KAAK,QAAQ,IAAI,IAAI,OAAO,GAAG,GAAG,CAAC,CACnC,KAAK,EAAE,IACV"}
1
+ {"version":3,"file":"getElementCSSSelector.js","names":[],"sources":["../../src/utils/getElementCSSSelector.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\ntype GetElementCSSSelectorOptions = {\n /**\n * If true, the function will attempt to use element ID to create condensed CSS selector.\n */\n useIdForTargetElement?: boolean;\n /**\n * If true, the function will attempt to use element ID for all ancestor elements to create condensed CSS selector.\n */\n useIdForAncestors?: boolean;\n};\n\n/**\n * Generates the CSS selector of a given element in the DOM tree.\n *\n * @example #main > div:nth-child(2) > button.submit\n * @example #unique-id\n */\nexport const getElementCSSSelector = (\n element: Node,\n {\n useIdForTargetElement = false,\n useIdForAncestors = false,\n }: GetElementCSSSelectorOptions = {},\n): string => {\n // Handle document node\n if (element.nodeType === Node.DOCUMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const nodeValue = getNodeSelector(\n htmlElement,\n useIdForTargetElement || useIdForAncestors,\n );\n\n // If optimized and found an ID selector, stop recursion early\n if (nodeValue.startsWith('#')) {\n return nodeValue;\n }\n\n const parent = htmlElement.parentElement;\n const parentSelector = parent\n ? getElementCSSSelector(parent, {\n useIdForAncestors,\n useIdForTargetElement: false,\n })\n : '';\n\n return parentSelector ? `${parentSelector} > ${nodeValue}` : nodeValue;\n};\n\nconst getNodeSelector = (element: Node, useElementId = false): string => {\n if (element.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const id = htmlElement.getAttribute('id');\n\n // Use ID if requested and it's unique\n if (useElementId && id) {\n // Check if ID is unique in the document\n const elementsWithSameId = htmlElement.ownerDocument.querySelectorAll(\n `#${CSS.escape(id)}`,\n );\n\n if (elementsWithSameId.length === 1) {\n return `#${CSS.escape(id)}`;\n }\n }\n\n let selector = getFullClassSelector(htmlElement);\n // Add nth-child if there are siblings with the same tag and classes\n const index = getNthChild(htmlElement);\n\n if (index > 0) {\n selector += `:nth-child(${index})`;\n }\n\n return selector;\n};\n\nconst getNthChild = (element: HTMLElement): number => {\n // parentElement is needed to access children\n if (!element.parentElement) {\n return 0;\n }\n\n const classes = Array.from(element.classList);\n const children = Array.from(element.parentElement.children);\n\n // The selector also matches any sibling with this tag and these classes\n const hasMatchingSibling = children.some(\n (sibling) =>\n sibling !== element &&\n sibling.localName === element.localName &&\n classes.every((cls) => sibling.classList.contains(cls)),\n );\n\n // nth-child counts every element sibling, not only the matching ones\n if (hasMatchingSibling) {\n return children.indexOf(element) + 1;\n }\n\n return 0;\n};\n\nconst getFullClassSelector = (element: Element): string =>\n element.localName +\n (element.classList.length > 0\n ? Array.from(element.classList)\n .map((cls) => `.${CSS.escape(cls)}`)\n .join('')\n : '');\n"],"mappings":";;;;;;;AAsBA,MAAa,yBACX,SACA,EACE,wBAAwB,OACxB,oBAAoB,UACY,CAAC,MACxB;CAEX,IAAI,QAAQ,aAAa,KAAK,eAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,YAAY,gBAChB,aACA,yBAAyB,iBAC3B;CAGA,IAAI,UAAU,WAAW,GAAG,GAC1B,OAAO;CAGT,MAAM,SAAS,YAAY;CAC3B,MAAM,iBAAiB,SACnB,sBAAsB,QAAQ;EAC5B;EACA,uBAAuB;CACzB,CAAC,IACD;CAEJ,OAAO,iBAAiB,GAAG,eAAe,KAAK,cAAc;AAC/D;AAEA,MAAM,mBAAmB,SAAe,eAAe,UAAkB;CACvE,IAAI,QAAQ,aAAa,KAAK,cAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,KAAK,YAAY,aAAa,IAAI;CAGxC,IAAI,gBAAgB,IAES;MAAA,YAAY,cAAc,iBACnD,IAAI,IAAI,OAAO,EAAE,GAGE,CAAC,CAAC,WAAW,GAChC,OAAO,IAAI,IAAI,OAAO,EAAE;CAAA;CAI5B,IAAI,WAAW,qBAAqB,WAAW;CAE/C,MAAM,QAAQ,YAAY,WAAW;CAErC,IAAI,QAAQ,GACV,YAAY,cAAc,MAAM;CAGlC,OAAO;AACT;AAEA,MAAM,eAAe,YAAiC;CAEpD,IAAI,CAAC,QAAQ,eACX,OAAO;CAGT,MAAM,UAAU,MAAM,KAAK,QAAQ,SAAS;CAC5C,MAAM,WAAW,MAAM,KAAK,QAAQ,cAAc,QAAQ;CAW1D,IAR2B,SAAS,MACjC,YACC,YAAY,WACZ,QAAQ,cAAc,QAAQ,aAC9B,QAAQ,OAAO,QAAQ,QAAQ,UAAU,SAAS,GAAG,CAAC,CAIrC,GACnB,OAAO,SAAS,QAAQ,OAAO,IAAI;CAGrC,OAAO;AACT;AAEA,MAAM,wBAAwB,YAC5B,QAAQ,aACP,QAAQ,UAAU,SAAS,IACxB,MAAM,KAAK,QAAQ,SAAS,CAAC,CAC1B,KAAK,QAAQ,IAAI,IAAI,OAAO,GAAG,GAAG,CAAC,CACnC,KAAK,EAAE,IACV"}
@@ -9,7 +9,7 @@ import { InstrumentationBase } from "@opentelemetry/instrumentation";
9
9
  * registered. Calling `disable()` will stop emitting logs, but the underlying
10
10
  * listeners remain active. Calling `enable()` again will resume emission.
11
11
  */
12
- declare class WebVitalsInstrumentation extends InstrumentationBase<WebVitalsInstrumentationConfig> {
12
+ export declare class WebVitalsInstrumentation extends InstrumentationBase<WebVitalsInstrumentationConfig> {
13
13
  private _isEnabled;
14
14
  private _listenersRegistered;
15
15
  private _applyCustomLogRecordData?;
@@ -35,5 +35,4 @@ declare class WebVitalsInstrumentation extends InstrumentationBase<WebVitalsInst
35
35
  private _emitWebVital;
36
36
  }
37
37
  //#endregion
38
- export { WebVitalsInstrumentation };
39
38
  //# sourceMappingURL=instrumentation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/web-vitals/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes } from '@opentelemetry/api';\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport type {\n CLSMetricWithAttribution,\n INPMetricWithAttribution,\n MetricWithAttribution,\n} from 'web-vitals/attribution';\nimport { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals/attribution';\nimport { version } from '../../package.json' with { type: 'json' };\nimport {\n ATTR_WEB_VITAL_DELTA,\n ATTR_WEB_VITAL_ID,\n ATTR_WEB_VITAL_NAME,\n ATTR_WEB_VITAL_NAVIGATION_TYPE,\n ATTR_WEB_VITAL_RATING,\n ATTR_WEB_VITAL_VALUE,\n WEB_VITAL_EVENT_NAME,\n} from './semconv.ts';\nimport type { WebVitalsInstrumentationConfig } from './types.ts';\n\n/**\n * Instrumentation for Core Web Vitals using the `web-vitals` library.\n * https://github.com/GoogleChrome/web-vitals\n *\n * Note: The `web-vitals` library does not support removing listeners once\n * registered. Calling `disable()` will stop emitting logs, but the underlying\n * listeners remain active. Calling `enable()` again will resume emission.\n */\nexport class WebVitalsInstrumentation extends InstrumentationBase<WebVitalsInstrumentationConfig> {\n // Using `declare` is required here: InstrumentationBase calls enable() during\n // construction, and standard field initialization would reset this flag after\n // super() returns, breaking the duplicate-registration guard.\n private declare _isEnabled: boolean;\n private declare _listenersRegistered: boolean;\n private _applyCustomLogRecordData?: (logRecord: LogRecord) => void;\n private _includeRawAttribution: boolean;\n\n constructor(config: WebVitalsInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/web-vitals', version, config);\n this._applyCustomLogRecordData = config.applyCustomLogRecordData;\n this._includeRawAttribution = config.includeRawAttribution ?? false;\n }\n\n protected override init() {\n return [];\n }\n\n /**\n * Enables the instrumentation and registers web-vitals listeners.\n * Listeners are registered only once. If disabled, subsequent calls resume emission.\n */\n override enable(): void {\n if (typeof PerformanceObserver === 'undefined') {\n this._diag.debug(\n 'PerformanceObserver not supported, web vitals will not be collected',\n );\n return;\n }\n\n this._isEnabled = true;\n\n if (this._listenersRegistered) {\n this._diag.debug('Listeners already registered, resuming emission');\n return;\n }\n\n this._listenersRegistered = true;\n this._diag.debug(`Registering listeners`);\n // CLS is only supported in Chromium. See:\n // https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#browser-support\n onCLS((metric) => this._emitWebVital(metric));\n onINP((metric) => this._emitWebVital(metric));\n onLCP((metric) => this._emitWebVital(metric));\n onFCP((metric) => this._emitWebVital(metric));\n onTTFB((metric) => this._emitWebVital(metric));\n }\n\n /**\n * Disables the instrumentation, pausing log emission.\n * Listeners remain active due to web-vitals library limitations.\n */\n override disable(): void {\n this._isEnabled = false;\n this._diag.debug('Instrumentation disabled, pausing emission');\n }\n\n /**\n * Gets the timestamp for a metric based on attribution timing.\n * Returns undefined to let OTel use the current time for metrics without\n * specific timing information.\n */\n private _getTimestampForMetric(\n metric: MetricWithAttribution,\n ): number | undefined {\n if (metric.name === 'CLS') {\n const { attribution } = metric as CLSMetricWithAttribution;\n if (attribution.largestShiftTime !== undefined) {\n return attribution.largestShiftTime;\n }\n return undefined;\n }\n if (metric.name === 'INP') {\n const { attribution } = metric as INPMetricWithAttribution;\n return attribution.interactionTime;\n }\n // FCP, LCP, TTFB: metric.value is already DOMHighResTimeStamp of the event\n return metric.value;\n }\n\n private _emitWebVital(metric: MetricWithAttribution): void {\n if (!this._isEnabled) {\n return;\n }\n const attributes: Attributes = {\n [ATTR_WEB_VITAL_NAME]: metric.name.toLowerCase(),\n [ATTR_WEB_VITAL_VALUE]: metric.value,\n // `delta` equals `value` on the first emission; subsequent emissions report only the change\n [ATTR_WEB_VITAL_DELTA]: metric.delta,\n [ATTR_WEB_VITAL_RATING]: metric.rating,\n [ATTR_WEB_VITAL_ID]: metric.id,\n [ATTR_WEB_VITAL_NAVIGATION_TYPE]: metric.navigationType,\n };\n\n const timestamp = this._getTimestampForMetric(metric);\n\n const logRecord: LogRecord = {\n eventName: WEB_VITAL_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes,\n ...(this._includeRawAttribution\n ? { body: JSON.stringify(metric.attribution) }\n : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n };\n\n if (this._applyCustomLogRecordData) {\n safeExecuteInTheMiddle(\n () => this._applyCustomLogRecordData?.(logRecord),\n (error) => {\n if (error) {\n this._diag.error('applyCustomLogRecordData hook failed', error);\n }\n },\n true,\n );\n }\n\n this.logger.emit(logRecord);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAsCA,IAAa,2BAAb,cAA8C,oBAAoD;CAMhG;CACA;CAEA,YAAY,SAAyC,CAAC,GAAG;EACvD,MAAM,qDAAqD,SAAS,MAAM;EAC1E,KAAK,4BAA4B,OAAO;EACxC,KAAK,yBAAyB,OAAO,yBAAyB;CAChE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;;;;;CAMA,SAAwB;EACtB,IAAI,OAAO,wBAAwB,aAAa;GAC9C,KAAK,MAAM,MACT,qEACF;GACA;EACF;EAEA,KAAK,aAAa;EAElB,IAAI,KAAK,sBAAsB;GAC7B,KAAK,MAAM,MAAM,iDAAiD;GAClE;EACF;EAEA,KAAK,uBAAuB;EAC5B,KAAK,MAAM,MAAM,uBAAuB;EAGxC,OAAO,WAAW,KAAK,cAAc,MAAM,CAAC;EAC5C,OAAO,WAAW,KAAK,cAAc,MAAM,CAAC;EAC5C,OAAO,WAAW,KAAK,cAAc,MAAM,CAAC;EAC5C,OAAO,WAAW,KAAK,cAAc,MAAM,CAAC;EAC5C,QAAQ,WAAW,KAAK,cAAc,MAAM,CAAC;CAC/C;;;;;CAMA,UAAyB;EACvB,KAAK,aAAa;EAClB,KAAK,MAAM,MAAM,4CAA4C;CAC/D;;;;;;CAOA,uBACE,QACoB;EACpB,IAAI,OAAO,SAAS,OAAO;GACzB,MAAM,EAAE,gBAAgB;GACxB,IAAI,YAAY,qBAAqB,KAAA,GACnC,OAAO,YAAY;GAErB;EACF;EACA,IAAI,OAAO,SAAS,OAAO;GACzB,MAAM,EAAE,gBAAgB;GACxB,OAAO,YAAY;EACrB;EAEA,OAAO,OAAO;CAChB;CAEA,cAAsB,QAAqC;EACzD,IAAI,CAAC,KAAK,YACR;EAEF,MAAM,aAAyB;IAC5B,sBAAsB,OAAO,KAAK,YAAY;IAC9C,uBAAuB,OAAO;IAE9B,uBAAuB,OAAO;IAC9B,wBAAwB,OAAO;IAC/B,oBAAoB,OAAO;IAC3B,iCAAiC,OAAO;EAC3C;EAEA,MAAM,YAAY,KAAK,uBAAuB,MAAM;EAEpD,MAAM,YAAuB;GAC3B,WAAW;GACX,gBAAgB,eAAe;GAC/B;GACA,GAAI,KAAK,yBACL,EAAE,MAAM,KAAK,UAAU,OAAO,WAAW,EAAE,IAC3C,CAAC;GACL,GAAI,cAAc,KAAA,IAAY,EAAE,UAAU,IAAI,CAAC;EACjD;EAEA,IAAI,KAAK,2BACP,6BACQ,KAAK,4BAA4B,SAAS,IAC/C,UAAU;GACT,IAAI,OACF,KAAK,MAAM,MAAM,wCAAwC,KAAK;EAElE,GACA,IACF;EAGF,KAAK,OAAO,KAAK,SAAS;CAC5B;AACF"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/web-vitals/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes } from '@opentelemetry/api';\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport type {\n CLSMetricWithAttribution,\n INPMetricWithAttribution,\n MetricWithAttribution,\n} from 'web-vitals/attribution';\nimport { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals/attribution';\nimport { version } from '../../package.json' with { type: 'json' };\nimport {\n ATTR_WEB_VITAL_DELTA,\n ATTR_WEB_VITAL_ID,\n ATTR_WEB_VITAL_NAME,\n ATTR_WEB_VITAL_NAVIGATION_TYPE,\n ATTR_WEB_VITAL_RATING,\n ATTR_WEB_VITAL_VALUE,\n WEB_VITAL_EVENT_NAME,\n} from './semconv.ts';\nimport type { WebVitalsInstrumentationConfig } from './types.ts';\n\n/**\n * Instrumentation for Core Web Vitals using the `web-vitals` library.\n * https://github.com/GoogleChrome/web-vitals\n *\n * Note: The `web-vitals` library does not support removing listeners once\n * registered. Calling `disable()` will stop emitting logs, but the underlying\n * listeners remain active. Calling `enable()` again will resume emission.\n */\nexport class WebVitalsInstrumentation extends InstrumentationBase<WebVitalsInstrumentationConfig> {\n // Using `declare` is required here: InstrumentationBase calls enable() during\n // construction, and standard field initialization would reset this flag after\n // super() returns, breaking the duplicate-registration guard.\n declare private _isEnabled: boolean;\n declare private _listenersRegistered: boolean;\n private _applyCustomLogRecordData?: (logRecord: LogRecord) => void;\n private _includeRawAttribution: boolean;\n\n constructor(config: WebVitalsInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/web-vitals', version, config);\n this._applyCustomLogRecordData = config.applyCustomLogRecordData;\n this._includeRawAttribution = config.includeRawAttribution ?? false;\n }\n\n protected override init() {\n return [];\n }\n\n /**\n * Enables the instrumentation and registers web-vitals listeners.\n * Listeners are registered only once. If disabled, subsequent calls resume emission.\n */\n override enable(): void {\n if (typeof PerformanceObserver === 'undefined') {\n this._diag.debug(\n 'PerformanceObserver not supported, web vitals will not be collected',\n );\n return;\n }\n\n this._isEnabled = true;\n\n if (this._listenersRegistered) {\n this._diag.debug('Listeners already registered, resuming emission');\n return;\n }\n\n this._listenersRegistered = true;\n this._diag.debug(`Registering listeners`);\n // CLS is only supported in Chromium. See:\n // https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#browser-support\n onCLS((metric) => this._emitWebVital(metric));\n onINP((metric) => this._emitWebVital(metric));\n onLCP((metric) => this._emitWebVital(metric));\n onFCP((metric) => this._emitWebVital(metric));\n onTTFB((metric) => this._emitWebVital(metric));\n }\n\n /**\n * Disables the instrumentation, pausing log emission.\n * Listeners remain active due to web-vitals library limitations.\n */\n override disable(): void {\n this._isEnabled = false;\n this._diag.debug('Instrumentation disabled, pausing emission');\n }\n\n /**\n * Gets the timestamp for a metric based on attribution timing.\n * Returns undefined to let OTel use the current time for metrics without\n * specific timing information.\n */\n private _getTimestampForMetric(\n metric: MetricWithAttribution,\n ): number | undefined {\n if (metric.name === 'CLS') {\n const { attribution } = metric as CLSMetricWithAttribution;\n if (attribution.largestShiftTime !== undefined) {\n return attribution.largestShiftTime;\n }\n return undefined;\n }\n if (metric.name === 'INP') {\n const { attribution } = metric as INPMetricWithAttribution;\n return attribution.interactionTime;\n }\n // FCP, LCP, TTFB: metric.value is already DOMHighResTimeStamp of the event\n return metric.value;\n }\n\n private _emitWebVital(metric: MetricWithAttribution): void {\n if (!this._isEnabled) {\n return;\n }\n const attributes: Attributes = {\n [ATTR_WEB_VITAL_NAME]: metric.name.toLowerCase(),\n [ATTR_WEB_VITAL_VALUE]: metric.value,\n // `delta` equals `value` on the first emission; subsequent emissions report only the change\n [ATTR_WEB_VITAL_DELTA]: metric.delta,\n [ATTR_WEB_VITAL_RATING]: metric.rating,\n [ATTR_WEB_VITAL_ID]: metric.id,\n [ATTR_WEB_VITAL_NAVIGATION_TYPE]: metric.navigationType,\n };\n\n const timestamp = this._getTimestampForMetric(metric);\n\n const logRecord: LogRecord = {\n eventName: WEB_VITAL_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes,\n ...(this._includeRawAttribution\n ? { body: JSON.stringify(metric.attribution) }\n : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n };\n\n if (this._applyCustomLogRecordData) {\n safeExecuteInTheMiddle(\n () => this._applyCustomLogRecordData?.(logRecord),\n (error) => {\n if (error) {\n this._diag.error('applyCustomLogRecordData hook failed', error);\n }\n },\n true,\n );\n }\n\n this.logger.emit(logRecord);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAsCA,IAAa,2BAAb,cAA8C,oBAAoD;CAMhG;CACA;CAEA,YAAY,SAAyC,CAAC,GAAG;EACvD,MAAM,qDAAqD,SAAS,MAAM;EAC1E,KAAK,4BAA4B,OAAO;EACxC,KAAK,yBAAyB,OAAO,yBAAyB;CAChE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;;;;;CAMA,SAAwB;EACtB,IAAI,OAAO,wBAAwB,aAAa;GAC9C,KAAK,MAAM,MACT,qEACF;GACA;EACF;EAEA,KAAK,aAAa;EAElB,IAAI,KAAK,sBAAsB;GAC7B,KAAK,MAAM,MAAM,iDAAiD;GAClE;EACF;EAEA,KAAK,uBAAuB;EAC5B,KAAK,MAAM,MAAM,uBAAuB;EAGxC,OAAO,WAAW,KAAK,cAAc,MAAM,CAAC;EAC5C,OAAO,WAAW,KAAK,cAAc,MAAM,CAAC;EAC5C,OAAO,WAAW,KAAK,cAAc,MAAM,CAAC;EAC5C,OAAO,WAAW,KAAK,cAAc,MAAM,CAAC;EAC5C,QAAQ,WAAW,KAAK,cAAc,MAAM,CAAC;CAC/C;;;;;CAMA,UAAyB;EACvB,KAAK,aAAa;EAClB,KAAK,MAAM,MAAM,4CAA4C;CAC/D;;;;;;CAOA,uBACE,QACoB;EACpB,IAAI,OAAO,SAAS,OAAO;GACzB,MAAM,EAAE,gBAAgB;GACxB,IAAI,YAAY,qBAAqB,KAAA,GACnC,OAAO,YAAY;GAErB;EACF;EACA,IAAI,OAAO,SAAS,OAAO;GACzB,MAAM,EAAE,gBAAgB;GACxB,OAAO,YAAY;EACrB;EAEA,OAAO,OAAO;CAChB;CAEA,cAAsB,QAAqC;EACzD,IAAI,CAAC,KAAK,YACR;EAEF,MAAM,aAAyB;IAC5B,sBAAsB,OAAO,KAAK,YAAY;IAC9C,uBAAuB,OAAO;IAE9B,uBAAuB,OAAO;IAC9B,wBAAwB,OAAO;IAC/B,oBAAoB,OAAO;IAC3B,iCAAiC,OAAO;EAC3C;EAEA,MAAM,YAAY,KAAK,uBAAuB,MAAM;EAEpD,MAAM,YAAuB;GAC3B,WAAW;GACX,gBAAgB,eAAe;GAC/B;GACA,GAAI,KAAK,yBACL,EAAE,MAAM,KAAK,UAAU,OAAO,WAAW,EAAE,IAC3C,CAAC;GACL,GAAI,cAAc,KAAA,IAAY,EAAE,UAAU,IAAI,CAAC;EACjD;EAEA,IAAI,KAAK,2BACP,6BACQ,KAAK,4BAA4B,SAAS,IAC/C,UAAU;GACT,IAAI,OACF,KAAK,MAAM,MAAM,wCAAwC,KAAK;EAElE,GACA,IACF;EAGF,KAAK,OAAO,KAAK,SAAS;CAC5B;AACF"}
@@ -4,7 +4,7 @@ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
4
4
  /**
5
5
  * WebVitalsInstrumentation Configuration
6
6
  */
7
- interface WebVitalsInstrumentationConfig extends InstrumentationConfig {
7
+ export interface WebVitalsInstrumentationConfig extends InstrumentationConfig {
8
8
  /**
9
9
  * @experimental
10
10
  * When true, sets the log record body to the JSON-stringified
@@ -21,5 +21,4 @@ interface WebVitalsInstrumentationConfig extends InstrumentationConfig {
21
21
  applyCustomLogRecordData?: (logRecord: LogRecord) => void;
22
22
  }
23
23
  //#endregion
24
- export { WebVitalsInstrumentationConfig };
25
24
  //# sourceMappingURL=types.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { XhrInstrumentationConfig } from "./types.js";
2
2
  import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
3
  //#region src/xhr/instrumentation.d.ts
4
- declare class XhrInstrumentation extends InstrumentationBase<XhrInstrumentationConfig> {
4
+ export declare class XhrInstrumentation extends InstrumentationBase<XhrInstrumentationConfig> {
5
5
  private _isEnabled;
6
6
  private _isXhrPatched;
7
7
  private _xhrSpanMap;
@@ -35,5 +35,4 @@ declare class XhrInstrumentation extends InstrumentationBase<XhrInstrumentationC
35
35
  private _addHeaders;
36
36
  }
37
37
  //#endregion
38
- export { XhrInstrumentation };
39
38
  //# sourceMappingURL=instrumentation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/xhr/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes, Context, Span } from '@opentelemetry/api';\nimport {\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n} from '@opentelemetry/api';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport {\n ATTR_ERROR_TYPE,\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_REQUEST_METHOD_ORIGINAL,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n} from '@opentelemetry/semantic-conventions';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { getNetworkContextRegistry } from '../utils/NetworkContextRegistry.ts';\nimport {\n getXHRBodyLength,\n normalizeHttpRequestMethod,\n} from '../utils/request.ts';\nimport { matchesUrl, parseUrl, serverPortFromUrl } from '../utils/url.ts';\nimport { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv.ts';\nimport type { XhrInstrumentationConfig } from './types.ts';\n\ntype XhrOpenFunction = typeof XMLHttpRequest.prototype.open;\ntype XhrSendFunction = typeof XMLHttpRequest.prototype.send;\ntype XhrEventName = 'abort' | 'timeout' | 'error' | 'load';\ntype XhrRecord = {\n method: string;\n url: URL;\n start?: number;\n span?: Span;\n listeners?: Record<XhrEventName, (...args: unknown[]) => unknown>;\n};\n\nexport class XhrInstrumentation extends InstrumentationBase<XhrInstrumentationConfig> {\n // Note: Intentionally *not* using `_enabled` as the field name to avoid\n // any possible confusion with the `_enabled` field used on the *Node.js*\n // InstrumentationBase class.\n // Also not initializing the fields to `false` because the base class\n // constructor already call `enable` modifying their values and it will\n // set the instrumentaitons in a base state (enabled, patched but with flags set to false)\n private declare _isEnabled: boolean;\n private declare _isXhrPatched: boolean;\n\n // To keep references to span/xhr tuples across XHR events and stores\n // init data like URL and method\n private _xhrSpanMap: WeakMap<XMLHttpRequest, XhrRecord> = new WeakMap();\n\n constructor(config: XhrInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/xhr', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n\n if (this._isXhrPatched) {\n this._diag.debug('XMLHttpRequest prototype already patched');\n this._isEnabled = true;\n return;\n }\n\n // `_wrap` throws if a third-party script has locked the target methods via\n // Object.defineProperty(XMLHttpRequest.prototype, 'open', { writable: false, ... }).\n try {\n this._wrap(XMLHttpRequest.prototype, 'open', this._patchOpen());\n } catch (err) {\n this._diag.warn(\n 'Failed to patch XMLHttpRequest.prototype.open; instrumentation will not be enabled. ' +\n 'Another script may have locked XMLHttpRequest.prototype.open via Object.defineProperty.',\n err,\n );\n return;\n }\n\n // If 1st patch has succeded try the second. Unpatch `open` if error\n // here to avoid having multiple patches of `open`\n try {\n this._wrap(XMLHttpRequest.prototype, 'send', this._patchSend());\n this._isXhrPatched = true;\n this._isEnabled = true;\n } catch (err) {\n this._unwrap(XMLHttpRequest.prototype, 'open');\n this._diag.warn(\n 'Failed to patch XMLHttpRequest.prototype.send; instrumentation will not be enabled. ' +\n 'Another script may have locked XMLHttpRequest.prototype.send via Object.defineProperty.',\n err,\n );\n }\n }\n\n override disable(): void {\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n }\n\n /**\n * Patches the \"open\" method of XmlHttpRequest\n */\n private _patchOpen(): (original: XhrOpenFunction) => XhrOpenFunction {\n return (original) => {\n const instrumentation = this;\n\n return function patchedOpen(\n this: XMLHttpRequest,\n ...args: Parameters<XhrOpenFunction>\n ): ReturnType<XhrOpenFunction> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n try {\n const method = args[0];\n const urlArgument =\n typeof args[1] === 'string' ? args[1] : args[1].toString();\n const url = parseUrl(urlArgument);\n const shouldIgnoreUrl = matchesUrl(\n url.href,\n instrumentation.getConfig().ignoreUrls,\n );\n if (shouldIgnoreUrl) {\n return original.apply(this, args);\n }\n instrumentation._xhrSpanMap.set(this, { method, url });\n } catch (e: unknown) {\n instrumentation._diag.error(\n 'Failed to instrument XmlHttpRequest.open',\n e,\n );\n }\n return original.apply(this, args);\n } as XhrOpenFunction;\n };\n }\n\n /**\n * Patches the \"open\" method of XmlHttpRequest\n */\n private _patchSend(): (original: XhrSendFunction) => XhrSendFunction {\n return (original) => {\n const instrumentation = this;\n\n return function patchedSend(\n this: XMLHttpRequest,\n ...args: Parameters<XhrSendFunction>\n ): ReturnType<XhrSendFunction> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n\n // Instrument the method if the XHR was previously picked in `open`\n const xhrRecord = instrumentation._xhrSpanMap.get(this);\n if (xhrRecord) {\n try {\n const { method, url } = xhrRecord;\n const span = instrumentation._createSpan(url, method);\n xhrRecord.span = span;\n xhrRecord.start = performance.now();\n\n if (instrumentation.getConfig().measureRequestSize && args?.[0]) {\n const bodyLength = getXHRBodyLength(args[0]);\n if (bodyLength) {\n span.setAttribute(ATTR_HTTP_REQUEST_BODY_SIZE, bodyLength);\n }\n }\n\n const onAbort = () => instrumentation._endSpan(this, 'abort');\n const onError = () => instrumentation._endSpan(this, 'error');\n const onTimeout = () => instrumentation._endSpan(this, 'timeout');\n const onLoad = () => instrumentation._endSpan(this, 'load');\n xhrRecord.listeners = {\n abort: onAbort,\n error: onError,\n timeout: onTimeout,\n load: onLoad,\n };\n\n const xhrContext = trace.setSpan(context.active(), span);\n context.with(xhrContext, () => {\n this.addEventListener('abort', onAbort);\n this.addEventListener('error', onError);\n this.addEventListener('timeout', onTimeout);\n this.addEventListener('load', onLoad);\n instrumentation._addHeaders(this, url, xhrContext);\n });\n } catch (e: unknown) {\n // failed to instrument request, remove span\n instrumentation._diag.error(\n 'Failed to instrument fetch request',\n e,\n );\n instrumentation._xhrSpanMap.delete(this);\n }\n }\n return original.apply(this, args);\n } as XhrSendFunction;\n };\n }\n\n /**\n * Creates a new span when method \"send\" is called\n */\n private _createSpan(url: URL, method: string): Span {\n const origMethod = method;\n const normMethod = normalizeHttpRequestMethod(method);\n const attributes = {} as Attributes;\n const { sanitizeUrl } = this.getConfig();\n\n attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;\n if (normMethod !== origMethod) {\n attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;\n }\n attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url.href) : url.href;\n attributes[ATTR_SERVER_ADDRESS] = url.hostname;\n const serverPort = serverPortFromUrl(url);\n if (serverPort) {\n attributes[ATTR_SERVER_PORT] = serverPort;\n }\n\n return this.tracer.startSpan(normMethod, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n }\n\n /**\n * Finish span, add attributes, network events etc.\n */\n private _endSpan(xhr: XMLHttpRequest, eventName: XhrEventName) {\n const xhrRecord = this._xhrSpanMap.get(xhr);\n\n if (xhrRecord?.listeners) {\n for (const k of Object.keys(xhrRecord.listeners)) {\n xhr.removeEventListener(\n k as XhrEventName,\n xhrRecord.listeners[k as XhrEventName],\n );\n }\n }\n\n if (xhrRecord?.span) {\n const { span, url, start } = xhrRecord;\n\n // Status code only has a meaningful value if request got a response\n if (eventName === 'load') {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, xhr.status);\n }\n\n if (eventName !== 'abort') {\n // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status\n // 1xx/2xx/3xx MUST be left unset UNLESS there was another error (e.g. a\n // network error receiving the response body)\n const isErrorStatus =\n xhr.status >= 400 || eventName === 'error' || eventName === 'timeout';\n\n if (isErrorStatus) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n const errorType =\n xhr.status >= 400\n ? String(xhr.status)\n : eventName === 'error'\n ? 'Error'\n : 'TimeoutError';\n span.setAttribute(ATTR_ERROR_TYPE, errorType);\n }\n }\n\n this._xhrSpanMap.delete(xhr);\n this._applyAttributesAfterSend(span, xhr);\n span.end();\n\n getNetworkContextRegistry().register(span, {\n key: url.href,\n startPerfNow: Number(start),\n endPerfNow: performance.now(),\n });\n }\n }\n\n /**\n * Runs the user provided `applyCustomAttributesOnSpan` function\n */\n private _applyAttributesAfterSend(span: Span, xhr: XMLHttpRequest) {\n const applyCustomAttributesOnSpan =\n this.getConfig().applyCustomAttributesOnSpan;\n if (applyCustomAttributesOnSpan) {\n safeExecuteInTheMiddle(\n () => applyCustomAttributesOnSpan(span, xhr),\n (error) => {\n if (!error) {\n return;\n }\n this._diag.error('applyCustomAttributesOnSpan', error);\n },\n true,\n );\n }\n }\n\n /**\n * Adds custom headers to XMLHttpRequest\n */\n private _addHeaders(xhr: XMLHttpRequest, url: URL, ctx: Context) {\n // Propagate only if in request goes to same origin or is in the allow list\n const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;\n const sameOrigin = location.origin === url.origin;\n const shouldPropagate = sameOrigin || matchesUrl(url.href, urlsToPropagate);\n\n if (shouldPropagate) {\n propagation.inject(ctx, xhr, {\n set: (x, k, v) => {\n x.setRequestHeader(k, typeof v === 'string' ? v : String(v));\n },\n });\n } else {\n const headers: Partial<Record<string, unknown>> = {};\n propagation.inject(ctx, headers, {\n set: (h, k, v) => {\n h[k] = v;\n },\n });\n if (Object.keys(headers).length > 0) {\n this._diag.debug('headers inject skipped due to CORS policy');\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;AA+CA,IAAa,qBAAb,cAAwC,oBAA8C;CAYpF,8BAA0D,IAAI,QAAQ;CAEtE,YAAY,SAAmC,CAAC,GAAG;EACjD,MAAM,8CAA8C,SAAS,MAAM;CACrE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,SAAwB;EACtB,IAAI,KAAK,YACP;EAGF,IAAI,KAAK,eAAe;GACtB,KAAK,MAAM,MAAM,0CAA0C;GAC3D,KAAK,aAAa;GAClB;EACF;EAIA,IAAI;GACF,KAAK,MAAM,eAAe,WAAW,QAAQ,KAAK,WAAW,CAAC;EAChE,SAAS,KAAK;GACZ,KAAK,MAAM,KACT,+KAEA,GACF;GACA;EACF;EAIA,IAAI;GACF,KAAK,MAAM,eAAe,WAAW,QAAQ,KAAK,WAAW,CAAC;GAC9D,KAAK,gBAAgB;GACrB,KAAK,aAAa;EACpB,SAAS,KAAK;GACZ,KAAK,QAAQ,eAAe,WAAW,MAAM;GAC7C,KAAK,MAAM,KACT,+KAEA,GACF;EACF;CACF;CAEA,UAAyB;EACvB,IAAI,CAAC,KAAK,YACR;EAEF,KAAK,aAAa;CACpB;;;;CAKA,aAAqE;EACnE,QAAQ,aAAa;GACnB,MAAM,kBAAkB;GAExB,OAAO,SAAS,YAEd,GAAG,MAC0B;IAC7B,IAAI,CAAC,gBAAgB,YACnB,OAAO,SAAS,MAAM,MAAM,IAAI;IAElC,IAAI;KACF,MAAM,SAAS,KAAK;KACpB,MAAM,cACJ,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK,KAAK,EAAE,CAAC,SAAS;KAC3D,MAAM,MAAM,SAAS,WAAW;KAKhC,IAJwB,WACtB,IAAI,MACJ,gBAAgB,UAAU,CAAC,CAAC,UAEZ,GAChB,OAAO,SAAS,MAAM,MAAM,IAAI;KAElC,gBAAgB,YAAY,IAAI,MAAM;MAAE;MAAQ;KAAI,CAAC;IACvD,SAAS,GAAY;KACnB,gBAAgB,MAAM,MACpB,4CACA,CACF;IACF;IACA,OAAO,SAAS,MAAM,MAAM,IAAI;GAClC;EACF;CACF;;;;CAKA,aAAqE;EACnE,QAAQ,aAAa;GACnB,MAAM,kBAAkB;GAExB,OAAO,SAAS,YAEd,GAAG,MAC0B;IAC7B,IAAI,CAAC,gBAAgB,YACnB,OAAO,SAAS,MAAM,MAAM,IAAI;IAIlC,MAAM,YAAY,gBAAgB,YAAY,IAAI,IAAI;IACtD,IAAI,WACF,IAAI;KACF,MAAM,EAAE,QAAQ,QAAQ;KACxB,MAAM,OAAO,gBAAgB,YAAY,KAAK,MAAM;KACpD,UAAU,OAAO;KACjB,UAAU,QAAQ,YAAY,IAAI;KAElC,IAAI,gBAAgB,UAAU,CAAC,CAAC,sBAAsB,OAAO,IAAI;MAC/D,MAAM,aAAa,iBAAiB,KAAK,EAAE;MAC3C,IAAI,YACF,KAAK,aAAa,6BAA6B,UAAU;KAE7D;KAEA,MAAM,gBAAgB,gBAAgB,SAAS,MAAM,OAAO;KAC5D,MAAM,gBAAgB,gBAAgB,SAAS,MAAM,OAAO;KAC5D,MAAM,kBAAkB,gBAAgB,SAAS,MAAM,SAAS;KAChE,MAAM,eAAe,gBAAgB,SAAS,MAAM,MAAM;KAC1D,UAAU,YAAY;MACpB,OAAO;MACP,OAAO;MACP,SAAS;MACT,MAAM;KACR;KAEA,MAAM,aAAa,MAAM,QAAQ,QAAQ,OAAO,GAAG,IAAI;KACvD,QAAQ,KAAK,kBAAkB;MAC7B,KAAK,iBAAiB,SAAS,OAAO;MACtC,KAAK,iBAAiB,SAAS,OAAO;MACtC,KAAK,iBAAiB,WAAW,SAAS;MAC1C,KAAK,iBAAiB,QAAQ,MAAM;MACpC,gBAAgB,YAAY,MAAM,KAAK,UAAU;KACnD,CAAC;IACH,SAAS,GAAY;KAEnB,gBAAgB,MAAM,MACpB,sCACA,CACF;KACA,gBAAgB,YAAY,OAAO,IAAI;IACzC;IAEF,OAAO,SAAS,MAAM,MAAM,IAAI;GAClC;EACF;CACF;;;;CAKA,YAAoB,KAAU,QAAsB;EAClD,MAAM,aAAa;EACnB,MAAM,aAAa,2BAA2B,MAAM;EACpD,MAAM,aAAa,CAAC;EACpB,MAAM,EAAE,gBAAgB,KAAK,UAAU;EAEvC,WAAW,4BAA4B;EACvC,IAAI,eAAe,YACjB,WAAW,qCAAqC;EAElD,WAAW,iBAAiB,cAAc,YAAY,IAAI,IAAI,IAAI,IAAI;EACtE,WAAW,uBAAuB,IAAI;EACtC,MAAM,aAAa,kBAAkB,GAAG;EACxC,IAAI,YACF,WAAW,oBAAoB;EAGjC,OAAO,KAAK,OAAO,UAAU,YAAY;GACvC,MAAM,SAAS;GACf;EACF,CAAC;CACH;;;;CAKA,SAAiB,KAAqB,WAAyB;EAC7D,MAAM,YAAY,KAAK,YAAY,IAAI,GAAG;EAE1C,IAAI,WAAW,WACb,KAAK,MAAM,KAAK,OAAO,KAAK,UAAU,SAAS,GAC7C,IAAI,oBACF,GACA,UAAU,UAAU,EACtB;EAIJ,IAAI,WAAW,MAAM;GACnB,MAAM,EAAE,MAAM,KAAK,UAAU;GAG7B,IAAI,cAAc,QAChB,KAAK,aAAa,gCAAgC,IAAI,MAAM;GAG9D,IAAI,cAAc,SAKd;QAAA,IAAI,UAAU,OAAO,cAAc,WAAW,cAAc,WAE3C;KACjB,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;KAC7C,MAAM,YACJ,IAAI,UAAU,MACV,OAAO,IAAI,MAAM,IACjB,cAAc,UACZ,UACA;KACR,KAAK,aAAa,iBAAiB,SAAS;IAC9C;;GAGF,KAAK,YAAY,OAAO,GAAG;GAC3B,KAAK,0BAA0B,MAAM,GAAG;GACxC,KAAK,IAAI;GAET,0BAA0B,CAAC,CAAC,SAAS,MAAM;IACzC,KAAK,IAAI;IACT,cAAc,OAAO,KAAK;IAC1B,YAAY,YAAY,IAAI;GAC9B,CAAC;EACH;CACF;;;;CAKA,0BAAkC,MAAY,KAAqB;EACjE,MAAM,8BACJ,KAAK,UAAU,CAAC,CAAC;EACnB,IAAI,6BACF,6BACQ,4BAA4B,MAAM,GAAG,IAC1C,UAAU;GACT,IAAI,CAAC,OACH;GAEF,KAAK,MAAM,MAAM,+BAA+B,KAAK;EACvD,GACA,IACF;CAEJ;;;;CAKA,YAAoB,KAAqB,KAAU,KAAc;EAE/D,MAAM,kBAAkB,KAAK,UAAU,CAAC,CAAC;EAIzC,IAHmB,SAAS,WAAW,IAAI,UACL,WAAW,IAAI,MAAM,eAAe,GAGxE,YAAY,OAAO,KAAK,KAAK,EAC3B,MAAM,GAAG,GAAG,MAAM;GAChB,EAAE,iBAAiB,GAAG,OAAO,MAAM,WAAW,IAAI,OAAO,CAAC,CAAC;EAC7D,EACF,CAAC;OACI;GACL,MAAM,UAA4C,CAAC;GACnD,YAAY,OAAO,KAAK,SAAS,EAC/B,MAAM,GAAG,GAAG,MAAM;IAChB,EAAE,KAAK;GACT,EACF,CAAC;GACD,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAChC,KAAK,MAAM,MAAM,2CAA2C;EAEhE;CACF;AACF"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/xhr/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes, Context, Span } from '@opentelemetry/api';\nimport {\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n} from '@opentelemetry/api';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport {\n ATTR_ERROR_TYPE,\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_REQUEST_METHOD_ORIGINAL,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n} from '@opentelemetry/semantic-conventions';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { getNetworkContextRegistry } from '../utils/NetworkContextRegistry.ts';\nimport {\n getXHRBodyLength,\n normalizeHttpRequestMethod,\n} from '../utils/request.ts';\nimport { matchesUrl, parseUrl, serverPortFromUrl } from '../utils/url.ts';\nimport { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv.ts';\nimport type { XhrInstrumentationConfig } from './types.ts';\n\ntype XhrOpenFunction = typeof XMLHttpRequest.prototype.open;\ntype XhrSendFunction = typeof XMLHttpRequest.prototype.send;\ntype XhrEventName = 'abort' | 'timeout' | 'error' | 'load';\ntype XhrRecord = {\n method: string;\n url: URL;\n start?: number;\n span?: Span;\n listeners?: Record<XhrEventName, (...args: unknown[]) => unknown>;\n};\n\nexport class XhrInstrumentation extends InstrumentationBase<XhrInstrumentationConfig> {\n // Note: Intentionally *not* using `_enabled` as the field name to avoid\n // any possible confusion with the `_enabled` field used on the *Node.js*\n // InstrumentationBase class.\n // Also not initializing the fields to `false` because the base class\n // constructor already call `enable` modifying their values and it will\n // set the instrumentaitons in a base state (enabled, patched but with flags set to false)\n declare private _isEnabled: boolean;\n declare private _isXhrPatched: boolean;\n\n // To keep references to span/xhr tuples across XHR events and stores\n // init data like URL and method\n private _xhrSpanMap: WeakMap<XMLHttpRequest, XhrRecord> = new WeakMap();\n\n constructor(config: XhrInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/xhr', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n\n if (this._isXhrPatched) {\n this._diag.debug('XMLHttpRequest prototype already patched');\n this._isEnabled = true;\n return;\n }\n\n // `_wrap` throws if a third-party script has locked the target methods via\n // Object.defineProperty(XMLHttpRequest.prototype, 'open', { writable: false, ... }).\n try {\n this._wrap(XMLHttpRequest.prototype, 'open', this._patchOpen());\n } catch (err) {\n this._diag.warn(\n 'Failed to patch XMLHttpRequest.prototype.open; instrumentation will not be enabled. ' +\n 'Another script may have locked XMLHttpRequest.prototype.open via Object.defineProperty.',\n err,\n );\n return;\n }\n\n // If 1st patch has succeded try the second. Unpatch `open` if error\n // here to avoid having multiple patches of `open`\n try {\n this._wrap(XMLHttpRequest.prototype, 'send', this._patchSend());\n this._isXhrPatched = true;\n this._isEnabled = true;\n } catch (err) {\n this._unwrap(XMLHttpRequest.prototype, 'open');\n this._diag.warn(\n 'Failed to patch XMLHttpRequest.prototype.send; instrumentation will not be enabled. ' +\n 'Another script may have locked XMLHttpRequest.prototype.send via Object.defineProperty.',\n err,\n );\n }\n }\n\n override disable(): void {\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n }\n\n /**\n * Patches the \"open\" method of XmlHttpRequest\n */\n private _patchOpen(): (original: XhrOpenFunction) => XhrOpenFunction {\n return (original) => {\n const instrumentation = this;\n\n return function patchedOpen(\n this: XMLHttpRequest,\n ...args: Parameters<XhrOpenFunction>\n ): ReturnType<XhrOpenFunction> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n try {\n const method = args[0];\n const urlArgument =\n typeof args[1] === 'string' ? args[1] : args[1].toString();\n const url = parseUrl(urlArgument);\n const shouldIgnoreUrl = matchesUrl(\n url.href,\n instrumentation.getConfig().ignoreUrls,\n );\n if (shouldIgnoreUrl) {\n return original.apply(this, args);\n }\n instrumentation._xhrSpanMap.set(this, { method, url });\n } catch (e: unknown) {\n instrumentation._diag.error(\n 'Failed to instrument XmlHttpRequest.open',\n e,\n );\n }\n return original.apply(this, args);\n } as XhrOpenFunction;\n };\n }\n\n /**\n * Patches the \"open\" method of XmlHttpRequest\n */\n private _patchSend(): (original: XhrSendFunction) => XhrSendFunction {\n return (original) => {\n const instrumentation = this;\n\n return function patchedSend(\n this: XMLHttpRequest,\n ...args: Parameters<XhrSendFunction>\n ): ReturnType<XhrSendFunction> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n\n // Instrument the method if the XHR was previously picked in `open`\n const xhrRecord = instrumentation._xhrSpanMap.get(this);\n if (xhrRecord) {\n try {\n const { method, url } = xhrRecord;\n const span = instrumentation._createSpan(url, method);\n xhrRecord.span = span;\n xhrRecord.start = performance.now();\n\n if (instrumentation.getConfig().measureRequestSize && args?.[0]) {\n const bodyLength = getXHRBodyLength(args[0]);\n if (bodyLength) {\n span.setAttribute(ATTR_HTTP_REQUEST_BODY_SIZE, bodyLength);\n }\n }\n\n const onAbort = () => instrumentation._endSpan(this, 'abort');\n const onError = () => instrumentation._endSpan(this, 'error');\n const onTimeout = () => instrumentation._endSpan(this, 'timeout');\n const onLoad = () => instrumentation._endSpan(this, 'load');\n xhrRecord.listeners = {\n abort: onAbort,\n error: onError,\n timeout: onTimeout,\n load: onLoad,\n };\n\n const xhrContext = trace.setSpan(context.active(), span);\n context.with(xhrContext, () => {\n this.addEventListener('abort', onAbort);\n this.addEventListener('error', onError);\n this.addEventListener('timeout', onTimeout);\n this.addEventListener('load', onLoad);\n instrumentation._addHeaders(this, url, xhrContext);\n });\n } catch (e: unknown) {\n // failed to instrument request, remove span\n instrumentation._diag.error(\n 'Failed to instrument fetch request',\n e,\n );\n instrumentation._xhrSpanMap.delete(this);\n }\n }\n return original.apply(this, args);\n } as XhrSendFunction;\n };\n }\n\n /**\n * Creates a new span when method \"send\" is called\n */\n private _createSpan(url: URL, method: string): Span {\n const origMethod = method;\n const normMethod = normalizeHttpRequestMethod(method);\n const attributes = {} as Attributes;\n const { sanitizeUrl } = this.getConfig();\n\n attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;\n if (normMethod !== origMethod) {\n attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;\n }\n attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url.href) : url.href;\n attributes[ATTR_SERVER_ADDRESS] = url.hostname;\n const serverPort = serverPortFromUrl(url);\n if (serverPort) {\n attributes[ATTR_SERVER_PORT] = serverPort;\n }\n\n return this.tracer.startSpan(normMethod, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n }\n\n /**\n * Finish span, add attributes, network events etc.\n */\n private _endSpan(xhr: XMLHttpRequest, eventName: XhrEventName) {\n const xhrRecord = this._xhrSpanMap.get(xhr);\n\n if (xhrRecord?.listeners) {\n for (const k of Object.keys(xhrRecord.listeners)) {\n xhr.removeEventListener(\n k as XhrEventName,\n xhrRecord.listeners[k as XhrEventName],\n );\n }\n }\n\n if (xhrRecord?.span) {\n const { span, url, start } = xhrRecord;\n\n // Status code only has a meaningful value if request got a response\n if (eventName === 'load') {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, xhr.status);\n }\n\n if (eventName !== 'abort') {\n // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status\n // 1xx/2xx/3xx MUST be left unset UNLESS there was another error (e.g. a\n // network error receiving the response body)\n const isErrorStatus =\n xhr.status >= 400 || eventName === 'error' || eventName === 'timeout';\n\n if (isErrorStatus) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n const errorType =\n xhr.status >= 400\n ? String(xhr.status)\n : eventName === 'error'\n ? 'Error'\n : 'TimeoutError';\n span.setAttribute(ATTR_ERROR_TYPE, errorType);\n }\n }\n\n this._xhrSpanMap.delete(xhr);\n this._applyAttributesAfterSend(span, xhr);\n span.end();\n\n getNetworkContextRegistry().register(span, {\n key: url.href,\n startPerfNow: Number(start),\n endPerfNow: performance.now(),\n });\n }\n }\n\n /**\n * Runs the user provided `applyCustomAttributesOnSpan` function\n */\n private _applyAttributesAfterSend(span: Span, xhr: XMLHttpRequest) {\n const applyCustomAttributesOnSpan =\n this.getConfig().applyCustomAttributesOnSpan;\n if (applyCustomAttributesOnSpan) {\n safeExecuteInTheMiddle(\n () => applyCustomAttributesOnSpan(span, xhr),\n (error) => {\n if (!error) {\n return;\n }\n this._diag.error('applyCustomAttributesOnSpan', error);\n },\n true,\n );\n }\n }\n\n /**\n * Adds custom headers to XMLHttpRequest\n */\n private _addHeaders(xhr: XMLHttpRequest, url: URL, ctx: Context) {\n // Propagate only if in request goes to same origin or is in the allow list\n const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;\n const sameOrigin = location.origin === url.origin;\n const shouldPropagate = sameOrigin || matchesUrl(url.href, urlsToPropagate);\n\n if (shouldPropagate) {\n propagation.inject(ctx, xhr, {\n set: (x, k, v) => {\n x.setRequestHeader(k, typeof v === 'string' ? v : String(v));\n },\n });\n } else {\n const headers: Partial<Record<string, unknown>> = {};\n propagation.inject(ctx, headers, {\n set: (h, k, v) => {\n h[k] = v;\n },\n });\n if (Object.keys(headers).length > 0) {\n this._diag.debug('headers inject skipped due to CORS policy');\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;AA+CA,IAAa,qBAAb,cAAwC,oBAA8C;CAYpF,8BAA0D,IAAI,QAAQ;CAEtE,YAAY,SAAmC,CAAC,GAAG;EACjD,MAAM,8CAA8C,SAAS,MAAM;CACrE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,SAAwB;EACtB,IAAI,KAAK,YACP;EAGF,IAAI,KAAK,eAAe;GACtB,KAAK,MAAM,MAAM,0CAA0C;GAC3D,KAAK,aAAa;GAClB;EACF;EAIA,IAAI;GACF,KAAK,MAAM,eAAe,WAAW,QAAQ,KAAK,WAAW,CAAC;EAChE,SAAS,KAAK;GACZ,KAAK,MAAM,KACT,+KAEA,GACF;GACA;EACF;EAIA,IAAI;GACF,KAAK,MAAM,eAAe,WAAW,QAAQ,KAAK,WAAW,CAAC;GAC9D,KAAK,gBAAgB;GACrB,KAAK,aAAa;EACpB,SAAS,KAAK;GACZ,KAAK,QAAQ,eAAe,WAAW,MAAM;GAC7C,KAAK,MAAM,KACT,+KAEA,GACF;EACF;CACF;CAEA,UAAyB;EACvB,IAAI,CAAC,KAAK,YACR;EAEF,KAAK,aAAa;CACpB;;;;CAKA,aAAqE;EACnE,QAAQ,aAAa;GACnB,MAAM,kBAAkB;GAExB,OAAO,SAAS,YAEd,GAAG,MAC0B;IAC7B,IAAI,CAAC,gBAAgB,YACnB,OAAO,SAAS,MAAM,MAAM,IAAI;IAElC,IAAI;KACF,MAAM,SAAS,KAAK;KACpB,MAAM,cACJ,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK,KAAK,EAAE,CAAC,SAAS;KAC3D,MAAM,MAAM,SAAS,WAAW;KAKhC,IAJwB,WACtB,IAAI,MACJ,gBAAgB,UAAU,CAAC,CAAC,UAEZ,GAChB,OAAO,SAAS,MAAM,MAAM,IAAI;KAElC,gBAAgB,YAAY,IAAI,MAAM;MAAE;MAAQ;KAAI,CAAC;IACvD,SAAS,GAAY;KACnB,gBAAgB,MAAM,MACpB,4CACA,CACF;IACF;IACA,OAAO,SAAS,MAAM,MAAM,IAAI;GAClC;EACF;CACF;;;;CAKA,aAAqE;EACnE,QAAQ,aAAa;GACnB,MAAM,kBAAkB;GAExB,OAAO,SAAS,YAEd,GAAG,MAC0B;IAC7B,IAAI,CAAC,gBAAgB,YACnB,OAAO,SAAS,MAAM,MAAM,IAAI;IAIlC,MAAM,YAAY,gBAAgB,YAAY,IAAI,IAAI;IACtD,IAAI,WACF,IAAI;KACF,MAAM,EAAE,QAAQ,QAAQ;KACxB,MAAM,OAAO,gBAAgB,YAAY,KAAK,MAAM;KACpD,UAAU,OAAO;KACjB,UAAU,QAAQ,YAAY,IAAI;KAElC,IAAI,gBAAgB,UAAU,CAAC,CAAC,sBAAsB,OAAO,IAAI;MAC/D,MAAM,aAAa,iBAAiB,KAAK,EAAE;MAC3C,IAAI,YACF,KAAK,aAAa,6BAA6B,UAAU;KAE7D;KAEA,MAAM,gBAAgB,gBAAgB,SAAS,MAAM,OAAO;KAC5D,MAAM,gBAAgB,gBAAgB,SAAS,MAAM,OAAO;KAC5D,MAAM,kBAAkB,gBAAgB,SAAS,MAAM,SAAS;KAChE,MAAM,eAAe,gBAAgB,SAAS,MAAM,MAAM;KAC1D,UAAU,YAAY;MACpB,OAAO;MACP,OAAO;MACP,SAAS;MACT,MAAM;KACR;KAEA,MAAM,aAAa,MAAM,QAAQ,QAAQ,OAAO,GAAG,IAAI;KACvD,QAAQ,KAAK,kBAAkB;MAC7B,KAAK,iBAAiB,SAAS,OAAO;MACtC,KAAK,iBAAiB,SAAS,OAAO;MACtC,KAAK,iBAAiB,WAAW,SAAS;MAC1C,KAAK,iBAAiB,QAAQ,MAAM;MACpC,gBAAgB,YAAY,MAAM,KAAK,UAAU;KACnD,CAAC;IACH,SAAS,GAAY;KAEnB,gBAAgB,MAAM,MACpB,sCACA,CACF;KACA,gBAAgB,YAAY,OAAO,IAAI;IACzC;IAEF,OAAO,SAAS,MAAM,MAAM,IAAI;GAClC;EACF;CACF;;;;CAKA,YAAoB,KAAU,QAAsB;EAClD,MAAM,aAAa;EACnB,MAAM,aAAa,2BAA2B,MAAM;EACpD,MAAM,aAAa,CAAC;EACpB,MAAM,EAAE,gBAAgB,KAAK,UAAU;EAEvC,WAAW,4BAA4B;EACvC,IAAI,eAAe,YACjB,WAAW,qCAAqC;EAElD,WAAW,iBAAiB,cAAc,YAAY,IAAI,IAAI,IAAI,IAAI;EACtE,WAAW,uBAAuB,IAAI;EACtC,MAAM,aAAa,kBAAkB,GAAG;EACxC,IAAI,YACF,WAAW,oBAAoB;EAGjC,OAAO,KAAK,OAAO,UAAU,YAAY;GACvC,MAAM,SAAS;GACf;EACF,CAAC;CACH;;;;CAKA,SAAiB,KAAqB,WAAyB;EAC7D,MAAM,YAAY,KAAK,YAAY,IAAI,GAAG;EAE1C,IAAI,WAAW,WACb,KAAK,MAAM,KAAK,OAAO,KAAK,UAAU,SAAS,GAC7C,IAAI,oBACF,GACA,UAAU,UAAU,EACtB;EAIJ,IAAI,WAAW,MAAM;GACnB,MAAM,EAAE,MAAM,KAAK,UAAU;GAG7B,IAAI,cAAc,QAChB,KAAK,aAAa,gCAAgC,IAAI,MAAM;GAG9D,IAAI,cAAc,SAKd;QAAA,IAAI,UAAU,OAAO,cAAc,WAAW,cAAc,WAE3C;KACjB,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;KAC7C,MAAM,YACJ,IAAI,UAAU,MACV,OAAO,IAAI,MAAM,IACjB,cAAc,UACZ,UACA;KACR,KAAK,aAAa,iBAAiB,SAAS;IAC9C;;GAGF,KAAK,YAAY,OAAO,GAAG;GAC3B,KAAK,0BAA0B,MAAM,GAAG;GACxC,KAAK,IAAI;GAET,0BAA0B,CAAC,CAAC,SAAS,MAAM;IACzC,KAAK,IAAI;IACT,cAAc,OAAO,KAAK;IAC1B,YAAY,YAAY,IAAI;GAC9B,CAAC;EACH;CACF;;;;CAKA,0BAAkC,MAAY,KAAqB;EACjE,MAAM,8BACJ,KAAK,UAAU,CAAC,CAAC;EACnB,IAAI,6BACF,6BACQ,4BAA4B,MAAM,GAAG,IAC1C,UAAU;GACT,IAAI,CAAC,OACH;GAEF,KAAK,MAAM,MAAM,+BAA+B,KAAK;EACvD,GACA,IACF;CAEJ;;;;CAKA,YAAoB,KAAqB,KAAU,KAAc;EAE/D,MAAM,kBAAkB,KAAK,UAAU,CAAC,CAAC;EAIzC,IAHmB,SAAS,WAAW,IAAI,UACL,WAAW,IAAI,MAAM,eAAe,GAGxE,YAAY,OAAO,KAAK,KAAK,EAC3B,MAAM,GAAG,GAAG,MAAM;GAChB,EAAE,iBAAiB,GAAG,OAAO,MAAM,WAAW,IAAI,OAAO,CAAC,CAAC;EAC7D,EACF,CAAC;OACI;GACL,MAAM,UAA4C,CAAC;GACnD,YAAY,OAAO,KAAK,SAAS,EAC/B,MAAM,GAAG,GAAG,MAAM;IAChB,EAAE,KAAK;GACT,EACF,CAAC;GACD,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAChC,KAAK,MAAM,MAAM,2CAA2C;EAEhE;CACF;AACF"}
@@ -2,7 +2,7 @@ import { Span } from "@opentelemetry/api";
2
2
  import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
3
  //#region src/xhr/types.d.ts
4
4
  type XhrCustomAttributeFunction = (span: Span, xhr: XMLHttpRequest) => void;
5
- interface XhrInstrumentationConfig extends InstrumentationConfig {
5
+ export interface XhrInstrumentationConfig extends InstrumentationConfig {
6
6
  /** URLs which should include trace headers when origin doesn't match */
7
7
  propagateTraceHeaderCorsUrls?: Array<string | RegExp>;
8
8
  /**
@@ -19,5 +19,4 @@ interface XhrInstrumentationConfig extends InstrumentationConfig {
19
19
  sanitizeUrl?: (url: string) => string;
20
20
  }
21
21
  //#endregion
22
- export { XhrInstrumentationConfig };
23
22
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentelemetry/browser-instrumentation",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "OpenTelemetry browser instrumentations.",
5
5
  "keywords": [
6
6
  "opentelemetry",
@@ -50,27 +50,33 @@
50
50
  "build": "tsdown",
51
51
  "watch": "tsdown --watch --no-clean",
52
52
  "check-types": "tsc",
53
+ "msw:init": "msw init",
53
54
  "test": "vitest run",
54
55
  "test:watch": "vitest",
55
56
  "test:coverage": "vitest --coverage"
56
57
  },
57
58
  "dependencies": {
58
- "@opentelemetry/api-logs": "^0.221.0",
59
- "@opentelemetry/instrumentation": "^0.221.0",
59
+ "@opentelemetry/api-logs": "^0.223.0",
60
+ "@opentelemetry/instrumentation": "^0.223.0",
60
61
  "@opentelemetry/semantic-conventions": "^1.43.0",
61
- "web-vitals": "^6.1.1"
62
+ "web-vitals": "^6.2.2"
62
63
  },
63
64
  "peerDependencies": {
64
65
  "@opentelemetry/api": "^1.9.1"
65
66
  },
66
67
  "devDependencies": {
67
- "@opentelemetry/core": "2.10.0",
68
- "@opentelemetry/propagator-b3": "2.10.0",
69
- "@opentelemetry/sdk-logs": "0.221.0",
70
- "@opentelemetry/sdk-trace-base": "2.10.0",
71
- "msw": "2.15.0"
68
+ "@opentelemetry/core": "2.12.0",
69
+ "@opentelemetry/propagator-b3": "2.12.0",
70
+ "@opentelemetry/sdk-logs": "0.223.0",
71
+ "@opentelemetry/sdk-trace-base": "2.12.0",
72
+ "msw": "3.0.1"
72
73
  },
73
74
  "publishConfig": {
74
75
  "access": "public"
76
+ },
77
+ "msw": {
78
+ "workerDirectory": [
79
+ "public"
80
+ ]
75
81
  }
76
82
  }