@opentelemetry/browser-instrumentation 0.5.0 → 0.5.1
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.
- package/dist/console/instrumentation.js.map +1 -1
- package/dist/errors/instrumentation.js.map +1 -1
- package/dist/navigation/index.d.ts +1 -2
- package/dist/navigation/index.js +1 -2
- package/dist/navigation/instrumentation.js.map +1 -1
- package/dist/navigation/utils.js +1 -47
- package/dist/navigation/utils.js.map +1 -1
- package/dist/navigation-timing/instrumentation.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/resource-timing/idle-callback-shim.js.map +1 -1
- package/dist/resource-timing/instrumentation.js +25 -15
- package/dist/resource-timing/instrumentation.js.map +1 -1
- package/dist/resource-timing/types.d.ts +10 -0
- package/dist/user-action/instrumentation.js.map +1 -1
- package/dist/utils/getElementCSSSelector.js.map +1 -1
- package/dist/web-vitals/instrumentation.js.map +1 -1
- package/package.json +2 -1
- package/dist/navigation/utils.d.ts +0 -12
|
@@ -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;CACD;AAED,MAAM,eAAsD;CAC1D,OAAO,eAAe;CACtB,KAAK,eAAe;CACpB,MAAM,eAAe;CACrB,MAAM,eAAe;CACrB,OAAO,eAAe;CACvB;;;;;AAMD,SAAS,yBAAyB,MAAyB;
|
|
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;CACD;AAED,MAAM,eAAsD;CAC1D,OAAO,eAAe;CACtB,KAAK,eAAe;CACpB,MAAM,eAAe;CACrB,MAAM,eAAe;CACrB,OAAO,eAAe;CACvB;;;;;AAMD,SAAS,yBAAyB,MAAyB;CACzD,OAAO,KACJ,KAAK,QAAQ;EACZ,IAAI,OAAO,QAAQ,YAAY,QAAQ,MACrC,IAAI;GACF,OAAO,KAAK,UAAU,IAAI;UACpB;GAEN,OAAO,OAAO,IAAI;;EAGtB,OAAO,OAAO,IAAI;GAClB,CACD,KAAK,IAAI;;;;;AAMd,IAAa,yBAAb,cAA4C,oBAAkD;CAI5F,YAAY,SAAuC,EAAE,EAAE;EACrD,MAAM,kDAAkD,SAAS,OAAO;;CAG1E,OAA0B;EACxB,OAAO,EAAE;;CAGX,wBAA6D;EAC3D,OAAO,KAAK,QAAQ,qBAAqB;;CAG3C,iBAA0C;EACxC,OAAO,KAAK,QAAQ,cAAc;;CAGpC,oBACE,QAC8D;EAC9D,MAAM,kBAAkB;EAExB,OAAO,SAAS,mBAAmB,UAAkC;GACnE,OAAO,SAAyB,GAAG,MAAiB;IAClD,IACE,gBAAgB,WAChB,gBAAgB,gBAAgB,CAAC,SAAS,OAAO,EACjD;KACA,MAAM,aAAa,QAAQ,QAAQ;KACnC,MAAM,OAAO,gBAAgB,uBAAuB,CAAC,KAAK;KAE1D,gBAAgB,OAAO,KAAK;MAC1B;MACA,WAAW;MACX,gBAAgB,aAAa;MAC7B,cAAc;MACd,SAAS;MACT,YAAY,GACT,sBAAsB,QACxB;MACF,CAAC;;IAGJ,OAAO,SAAS,MAAM,MAAM,KAAK;;;;CAKvC,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,OAAO,CAAC;;CAKnE,UAAyB;EACvB,KAAK,UAAU"}
|
|
@@ -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 error: Error | string | null | undefined =\n 'reason' in event ? event.reason : event.error;\n\n if (error == null) {\n return;\n }\n\n let errorAttributes: AnyValueMap;\n if (typeof error === 'string') {\n errorAttributes = { [ATTR_EXCEPTION_MESSAGE]: error };\n } else {\n errorAttributes = {\n [ATTR_EXCEPTION_TYPE]: error.name,\n [ATTR_EXCEPTION_MESSAGE]: error.message,\n [ATTR_EXCEPTION_STACKTRACE]: error.stack,\n };\n }\n\n const customAttributes = this._applyCustomAttributes(error);\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\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,EAAE,EAAE;
|
|
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 error: Error | string | null | undefined =\n 'reason' in event ? event.reason : event.error;\n\n if (error == null) {\n return;\n }\n\n let errorAttributes: AnyValueMap;\n if (typeof error === 'string') {\n errorAttributes = { [ATTR_EXCEPTION_MESSAGE]: error };\n } else {\n errorAttributes = {\n [ATTR_EXCEPTION_TYPE]: error.name,\n [ATTR_EXCEPTION_MESSAGE]: error.message,\n [ATTR_EXCEPTION_STACKTRACE]: error.stack,\n };\n }\n\n const customAttributes = this._applyCustomAttributes(error);\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\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,EAAE,EAAE;EACpD,MAAM,iDAAiD,SAAS,OAAO;;CAGzE,OAA0B;EACxB,OAAO,EAAE;;CAGX,SAAwB;EACtB,IAAI,KAAK,YACP;EAEF,KAAK,aAAa;EAElB,KAAK,mBAAmB,UAAU,KAAK,SAAS,MAAM;EACtD,OAAO,iBAAiB,SAAS,KAAK,gBAAgB;EACtD,OAAO,iBAAiB,sBAAsB,KAAK,gBAAgB;;CAGrE,UAAyB;EACvB,IAAI,CAAC,KAAK,YACR;EAEF,KAAK,aAAa;EAElB,IAAI,KAAK,iBAAiB;GACxB,OAAO,oBAAoB,SAAS,KAAK,gBAAgB;GACzD,OAAO,oBAAoB,sBAAsB,KAAK,gBAAgB;GACtE,KAAK,kBAAkB,KAAA;;;CAI3B,SAAiB,OAAiD;EAChE,MAAM,QACJ,YAAY,QAAQ,MAAM,SAAS,MAAM;EAE3C,IAAI,SAAS,MACX;EAGF,IAAI;EACJ,IAAI,OAAO,UAAU,UACnB,kBAAkB,GAAG,yBAAyB,OAAO;OAErD,kBAAkB;IACf,sBAAsB,MAAM;IAC5B,yBAAyB,MAAM;IAC/B,4BAA4B,MAAM;GACpC;EAGH,MAAM,mBAAmB,KAAK,uBAAuB,MAAM;EAE3D,MAAM,YAAuB;GAC3B,WAAW;GACX,gBAAgB,eAAe;GAC/B,YAAY;IAAE,GAAG;IAAiB,GAAG;IAAkB;GACxD;EAED,KAAK,OAAO,KAAK,UAAU;;CAG7B,uBAA+B,OAAmC;EAChE,MAAM,OAAO,KAAK,WAAW,CAAC;EAC9B,IAAI,CAAC,MACH,OAAO,EAAE;EAEX,IAAI,SAAqB,EAAE;EAC3B,6BACQ;GACJ,SAAS,KAAK,MAAM;MAErB,QAAQ;GACP,IAAI,KACF,KAAK,MAAM,MAAM,qCAAqC,IAAI;KAG9D,KACD;EACD,OAAO"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import { NavigationInstrumentationConfig, NavigationType } from "./types.js";
|
|
2
2
|
import { NavigationInstrumentation } from "./instrumentation.js";
|
|
3
|
-
|
|
4
|
-
export { NavigationInstrumentation, type NavigationInstrumentationConfig, type NavigationType, defaultSanitizeUrl };
|
|
3
|
+
export { NavigationInstrumentation, type NavigationInstrumentationConfig, type NavigationType };
|
package/dist/navigation/index.js
CHANGED
|
@@ -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 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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"}
|
package/dist/navigation/utils.js
CHANGED
|
@@ -1,50 +1,4 @@
|
|
|
1
1
|
//#region src/navigation/utils.ts
|
|
2
|
-
const SENSITIVE_PARAMS = [
|
|
3
|
-
"password",
|
|
4
|
-
"passwd",
|
|
5
|
-
"secret",
|
|
6
|
-
"api_key",
|
|
7
|
-
"apikey",
|
|
8
|
-
"auth",
|
|
9
|
-
"authorization",
|
|
10
|
-
"token",
|
|
11
|
-
"access_token",
|
|
12
|
-
"refresh_token",
|
|
13
|
-
"jwt",
|
|
14
|
-
"session",
|
|
15
|
-
"sessionid",
|
|
16
|
-
"key",
|
|
17
|
-
"private_key",
|
|
18
|
-
"client_secret",
|
|
19
|
-
"client_id",
|
|
20
|
-
"signature",
|
|
21
|
-
"hash"
|
|
22
|
-
];
|
|
23
|
-
/**
|
|
24
|
-
* Default URL sanitization function that redacts credentials and sensitive query parameters.
|
|
25
|
-
* This is the default implementation used when no custom sanitizeUrl callback is provided.
|
|
26
|
-
*
|
|
27
|
-
* @param url - The URL to sanitize
|
|
28
|
-
* @returns The sanitized URL with credentials and sensitive parameters redacted
|
|
29
|
-
*/
|
|
30
|
-
function defaultSanitizeUrl(url) {
|
|
31
|
-
try {
|
|
32
|
-
const urlObj = new URL(url);
|
|
33
|
-
if (urlObj.username || urlObj.password) {
|
|
34
|
-
urlObj.username = "REDACTED";
|
|
35
|
-
urlObj.password = "REDACTED";
|
|
36
|
-
}
|
|
37
|
-
for (const param of SENSITIVE_PARAMS) if (urlObj.searchParams.has(param)) urlObj.searchParams.set(param, "REDACTED");
|
|
38
|
-
return urlObj.toString();
|
|
39
|
-
} catch {
|
|
40
|
-
let sanitized = url.replace(/\/\/[^:/@]+:[^/@]+@/, "//REDACTED:REDACTED@");
|
|
41
|
-
for (const param of SENSITIVE_PARAMS) {
|
|
42
|
-
const regex = new RegExp(`([?&]${param}(?:%3D|=))[^&]*`, "gi");
|
|
43
|
-
sanitized = sanitized.replace(regex, "$1REDACTED");
|
|
44
|
-
}
|
|
45
|
-
return sanitized;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
2
|
/**
|
|
49
3
|
* Determines if navigation between two URLs represents a hash change.
|
|
50
4
|
* A hash change is true if the URLs are the same except for the hash part,
|
|
@@ -72,6 +26,6 @@ function isHashChange(fromUrl, toUrl) {
|
|
|
72
26
|
}
|
|
73
27
|
}
|
|
74
28
|
//#endregion
|
|
75
|
-
export {
|
|
29
|
+
export { isHashChange };
|
|
76
30
|
|
|
77
31
|
//# sourceMappingURL=utils.js.map
|
|
@@ -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\
|
|
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,OAAO;EAClD,MAAM,IAAI,IAAI,IAAI,OAAO,OAAO,SAAS,OAAO;EAChD,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;SAE5C;EACN,MAAM,WAAW,QAAQ,MAAM,IAAI,CAAC;EACpC,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC;EAChC,MAAM,WAAW,QAAQ,MAAM,IAAI,CAAC,MAAM;EAC1C,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC,MAAM;EAMtC,OAJiB,aAAa,UACH,aAAa,UAChB,WAAW"}
|
|
@@ -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 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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"}
|
package/dist/package.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idle-callback-shim.js","names":[],"sources":["../../src/resource-timing/idle-callback-shim.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst IDLE_DEADLINE_MS = 50;\n\n// requestIdleCallback is not yet Baseline (not supported in Safari).\n// Feature-detect here so we can shim it below for unsupported browsers.\n// eslint-disable-next-line baseline-js/use-baseline\nconst supportsIdleCallback = typeof window.requestIdleCallback === 'function';\n\nexport interface IdleCallbackHandle {\n id: number;\n native: boolean;\n}\n\n/**\n * Schedules a callback during idle time, using the native requestIdleCallback\n * when available. Falls back to setTimeout with a synthetic IdleDeadline that\n * reports ~50ms of available time (per the W3C spec recommendation).\n */\nexport function requestIdleCallbackShim(\n callback: IdleRequestCallback,\n options?: IdleRequestOptions,\n): IdleCallbackHandle {\n if (supportsIdleCallback) {\n // requestIdleCallback is not yet Baseline (not supported in Safari).\n // eslint-disable-next-line baseline-js/use-baseline\n const id = window.requestIdleCallback(callback, options);\n return { id, native: true };\n }\n\n const id = window.setTimeout(() => {\n const start = performance.now();\n callback({\n didTimeout: false,\n timeRemaining: () =>\n Math.max(0, IDLE_DEADLINE_MS - (performance.now() - start)),\n });\n }, 1);\n return { id, native: false };\n}\n\n/**\n * Cancels a previously scheduled idle callback.\n */\nexport function cancelIdleCallbackShim(handle: IdleCallbackHandle): void {\n if (handle.native) {\n // eslint-disable-next-line baseline-js/use-baseline\n window.cancelIdleCallback(handle.id);\n } else {\n clearTimeout(handle.id);\n }\n}\n"],"mappings":";AAKA,MAAM,mBAAmB;AAKzB,MAAM,uBAAuB,OAAO,OAAO,wBAAwB;;;;;;AAYnE,SAAgB,wBACd,UACA,SACoB;
|
|
1
|
+
{"version":3,"file":"idle-callback-shim.js","names":[],"sources":["../../src/resource-timing/idle-callback-shim.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst IDLE_DEADLINE_MS = 50;\n\n// requestIdleCallback is not yet Baseline (not supported in Safari).\n// Feature-detect here so we can shim it below for unsupported browsers.\n// eslint-disable-next-line baseline-js/use-baseline\nconst supportsIdleCallback = typeof window.requestIdleCallback === 'function';\n\nexport interface IdleCallbackHandle {\n id: number;\n native: boolean;\n}\n\n/**\n * Schedules a callback during idle time, using the native requestIdleCallback\n * when available. Falls back to setTimeout with a synthetic IdleDeadline that\n * reports ~50ms of available time (per the W3C spec recommendation).\n */\nexport function requestIdleCallbackShim(\n callback: IdleRequestCallback,\n options?: IdleRequestOptions,\n): IdleCallbackHandle {\n if (supportsIdleCallback) {\n // requestIdleCallback is not yet Baseline (not supported in Safari).\n // eslint-disable-next-line baseline-js/use-baseline\n const id = window.requestIdleCallback(callback, options);\n return { id, native: true };\n }\n\n const id = window.setTimeout(() => {\n const start = performance.now();\n callback({\n didTimeout: false,\n timeRemaining: () =>\n Math.max(0, IDLE_DEADLINE_MS - (performance.now() - start)),\n });\n }, 1);\n return { id, native: false };\n}\n\n/**\n * Cancels a previously scheduled idle callback.\n */\nexport function cancelIdleCallbackShim(handle: IdleCallbackHandle): void {\n if (handle.native) {\n // eslint-disable-next-line baseline-js/use-baseline\n window.cancelIdleCallback(handle.id);\n } else {\n clearTimeout(handle.id);\n }\n}\n"],"mappings":";AAKA,MAAM,mBAAmB;AAKzB,MAAM,uBAAuB,OAAO,OAAO,wBAAwB;;;;;;AAYnE,SAAgB,wBACd,UACA,SACoB;CACpB,IAAI,sBAIF,OAAO;EAAE,IADE,OAAO,oBAAoB,UAAU,QACrC;EAAE,QAAQ;EAAM;CAW7B,OAAO;EAAE,IARE,OAAO,iBAAiB;GACjC,MAAM,QAAQ,YAAY,KAAK;GAC/B,SAAS;IACP,YAAY;IACZ,qBACE,KAAK,IAAI,GAAG,oBAAoB,YAAY,KAAK,GAAG,OAAO;IAC9D,CAAC;KACD,EACQ;EAAE,QAAQ;EAAO;;;;;AAM9B,SAAgB,uBAAuB,QAAkC;CACvE,IAAI,OAAO,QAET,OAAO,mBAAmB,OAAO,GAAG;MAEpC,aAAa,OAAO,GAAG"}
|
|
@@ -3,6 +3,7 @@ import { cancelIdleCallbackShim, requestIdleCallbackShim } from "./idle-callback
|
|
|
3
3
|
import { ATTR_RESOURCE_CONNECT_END, ATTR_RESOURCE_CONNECT_START, ATTR_RESOURCE_DECODED_BODY_SIZE, ATTR_RESOURCE_DOMAIN_LOOKUP_END, ATTR_RESOURCE_DOMAIN_LOOKUP_START, ATTR_RESOURCE_DURATION, ATTR_RESOURCE_ENCODED_BODY_SIZE, ATTR_RESOURCE_FETCH_START, ATTR_RESOURCE_INITIATOR_TYPE, ATTR_RESOURCE_NEXT_HOP_PROTOCOL, ATTR_RESOURCE_REDIRECT_END, ATTR_RESOURCE_REDIRECT_START, ATTR_RESOURCE_RENDER_BLOCKING_STATUS, ATTR_RESOURCE_REQUEST_START, ATTR_RESOURCE_RESPONSE_END, ATTR_RESOURCE_RESPONSE_START, ATTR_RESOURCE_SECURE_CONNECTION_START, ATTR_RESOURCE_TRANSFER_SIZE, ATTR_RESOURCE_URL, ATTR_RESOURCE_WORKER_START, RESOURCE_TIMING_EVENT_NAME } from "./semconv.js";
|
|
4
4
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
5
5
|
import { InstrumentationBase } from "@opentelemetry/instrumentation";
|
|
6
|
+
import { isUrlIgnored } from "@opentelemetry/core";
|
|
6
7
|
//#region src/resource-timing/instrumentation.ts
|
|
7
8
|
const DEFAULT_BATCH_SIZE = 50;
|
|
8
9
|
const DEFAULT_FORCE_PROCESSING_AFTER = 1e3;
|
|
@@ -63,26 +64,35 @@ var ResourceTimingInstrumentation = class extends InstrumentationBase {
|
|
|
63
64
|
}
|
|
64
65
|
_setupObserver() {
|
|
65
66
|
if (!this._isEnabled) return;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
67
|
+
const observer = new PerformanceObserver((list) => {
|
|
68
|
+
if (!this._isEnabled) return;
|
|
69
|
+
const maxQueueSize = Math.max(this._config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, MIN_QUEUE_SIZE);
|
|
70
|
+
const entries = list.getEntries();
|
|
71
|
+
const initiatorTypes = this._config.initiatorTypes;
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (initiatorTypes !== void 0 && !initiatorTypes.includes(entry.initiatorType)) continue;
|
|
74
|
+
let ignored;
|
|
75
|
+
try {
|
|
76
|
+
ignored = isUrlIgnored(entry.name, this._config.ignoreUrls);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
this._diag.error("Failed to check ignoreUrls for resource entry", e);
|
|
79
|
+
continue;
|
|
76
80
|
}
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
if (ignored) continue;
|
|
82
|
+
if (this._pendingEntries.length >= maxQueueSize) this._flush();
|
|
83
|
+
this._pendingEntries.push(entry);
|
|
84
|
+
}
|
|
85
|
+
if (this._pendingEntries.length > 0) this._scheduleProcessing();
|
|
86
|
+
});
|
|
87
|
+
this._observer = observer;
|
|
88
|
+
try {
|
|
80
89
|
observer.observe({
|
|
81
90
|
type: "resource",
|
|
82
91
|
buffered: true
|
|
83
92
|
});
|
|
84
|
-
} catch {
|
|
85
|
-
this._diag.
|
|
93
|
+
} catch (e) {
|
|
94
|
+
this._diag.error("Failed to start resource PerformanceObserver", e);
|
|
95
|
+
this._observer = void 0;
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
98
|
_scheduleProcessing() {
|
|
@@ -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 { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { version } from '../../package.json' with { type: 'json' };\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 try {\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 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 observer.observe({ type: 'resource', buffered: true });\n } catch {\n this._diag.warn(\n 'PerformanceObserver not supported, resource timings will not be collected',\n );\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 this.logger.emit({\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 } 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 { SeverityNumber } from '@opentelemetry/api-logs';\nimport { isUrlIgnored } from '@opentelemetry/core';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { version } from '../../package.json' with { type: 'json' };\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 let ignored: boolean;\n try {\n ignored = isUrlIgnored(entry.name, this._config.ignoreUrls);\n } catch (e) {\n this._diag.error('Failed to check ignoreUrls for resource entry', e);\n continue;\n }\n if (ignored) {\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 this.logger.emit({\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 } 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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"}
|
|
@@ -32,6 +32,16 @@ interface ResourceTimingInstrumentationConfig extends InstrumentationConfig {
|
|
|
32
32
|
* are captured. When unset, all resource entries are captured.
|
|
33
33
|
*/
|
|
34
34
|
initiatorTypes?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* URLs to ignore. Entries whose URL matches any of the patterns will not be
|
|
37
|
+
* captured. Strings are compared with strict equality — matching is
|
|
38
|
+
* case-sensitive and the URL is not normalized, so a trailing slash, query
|
|
39
|
+
* string, or different casing will not match. Prefer RegExps for robust
|
|
40
|
+
* matching (e.g. `[/\/v1\/traces$/, /\/v1\/logs$/]`).
|
|
41
|
+
* Avoid RegExps with the `y` flag — its stateful `lastIndex` causes
|
|
42
|
+
* alternating match results across repeated calls.
|
|
43
|
+
*/
|
|
44
|
+
ignoreUrls?: (string | RegExp)[];
|
|
35
45
|
}
|
|
36
46
|
//#endregion
|
|
37
47
|
export { ResourceTimingInstrumentationConfig };
|
|
@@ -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 */\n\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } 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 this.logger.emit({\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\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":";;;;;;AAwBA,MAAM,gCAA0D,CAAC,QAAQ;AACzE,MAAM,gCAAgC;;;;AAKtC,IAAa,4BAAb,cAA+C,oBAAqD;CAGlG,YAAY,SAA0C,EAAE,EAAE;
|
|
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 */\n\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } 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 this.logger.emit({\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\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":";;;;;;AAwBA,MAAM,gCAA0D,CAAC,QAAQ;AACzE,MAAM,gCAAgC;;;;AAKtC,IAAa,4BAAb,cAA+C,oBAAqD;CAGlG,YAAY,SAA0C,EAAE,EAAE;EACxD,MACE,sDACA,SACA,OACD;;CAGH,OAA0B;EACxB,OAAO,EAAE;;CAGX,8BAAsC,OAAgC;EACpE,QAAQ,MAAM,QAAd;GACE,KAAK,GACH,OAAO;GACT,KAAK,GACH,OAAO;GACT,KAAK,GACH,OAAO;GACT,SACE,OAAO;;;CAIb,QAAgB,OAAmB;EACjC,MAAM,UAAU,MAAM;EAEtB,IAAI,EAAE,mBAAmB,cACvB;EAGF,IAAI,QAAQ,aAAa,WAAW,EAClC;EAGF,MAAM,cAAc,sBAAsB,SAAS;GACjD,uBAAuB;GACvB,mBAAmB;GACpB,CAAC;EACF,MAAM,yBAAiD,EAAE;EAGzD,KAAK,MAAM,QAAQ,QAAQ,YACzB,IAAI,KAAK,KAAK,WAAW,8BAA8B,EACrD,uBACE,KAAK,KAAK,MAAM,GAAqC,IACnD,KAAK;EAIb,KAAK,OAAO,KAAK;GACf,gBAAgB,eAAe;GAC/B,WAAW;GACX,YAAY;KACT,cAAc,MAAM;KACpB,cAAc,MAAM;KACpB,gBAAgB,QAAQ;KACxB,YAAY;KACZ,0BAA0B,KAAK,8BAA8B,MAAM;KACnE,oBAAoB;IACtB;GACF,CAAC;;CAGJ,SAAwB;EACtB,MAAM,sBACJ,KAAK,QAAQ,uBAAuB;EACtC,IAAI,CAAC,KAAK,iBACR,KAAK,kBAAkB,KAAK,QAAQ,KAAK,KAAK;EAGhD,IAAI,oBAAoB,SAAS,QAAQ,EACvC,SAAS,iBAAiB,SAAS,KAAK,iBAAiB,KAAK;;CAIlE,UAAyB;EACvB,IAAI,KAAK,iBACP,SAAS,oBAAoB,SAAS,KAAK,iBAAiB,KAAK"}
|
|
@@ -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,EAAE,KACzB;
|
|
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,EAAE,KACzB;CAEX,IAAI,QAAQ,aAAa,KAAK,eAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,YAAY,gBAChB,aACA,yBAAyB,kBAC1B;CAGD,IAAI,UAAU,WAAW,IAAI,EAC3B,OAAO;CAGT,MAAM,SAAS,YAAY;CAC3B,MAAM,iBAAiB,SACnB,sBAAsB,QAAQ;EAC5B;EACA,uBAAuB;EACxB,CAAC,GACF;CAEJ,OAAO,iBAAiB,GAAG,eAAe,KAAK,cAAc;;AAG/D,MAAM,mBAAmB,SAAe,eAAe,UAAkB;CACvE,IAAI,QAAQ,aAAa,KAAK,cAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,KAAK,YAAY,aAAa,KAAK;CAGzC,IAAI,gBAAgB;MAES,YAAY,cAAc,iBACnD,IAAI,IAAI,OAAO,GAAG,GAGE,CAAC,WAAW,GAChC,OAAO,IAAI,IAAI,OAAO,GAAG;;CAI7B,IAAI,WAAW,qBAAqB,YAAY;CAEhD,MAAM,QAAQ,YAAY,YAAY;CAEtC,IAAI,QAAQ,GACV,YAAY,cAAc,MAAM;CAGlC,OAAO;;AAGT,MAAM,eAAe,YAAiC;CAEpD,IAAI,CAAC,QAAQ,eACX,OAAO;CAGT,MAAM,WAAW,qBAAqB,QAAQ;CAG9C,MAAM,WAAW,MAAM,KAAK,QAAQ,cAAc,SAAS,CAAC,QACzD,YAAY,qBAAqB,QAAQ,KAAK,SAChD;CAGD,IAAI,SAAS,SAAS,GACpB,OAAO,SAAS,QAAQ,QAAQ,GAAG;CAGrC,OAAO;;AAGT,MAAM,wBAAwB,YAC5B,QAAQ,aACP,QAAQ,UAAU,SAAS,IACxB,MAAM,KAAK,QAAQ,UAAU,CAC1B,KAAK,QAAQ,IAAI,IAAI,OAAO,IAAI,GAAG,CACnC,KAAK,GAAG,GACX"}
|
|
@@ -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,EAAE,EAAE;
|
|
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,EAAE,EAAE;EACvD,MAAM,qDAAqD,SAAS,OAAO;EAC3E,KAAK,4BAA4B,OAAO;EACxC,KAAK,yBAAyB,OAAO,yBAAyB;;CAGhE,OAA0B;EACxB,OAAO,EAAE;;;;;;CAOX,SAAwB;EACtB,IAAI,OAAO,wBAAwB,aAAa;GAC9C,KAAK,MAAM,MACT,sEACD;GACD;;EAGF,KAAK,aAAa;EAElB,IAAI,KAAK,sBAAsB;GAC7B,KAAK,MAAM,MAAM,kDAAkD;GACnE;;EAGF,KAAK,uBAAuB;EAC5B,KAAK,MAAM,MAAM,wBAAwB;EAGzC,OAAO,WAAW,KAAK,cAAc,OAAO,CAAC;EAC7C,OAAO,WAAW,KAAK,cAAc,OAAO,CAAC;EAC7C,OAAO,WAAW,KAAK,cAAc,OAAO,CAAC;EAC7C,OAAO,WAAW,KAAK,cAAc,OAAO,CAAC;EAC7C,QAAQ,WAAW,KAAK,cAAc,OAAO,CAAC;;;;;;CAOhD,UAAyB;EACvB,KAAK,aAAa;EAClB,KAAK,MAAM,MAAM,6CAA6C;;;;;;;CAQhE,uBACE,QACoB;EACpB,IAAI,OAAO,SAAS,OAAO;GACzB,MAAM,EAAE,gBAAgB;GACxB,IAAI,YAAY,qBAAqB,KAAA,GACnC,OAAO,YAAY;GAErB;;EAEF,IAAI,OAAO,SAAS,OAAO;GACzB,MAAM,EAAE,gBAAgB;GACxB,OAAO,YAAY;;EAGrB,OAAO,OAAO;;CAGhB,cAAsB,QAAqC;EACzD,IAAI,CAAC,KAAK,YACR;EAEF,MAAM,aAAyB;IAC5B,sBAAsB,OAAO,KAAK,aAAa;IAC/C,uBAAuB,OAAO;IAE9B,uBAAuB,OAAO;IAC9B,wBAAwB,OAAO;IAC/B,oBAAoB,OAAO;IAC3B,iCAAiC,OAAO;GAC1C;EAED,MAAM,YAAY,KAAK,uBAAuB,OAAO;EAErD,MAAM,YAAuB;GAC3B,WAAW;GACX,gBAAgB,eAAe;GAC/B;GACA,GAAI,KAAK,yBACL,EAAE,MAAM,KAAK,UAAU,OAAO,YAAY,EAAE,GAC5C,EAAE;GACN,GAAI,cAAc,KAAA,IAAY,EAAE,WAAW,GAAG,EAAE;GACjD;EAED,IAAI,KAAK,2BACP,6BACQ,KAAK,4BAA4B,UAAU,GAChD,UAAU;GACT,IAAI,OACF,KAAK,MAAM,MAAM,wCAAwC,MAAM;KAGnE,KACD;EAGH,KAAK,OAAO,KAAK,UAAU"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentelemetry/browser-instrumentation",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "OpenTelemetry browser instrumentations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opentelemetry",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@opentelemetry/api-logs": "^0.217.0",
|
|
54
|
+
"@opentelemetry/core": "^2.7.1",
|
|
54
55
|
"@opentelemetry/instrumentation": "^0.217.0",
|
|
55
56
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
56
57
|
"web-vitals": "^5.2.0"
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
//#region src/navigation/utils.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Default URL sanitization function that redacts credentials and sensitive query parameters.
|
|
4
|
-
* This is the default implementation used when no custom sanitizeUrl callback is provided.
|
|
5
|
-
*
|
|
6
|
-
* @param url - The URL to sanitize
|
|
7
|
-
* @returns The sanitized URL with credentials and sensitive parameters redacted
|
|
8
|
-
*/
|
|
9
|
-
declare function defaultSanitizeUrl(url: string): string;
|
|
10
|
-
//#endregion
|
|
11
|
-
export { defaultSanitizeUrl };
|
|
12
|
-
//# sourceMappingURL=utils.d.ts.map
|