@opentelemetry/browser-instrumentation 0.3.0 → 0.4.0

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/README.md CHANGED
@@ -13,6 +13,7 @@ npm install @opentelemetry/browser-instrumentation
13
13
 
14
14
  ## Instrumentations
15
15
 
16
+ - [Navigation](#navigation) — automatic instrumentation for browser navigations (initial load and SPA route changes)
16
17
  - [Navigation Timing](#navigation-timing) — automatic instrumentation for navigation timing
17
18
  - [Resource Timing](#resource-timing) — automatic instrumentation for resource timing
18
19
  - [User Action](#user-action) — automatic instrumentation for user actions (clicks)
@@ -28,6 +29,7 @@ import {
28
29
  SimpleLogRecordProcessor,
29
30
  } from '@opentelemetry/sdk-logs';
30
31
  import { registerInstrumentations } from '@opentelemetry/instrumentation';
32
+ import { NavigationInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation';
31
33
  import { NavigationTimingInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation-timing';
32
34
  import { ResourceTimingInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/resource-timing';
33
35
  import { UserActionInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/user-action';
@@ -42,6 +44,7 @@ logs.setGlobalLoggerProvider(logProvider);
42
44
 
43
45
  registerInstrumentations({
44
46
  instrumentations: [
47
+ new NavigationInstrumentation(),
45
48
  new NavigationTimingInstrumentation(),
46
49
  new ResourceTimingInstrumentation(),
47
50
  new UserActionInstrumentation(),
@@ -52,6 +55,62 @@ registerInstrumentations({
52
55
 
53
56
  ---
54
57
 
58
+ ### Navigation
59
+
60
+ ```typescript
61
+ import { NavigationInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation';
62
+ ```
63
+
64
+ Emits a `browser.navigation` event for the initial page load (hard navigation) and for subsequent in-page navigations (soft navigations), including `history.pushState`, `history.replaceState`, `popstate`, and hash changes. When enabled via config, the [Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API) is used in preference to patching `history`.
65
+
66
+ #### Configuration
67
+
68
+ ```typescript
69
+ import {
70
+ NavigationInstrumentation,
71
+ defaultSanitizeUrl,
72
+ } from '@opentelemetry/browser-instrumentation/experimental/navigation';
73
+
74
+ new NavigationInstrumentation({
75
+ // Use window.navigation (Navigation API) when available instead of
76
+ // patching history.pushState / history.replaceState. Default: false.
77
+ useNavigationApiIfAvailable: true,
78
+
79
+ // Rewrite the captured URL before it is emitted. Useful for stripping
80
+ // path segments, query parameters, or tokens that should not be exported.
81
+ sanitizeUrl: (url) => defaultSanitizeUrl(url),
82
+
83
+ // Mutate the log record before it is emitted (e.g. attach custom attributes).
84
+ applyCustomLogRecordData: (logRecord) => {
85
+ logRecord.attributes = {
86
+ ...logRecord.attributes,
87
+ 'app.route.id': '...',
88
+ };
89
+ },
90
+ });
91
+ ```
92
+
93
+ | Option | Type | Default | Description |
94
+ |--------|------|---------|-------------|
95
+ | `useNavigationApiIfAvailable` | `boolean` | `false` | When `true`, subscribes to the Navigation API (`currententrychange`) instead of patching `history.pushState` / `history.replaceState`. Falls back to history patching when the Navigation API is unavailable. |
96
+ | `sanitizeUrl` | `(url: string) => string` | — | Called before the URL is written to `url.full`. |
97
+ | `applyCustomLogRecordData` | `(logRecord: LogRecord) => void` | — | Hook to modify log records before they are emitted. Errors thrown from this hook are caught and logged via the instrumentation diag logger. |
98
+
99
+ `defaultSanitizeUrl` is exported for composition — it redacts `user:password@` credentials and a set of common sensitive query parameters (`api_key`, `token`, `password`, etc.).
100
+
101
+ #### Captured Attributes
102
+
103
+ Each `browser.navigation` event includes:
104
+
105
+ | Attribute | Description |
106
+ |-----------|-------------|
107
+ | `url.full` | The destination URL (after `sanitizeUrl` if configured). |
108
+ | `browser.navigation.same_document` | `true` for SPA route changes; `false` for full-page loads. |
109
+ | `browser.navigation.hash_change` | `true` when the navigation only adds or changes the URL fragment. |
110
+ | `browser.navigation.type` | One of `push`, `replace`, `reload`, `traverse` (omitted for the initial hard navigation). |
111
+
112
+ ---
113
+
55
114
  ### Navigation Timing
56
115
 
57
116
  ```typescript
@@ -0,0 +1,3 @@
1
+ import { ConsoleInstrumentationConfig, ConsoleMethod } from "./types.js";
2
+ import { ConsoleInstrumentation } from "./instrumentation.js";
3
+ export { ConsoleInstrumentation, type ConsoleInstrumentationConfig, type ConsoleMethod };
@@ -0,0 +1,2 @@
1
+ import { ConsoleInstrumentation } from "./instrumentation.js";
2
+ export { ConsoleInstrumentation };
@@ -0,0 +1,21 @@
1
+ import { ConsoleInstrumentationConfig } from "./types.js";
2
+ import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
+
4
+ //#region src/console/instrumentation.d.ts
5
+ /**
6
+ * OpenTelemetry instrumentation that captures console calls and emits them as OpenTelemetry logs.
7
+ */
8
+ declare class ConsoleInstrumentation extends InstrumentationBase<ConsoleInstrumentationConfig> {
9
+ private _isPatched;
10
+ private _active;
11
+ constructor(config?: ConsoleInstrumentationConfig);
12
+ protected init(): never[];
13
+ private _getMessageSerializer;
14
+ private _getLogMethods;
15
+ private _patchConsoleMethod;
16
+ enable(): void;
17
+ disable(): void;
18
+ }
19
+ //#endregion
20
+ export { ConsoleInstrumentation };
21
+ //# sourceMappingURL=instrumentation.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { version } from "../package.js";
2
+ import { ATTR_CONSOLE_METHOD, CONSOLE_LOG_EVENT_NAME } from "./semconv.js";
3
+ import { context } from "@opentelemetry/api";
4
+ import { SeverityNumber } from "@opentelemetry/api-logs";
5
+ import { InstrumentationBase } from "@opentelemetry/instrumentation";
6
+ //#region src/console/instrumentation.ts
7
+ const DEFAULT_LOG_METHODS = [
8
+ "log",
9
+ "warn",
10
+ "error",
11
+ "info",
12
+ "debug"
13
+ ];
14
+ const SEVERITY_MAP = {
15
+ debug: SeverityNumber.DEBUG,
16
+ log: SeverityNumber.INFO,
17
+ info: SeverityNumber.INFO,
18
+ warn: SeverityNumber.WARN,
19
+ error: SeverityNumber.ERROR
20
+ };
21
+ /**
22
+ * Default serializer for console arguments.
23
+ * Joins arguments as strings.
24
+ */
25
+ function defaultMessageSerializer(args) {
26
+ return args.map((arg) => {
27
+ if (typeof arg === "object" && arg !== null) try {
28
+ return JSON.stringify(arg);
29
+ } catch {
30
+ return String(arg);
31
+ }
32
+ return String(arg);
33
+ }).join(" ");
34
+ }
35
+ /**
36
+ * OpenTelemetry instrumentation that captures console calls and emits them as OpenTelemetry logs.
37
+ */
38
+ var ConsoleInstrumentation = class extends InstrumentationBase {
39
+ constructor(config = {}) {
40
+ super("@opentelemetry/browser-instrumentation/console", version, config);
41
+ }
42
+ init() {
43
+ return [];
44
+ }
45
+ _getMessageSerializer() {
46
+ return this._config.messageSerializer ?? defaultMessageSerializer;
47
+ }
48
+ _getLogMethods() {
49
+ return this._config.logMethods ?? DEFAULT_LOG_METHODS;
50
+ }
51
+ _patchConsoleMethod(method) {
52
+ const instrumentation = this;
53
+ return function patchConsoleMethod(original) {
54
+ return function(...args) {
55
+ if (instrumentation._active && instrumentation._getLogMethods().includes(method)) {
56
+ const logContext = context.active();
57
+ const body = instrumentation._getMessageSerializer()(args);
58
+ instrumentation.logger.emit({
59
+ body,
60
+ eventName: CONSOLE_LOG_EVENT_NAME,
61
+ severityNumber: SEVERITY_MAP[method],
62
+ severityText: method,
63
+ context: logContext,
64
+ attributes: { [ATTR_CONSOLE_METHOD]: method }
65
+ });
66
+ }
67
+ return original.apply(this, args);
68
+ };
69
+ };
70
+ }
71
+ enable() {
72
+ this._active = true;
73
+ if (this._isPatched) return;
74
+ this._isPatched = true;
75
+ for (const method of DEFAULT_LOG_METHODS) if (typeof console[method] === "function") this._wrap(console, method, this._patchConsoleMethod(method));
76
+ }
77
+ disable() {
78
+ this._active = false;
79
+ }
80
+ };
81
+ //#endregion
82
+ export { ConsoleInstrumentation };
83
+
84
+ //# sourceMappingURL=instrumentation.js.map
@@ -0,0 +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;AACzD,QAAO,KACJ,KAAK,QAAQ;AACZ,MAAI,OAAO,QAAQ,YAAY,QAAQ,KACrC,KAAI;AACF,UAAO,KAAK,UAAU,IAAI;UACpB;AAEN,UAAO,OAAO,IAAI;;AAGtB,SAAO,OAAO,IAAI;GAClB,CACD,KAAK,IAAI;;;;;AAMd,IAAa,yBAAb,cAA4C,oBAAkD;CAI5F,YAAY,SAAuC,EAAE,EAAE;AACrD,QAAM,kDAAkD,SAAS,OAAO;;CAG1E,OAA0B;AACxB,SAAO,EAAE;;CAGX,wBAA6D;AAC3D,SAAO,KAAK,QAAQ,qBAAqB;;CAG3C,iBAA0C;AACxC,SAAO,KAAK,QAAQ,cAAc;;CAGpC,oBACE,QAC8D;EAC9D,MAAM,kBAAkB;AAExB,SAAO,SAAS,mBAAmB,UAAkC;AACnE,UAAO,SAAyB,GAAG,MAAiB;AAClD,QACE,gBAAgB,WAChB,gBAAgB,gBAAgB,CAAC,SAAS,OAAO,EACjD;KACA,MAAM,aAAa,QAAQ,QAAQ;KACnC,MAAM,OAAO,gBAAgB,uBAAuB,CAAC,KAAK;AAE1D,qBAAgB,OAAO,KAAK;MAC1B;MACA,WAAW;MACX,gBAAgB,aAAa;MAC7B,cAAc;MACd,SAAS;MACT,YAAY,GACT,sBAAsB,QACxB;MACF,CAAC;;AAGJ,WAAO,SAAS,MAAM,MAAM,KAAK;;;;CAKvC,SAAwB;AACtB,OAAK,UAAU;AACf,MAAI,KAAK,WACP;AAEF,OAAK,aAAa;AAClB,OAAK,MAAM,UAAU,oBACnB,KAAI,OAAO,QAAQ,YAAY,WAC7B,MAAK,MAAM,SAAS,QAAQ,KAAK,oBAAoB,OAAO,CAAC;;CAKnE,UAAyB;AACvB,OAAK,UAAU"}
@@ -0,0 +1,14 @@
1
+ //#region src/console/semconv.ts
2
+ /**
3
+ * Event name for console log events.
4
+ */
5
+ const CONSOLE_LOG_EVENT_NAME = "browser.console";
6
+ /**
7
+ * The console method that was called (e.g., 'log', 'warn', 'error', 'info', 'debug').
8
+ * @example 'error'
9
+ */
10
+ const ATTR_CONSOLE_METHOD = "browser.console.method";
11
+ //#endregion
12
+ export { ATTR_CONSOLE_METHOD, CONSOLE_LOG_EVENT_NAME };
13
+
14
+ //# sourceMappingURL=semconv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semconv.js","names":[],"sources":["../../src/console/semconv.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/*\n * This file contains a copy of unstable semantic convention definitions\n * used by this package.\n * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv\n */\n\n/**\n * Event name for console log events.\n */\nexport const CONSOLE_LOG_EVENT_NAME = 'browser.console';\n\n/**\n * The console method that was called (e.g., 'log', 'warn', 'error', 'info', 'debug').\n * @example 'error'\n */\nexport const ATTR_CONSOLE_METHOD = 'browser.console.method';\n"],"mappings":";;;;AAcA,MAAa,yBAAyB;;;;;AAMtC,MAAa,sBAAsB"}
@@ -0,0 +1,25 @@
1
+ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
2
+
3
+ //#region src/console/types.d.ts
4
+ /**
5
+ * Console methods that can be instrumented.
6
+ */
7
+ type ConsoleMethod = 'log' | 'warn' | 'error' | 'info' | 'debug';
8
+ /**
9
+ * ConsoleInstrumentation Configuration
10
+ */
11
+ interface ConsoleInstrumentationConfig extends InstrumentationConfig {
12
+ /**
13
+ * Console methods to instrument.
14
+ * @default ['log', 'warn', 'error', 'info', 'debug']
15
+ */
16
+ logMethods?: ConsoleMethod[];
17
+ /**
18
+ * Custom serializer for console arguments.
19
+ * @default Joins args as strings
20
+ */
21
+ messageSerializer?: (args: unknown[]) => string;
22
+ }
23
+ //#endregion
24
+ export { ConsoleInstrumentationConfig, ConsoleMethod };
25
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { NavigationInstrumentationConfig, NavigationType } from "./types.js";
2
+ import { NavigationInstrumentation } from "./instrumentation.js";
3
+ import { defaultSanitizeUrl } from "./utils.js";
4
+ export { NavigationInstrumentation, type NavigationInstrumentationConfig, type NavigationType, defaultSanitizeUrl };
@@ -0,0 +1,3 @@
1
+ import { defaultSanitizeUrl } from "./utils.js";
2
+ import { NavigationInstrumentation } from "./instrumentation.js";
3
+ export { NavigationInstrumentation, defaultSanitizeUrl };
@@ -0,0 +1,29 @@
1
+ import { NavigationInstrumentationConfig } from "./types.js";
2
+ import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
+
4
+ //#region src/navigation/instrumentation.d.ts
5
+ declare class NavigationInstrumentation extends InstrumentationBase<NavigationInstrumentationConfig> {
6
+ private _isEnabled;
7
+ private _isHistoryPatched;
8
+ private _hasProcessedInitialLoad;
9
+ private _lastUrl;
10
+ private _onDOMContentLoaded?;
11
+ private _onPopState?;
12
+ private _onCurrentEntryChange?;
13
+ constructor(config?: NavigationInstrumentationConfig);
14
+ protected init(): never[];
15
+ enable(): void;
16
+ disable(): void;
17
+ private _getNavigationApi;
18
+ private _onHardNavigation;
19
+ private _onSoftNavigation;
20
+ private _waitForPageLoad;
21
+ private _patchHistoryApi;
22
+ private _patchHistoryMethod;
23
+ private _applyCustomLogRecordData;
24
+ private _determineSameDocument;
25
+ private _mapChangeStateToType;
26
+ }
27
+ //#endregion
28
+ export { NavigationInstrumentation };
29
+ //# sourceMappingURL=instrumentation.d.ts.map
@@ -0,0 +1,156 @@
1
+ import { version } from "../package.js";
2
+ import { ATTR_BROWSER_NAVIGATION_HASH_CHANGE, ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT, ATTR_BROWSER_NAVIGATION_TYPE, ATTR_URL_FULL, BROWSER_NAVIGATION_EVENT_NAME } from "./semconv.js";
3
+ import { isHashChange } from "./utils.js";
4
+ import { SeverityNumber } from "@opentelemetry/api-logs";
5
+ import { InstrumentationBase, safeExecuteInTheMiddle } from "@opentelemetry/instrumentation";
6
+ //#region src/navigation/instrumentation.ts
7
+ var NavigationInstrumentation = class extends InstrumentationBase {
8
+ constructor(config = {}) {
9
+ super("@opentelemetry/browser-instrumentation/navigation", version, config);
10
+ this._lastUrl = location.href;
11
+ }
12
+ init() {
13
+ return [];
14
+ }
15
+ enable() {
16
+ if (this._isEnabled) return;
17
+ this._isEnabled = true;
18
+ const navigationApi = this._getNavigationApi();
19
+ if (!navigationApi && !this._isHistoryPatched) {
20
+ this._patchHistoryApi();
21
+ this._isHistoryPatched = true;
22
+ }
23
+ this._waitForPageLoad();
24
+ if (navigationApi) {
25
+ this._onCurrentEntryChange = (event) => {
26
+ this._onSoftNavigation("currententrychange", event);
27
+ };
28
+ navigationApi.addEventListener("currententrychange", this._onCurrentEntryChange);
29
+ } else {
30
+ this._onPopState = () => {
31
+ this._onSoftNavigation("popstate");
32
+ };
33
+ window.addEventListener("popstate", this._onPopState);
34
+ }
35
+ }
36
+ disable() {
37
+ if (!this._isEnabled) return;
38
+ this._isEnabled = false;
39
+ if (this._onDOMContentLoaded) {
40
+ document.removeEventListener("DOMContentLoaded", this._onDOMContentLoaded);
41
+ this._onDOMContentLoaded = void 0;
42
+ }
43
+ if (this._onPopState) {
44
+ window.removeEventListener("popstate", this._onPopState);
45
+ this._onPopState = void 0;
46
+ }
47
+ if (this._onCurrentEntryChange) {
48
+ this._getNavigationApi()?.removeEventListener("currententrychange", this._onCurrentEntryChange);
49
+ this._onCurrentEntryChange = void 0;
50
+ }
51
+ this._hasProcessedInitialLoad = false;
52
+ }
53
+ _getNavigationApi() {
54
+ if (!this.getConfig().useNavigationApiIfAvailable) return;
55
+ return window.navigation;
56
+ }
57
+ _onHardNavigation() {
58
+ const cfg = this.getConfig();
59
+ const logRecord = {
60
+ eventName: BROWSER_NAVIGATION_EVENT_NAME,
61
+ severityNumber: SeverityNumber.INFO,
62
+ attributes: {
63
+ [ATTR_URL_FULL]: cfg.sanitizeUrl ? cfg.sanitizeUrl(document.documentURI) : document.documentURI,
64
+ [ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: false,
65
+ [ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: false
66
+ }
67
+ };
68
+ this._applyCustomLogRecordData(logRecord);
69
+ this.logger.emit(logRecord);
70
+ }
71
+ _onSoftNavigation(changeState, navigationEvent) {
72
+ const referrerUrl = this._lastUrl;
73
+ const currentUrl = changeState === "currententrychange" && navigationEvent?.target?.currentEntry?.url ? navigationEvent.target.currentEntry.url : location.href;
74
+ if (referrerUrl === currentUrl) return;
75
+ const navType = this._mapChangeStateToType(changeState, navigationEvent);
76
+ const sameDocument = this._determineSameDocument(referrerUrl, currentUrl);
77
+ const hashChange = isHashChange(referrerUrl, currentUrl);
78
+ const cfg = this.getConfig();
79
+ const logRecord = {
80
+ eventName: BROWSER_NAVIGATION_EVENT_NAME,
81
+ severityNumber: SeverityNumber.INFO,
82
+ attributes: {
83
+ [ATTR_URL_FULL]: cfg.sanitizeUrl ? cfg.sanitizeUrl(currentUrl) : currentUrl,
84
+ [ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT]: sameDocument,
85
+ [ATTR_BROWSER_NAVIGATION_HASH_CHANGE]: hashChange,
86
+ ...navType ? { [ATTR_BROWSER_NAVIGATION_TYPE]: navType } : {}
87
+ }
88
+ };
89
+ this._applyCustomLogRecordData(logRecord);
90
+ this.logger.emit(logRecord);
91
+ this._lastUrl = currentUrl;
92
+ }
93
+ _waitForPageLoad() {
94
+ if (document.readyState === "complete" && !this._hasProcessedInitialLoad) {
95
+ this._hasProcessedInitialLoad = true;
96
+ this._onHardNavigation();
97
+ return;
98
+ }
99
+ this._onDOMContentLoaded = () => {
100
+ if (!this._hasProcessedInitialLoad) {
101
+ this._hasProcessedInitialLoad = true;
102
+ this._onHardNavigation();
103
+ }
104
+ };
105
+ document.addEventListener("DOMContentLoaded", this._onDOMContentLoaded);
106
+ }
107
+ _patchHistoryApi() {
108
+ this._wrap(history, "replaceState", this._patchHistoryMethod("replaceState"));
109
+ this._wrap(history, "pushState", this._patchHistoryMethod("pushState"));
110
+ }
111
+ _patchHistoryMethod(changeState) {
112
+ const plugin = this;
113
+ return (original) => {
114
+ return function patchedHistoryMethod(...args) {
115
+ if (!plugin._isEnabled) return original.apply(this, args);
116
+ const result = original.apply(this, args);
117
+ if (location.href !== plugin._lastUrl) plugin._onSoftNavigation(changeState);
118
+ return result;
119
+ };
120
+ };
121
+ }
122
+ _applyCustomLogRecordData(logRecord) {
123
+ const hook = this.getConfig().applyCustomLogRecordData;
124
+ if (!hook) return;
125
+ safeExecuteInTheMiddle(() => hook(logRecord), (error) => {
126
+ if (error) this._diag.error("applyCustomLogRecordData hook failed", error);
127
+ }, true);
128
+ }
129
+ _determineSameDocument(fromUrl, toUrl) {
130
+ try {
131
+ const fromURL = new URL(fromUrl);
132
+ const toURL = new URL(toUrl);
133
+ return fromURL.origin === toURL.origin;
134
+ } catch {
135
+ return true;
136
+ }
137
+ }
138
+ _mapChangeStateToType(changeState, navigationEvent) {
139
+ if (changeState === "currententrychange") switch (navigationEvent?.navigationType) {
140
+ case "traverse": return "traverse";
141
+ case "replace": return "replace";
142
+ case "reload": return "reload";
143
+ default: return "push";
144
+ }
145
+ switch (changeState) {
146
+ case "pushState": return "push";
147
+ case "replaceState": return "replace";
148
+ case "popstate": return "traverse";
149
+ default: return;
150
+ }
151
+ }
152
+ };
153
+ //#endregion
154
+ export { NavigationInstrumentation };
155
+
156
+ //# sourceMappingURL=instrumentation.js.map
@@ -0,0 +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":";;;;;;AAuDA,IAAa,4BAAb,cAA+C,oBAAqD;CAYlG,YAAY,SAA0C,EAAE,EAAE;AACxD,QAAM,qDAAqD,SAAS,OAAO;AAC3E,OAAK,WAAW,SAAS;;CAG3B,OAA0B;AACxB,SAAO,EAAE;;CAGX,SAAwB;AACtB,MAAI,KAAK,WACP;AAEF,OAAK,aAAa;EAElB,MAAM,gBAAgB,KAAK,mBAAmB;AAG9C,MAAI,CAAC,iBAAiB,CAAC,KAAK,mBAAmB;AAC7C,QAAK,kBAAkB;AACvB,QAAK,oBAAoB;;AAG3B,OAAK,kBAAkB;AAEvB,MAAI,eAAe;AACjB,QAAK,yBAAyB,UAAU;AACtC,SAAK,kBAAkB,sBAAsB,MAAM;;AAErD,iBAAc,iBACZ,sBACA,KAAK,sBACN;SACI;AACL,QAAK,oBAAoB;AACvB,SAAK,kBAAkB,WAAW;;AAEpC,UAAO,iBAAiB,YAAY,KAAK,YAAY;;;CAIzD,UAAyB;AACvB,MAAI,CAAC,KAAK,WACR;AAEF,OAAK,aAAa;AAElB,MAAI,KAAK,qBAAqB;AAC5B,YAAS,oBACP,oBACA,KAAK,oBACN;AACD,QAAK,sBAAsB,KAAA;;AAE7B,MAAI,KAAK,aAAa;AACpB,UAAO,oBAAoB,YAAY,KAAK,YAAY;AACxD,QAAK,cAAc,KAAA;;AAErB,MAAI,KAAK,uBAAuB;AACR,QAAK,mBACd,EAAE,oBACb,sBACA,KAAK,sBACN;AACD,QAAK,wBAAwB,KAAA;;AAG/B,OAAK,2BAA2B;;CAGlC,oBAAuD;AAErD,MAAI,CADQ,KAAK,WACT,CAAC,4BACP;AAEF,SAAQ,OAAqD;;CAG/D,oBAAkC;EAChC,MAAM,MAAM,KAAK,WAAW;EAC5B,MAAM,YAAuB;GAC3B,WAAW;GACX,gBAAgB,eAAe;GAC/B,YAAY;KACT,gBAAgB,IAAI,cACjB,IAAI,YAAY,SAAS,YAAY,GACrC,SAAS;KACZ,wCAAwC;KACxC,sCAAsC;IACxC;GACF;AACD,OAAK,0BAA0B,UAAU;AACzC,OAAK,OAAO,KAAK,UAAU;;CAG7B,kBACE,aACA,iBACM;EACN,MAAM,cAAc,KAAK;EACzB,MAAM,aACJ,gBAAgB,wBAChB,iBAAiB,QAAQ,cAAc,MACnC,gBAAgB,OAAO,aAAa,MACpC,SAAS;AAEf,MAAI,gBAAgB,WAClB;EAGF,MAAM,UAAU,KAAK,sBAAsB,aAAa,gBAAgB;EACxE,MAAM,eAAe,KAAK,uBAAuB,aAAa,WAAW;EACzE,MAAM,aAAa,aAAa,aAAa,WAAW;EACxD,MAAM,MAAM,KAAK,WAAW;EAE5B,MAAM,YAAuB;GAC3B,WAAW;GACX,gBAAgB,eAAe;GAC/B,YAAY;KACT,gBAAgB,IAAI,cACjB,IAAI,YAAY,WAAW,GAC3B;KACH,wCAAwC;KACxC,sCAAsC;IACvC,GAAI,UAAU,GAAG,+BAA+B,SAAS,GAAG,EAAE;IAC/D;GACF;AACD,OAAK,0BAA0B,UAAU;AACzC,OAAK,OAAO,KAAK,UAAU;AAE3B,OAAK,WAAW;;CAGlB,mBAAiC;AAC/B,MAAI,SAAS,eAAe,cAAc,CAAC,KAAK,0BAA0B;AACxE,QAAK,2BAA2B;AAChC,QAAK,mBAAmB;AACxB;;AAGF,OAAK,4BAA4B;AAC/B,OAAI,CAAC,KAAK,0BAA0B;AAClC,SAAK,2BAA2B;AAChC,SAAK,mBAAmB;;;AAG5B,WAAS,iBAAiB,oBAAoB,KAAK,oBAAoB;;CAGzE,mBAAiC;AAC/B,OAAK,MACH,SACA,gBACA,KAAK,oBAAoB,eAAe,CACzC;AACD,OAAK,MAAM,SAAS,aAAa,KAAK,oBAAoB,YAAY,CAAC;;CAGzE,oBAA4B,aAA2C;EACrE,MAAM,SAAS;AACf,UAAQ,aAAoD;AAC1D,UAAO,SAAS,qBAEd,GAAG,MACH;AACA,QAAI,CAAC,OAAO,WACV,QAAO,SAAS,MAAM,MAAM,KAAK;IAEnC,MAAM,SAAS,SAAS,MAAM,MAAM,KAAK;AACzC,QAAI,SAAS,SAAS,OAAO,SAC3B,QAAO,kBAAkB,YAAY;AAEvC,WAAO;;;;CAKb,0BAAkC,WAA4B;EAE5D,MAAM,OADM,KAAK,WACD,CAAC;AACjB,MAAI,CAAC,KACH;AAEF,+BACQ,KAAK,UAAU,GACpB,UAAU;AACT,OAAI,MACF,MAAK,MAAM,MAAM,wCAAwC,MAAM;KAGnE,KACD;;CAGH,uBAA+B,SAAiB,OAAwB;AACtE,MAAI;GACF,MAAM,UAAU,IAAI,IAAI,QAAQ;GAChC,MAAM,QAAQ,IAAI,IAAI,MAAM;AAC5B,UAAO,QAAQ,WAAW,MAAM;UAC1B;AAGN,UAAO;;;CAIX,sBACE,aACA,iBAC4B;AAC5B,MAAI,gBAAgB,qBAElB,SADgB,iBAAiB,gBACjC;GACE,KAAK,WACH,QAAO;GACT,KAAK,UACH,QAAO;GACT,KAAK,SACH,QAAO;GACT,QAGE,QAAO;;AAIb,UAAQ,aAAR;GACE,KAAK,YACH,QAAO;GACT,KAAK,eACH,QAAO;GACT,KAAK,WACH,QAAO;GACT,QACE"}
@@ -0,0 +1,22 @@
1
+ //#region src/navigation/semconv.ts
2
+ const BROWSER_NAVIGATION_EVENT_NAME = "browser.navigation";
3
+ /**
4
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
5
+ */
6
+ const ATTR_URL_FULL = "url.full";
7
+ /**
8
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
9
+ */
10
+ const ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT = "browser.navigation.same_document";
11
+ /**
12
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
13
+ */
14
+ const ATTR_BROWSER_NAVIGATION_HASH_CHANGE = "browser.navigation.hash_change";
15
+ /**
16
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
17
+ */
18
+ const ATTR_BROWSER_NAVIGATION_TYPE = "browser.navigation.type";
19
+ //#endregion
20
+ export { ATTR_BROWSER_NAVIGATION_HASH_CHANGE, ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT, ATTR_BROWSER_NAVIGATION_TYPE, ATTR_URL_FULL, BROWSER_NAVIGATION_EVENT_NAME };
21
+
22
+ //# sourceMappingURL=semconv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semconv.js","names":[],"sources":["../../src/navigation/semconv.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/*\n * This file contains a copy of unstable semantic convention definitions\n * used by this package.\n * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv\n */\n\nexport const BROWSER_NAVIGATION_EVENT_NAME = 'browser.navigation';\n\n/**\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_URL_FULL = 'url.full';\n\n/**\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_BROWSER_NAVIGATION_SAME_DOCUMENT =\n 'browser.navigation.same_document';\n\n/**\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_BROWSER_NAVIGATION_HASH_CHANGE =\n 'browser.navigation.hash_change';\n\n/**\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_BROWSER_NAVIGATION_TYPE = 'browser.navigation.type';\n"],"mappings":";AAWA,MAAa,gCAAgC;;;;AAK7C,MAAa,gBAAgB;;;;AAK7B,MAAa,wCACX;;;;AAKF,MAAa,sCACX;;;;AAKF,MAAa,+BAA+B"}
@@ -0,0 +1,21 @@
1
+ import { LogRecord } from "@opentelemetry/api-logs";
2
+ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
+
4
+ //#region src/navigation/types.d.ts
5
+ type ApplyCustomLogRecordDataFunction = (logRecord: LogRecord) => void;
6
+ type SanitizeUrlFunction = (url: string) => string;
7
+ type NavigationType = 'push' | 'replace' | 'reload' | 'traverse';
8
+ /**
9
+ * NavigationInstrumentation Configuration
10
+ */
11
+ interface NavigationInstrumentationConfig extends InstrumentationConfig {
12
+ /** Hook to modify log records before they are emitted. */
13
+ applyCustomLogRecordData?: ApplyCustomLogRecordDataFunction;
14
+ /** Use the Navigation API `currententrychange` event if available (experimental). Defaults to false. */
15
+ useNavigationApiIfAvailable?: boolean;
16
+ /** Custom function to sanitize URLs before adding to log records. */
17
+ sanitizeUrl?: SanitizeUrlFunction;
18
+ }
19
+ //#endregion
20
+ export { NavigationInstrumentationConfig, NavigationType };
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,77 @@
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
+ /**
49
+ * Determines if navigation between two URLs represents a hash change.
50
+ * A hash change is true if the URLs are the same except for the hash part,
51
+ * AND the hash is being added or changed (not removed).
52
+ *
53
+ * @param fromUrl - The source URL
54
+ * @param toUrl - The destination URL
55
+ * @returns true if this represents a hash change navigation
56
+ */
57
+ function isHashChange(fromUrl, toUrl) {
58
+ try {
59
+ const a = new URL(fromUrl, window.location.origin);
60
+ const b = new URL(toUrl, window.location.origin);
61
+ const sameBase = a.origin === b.origin && a.pathname === b.pathname && a.search === b.search;
62
+ const fromHasHash = a.hash !== "";
63
+ const toHasHash = b.hash !== "";
64
+ const hashesAreDifferent = a.hash !== b.hash;
65
+ return sameBase && hashesAreDifferent && (fromHasHash && toHasHash || !fromHasHash && toHasHash);
66
+ } catch {
67
+ const fromBase = fromUrl.split("#")[0];
68
+ const toBase = toUrl.split("#")[0];
69
+ const fromHash = fromUrl.split("#")[1] ?? "";
70
+ const toHash = toUrl.split("#")[1] ?? "";
71
+ return fromBase === toBase && fromHash !== toHash && toHash !== "";
72
+ }
73
+ }
74
+ //#endregion
75
+ export { defaultSanitizeUrl, isHashChange };
76
+
77
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +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\nconst SENSITIVE_PARAMS = [\n 'password',\n 'passwd',\n 'secret',\n 'api_key',\n 'apikey',\n 'auth',\n 'authorization',\n 'token',\n 'access_token',\n 'refresh_token',\n 'jwt',\n 'session',\n 'sessionid',\n 'key',\n 'private_key',\n 'client_secret',\n 'client_id',\n 'signature',\n 'hash',\n];\n\n/**\n * Default URL sanitization function that redacts credentials and sensitive query parameters.\n * This is the default implementation used when no custom sanitizeUrl callback is provided.\n *\n * @param url - The URL to sanitize\n * @returns The sanitized URL with credentials and sensitive parameters redacted\n */\nexport function defaultSanitizeUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n\n if (urlObj.username || urlObj.password) {\n urlObj.username = 'REDACTED';\n urlObj.password = 'REDACTED';\n }\n\n for (const param of SENSITIVE_PARAMS) {\n if (urlObj.searchParams.has(param)) {\n urlObj.searchParams.set(param, 'REDACTED');\n }\n }\n\n return urlObj.toString();\n } catch {\n // If URL parsing fails, redact credentials and sensitive query parameters\n // using regexes. The credential regex uses a restricted character class to\n // avoid polynomial time complexity.\n let sanitized = url.replace(/\\/\\/[^:/@]+:[^/@]+@/, '//REDACTED:REDACTED@');\n\n for (const param of SENSITIVE_PARAMS) {\n // Match param=value or param%3Dvalue (URL encoded)\n const regex = new RegExp(`([?&]${param}(?:%3D|=))[^&]*`, 'gi');\n sanitized = sanitized.replace(regex, '$1REDACTED');\n }\n\n return sanitized;\n }\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":";AAKA,MAAM,mBAAmB;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;AASD,SAAgB,mBAAmB,KAAqB;AACtD,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,IAAI;AAE3B,MAAI,OAAO,YAAY,OAAO,UAAU;AACtC,UAAO,WAAW;AAClB,UAAO,WAAW;;AAGpB,OAAK,MAAM,SAAS,iBAClB,KAAI,OAAO,aAAa,IAAI,MAAM,CAChC,QAAO,aAAa,IAAI,OAAO,WAAW;AAI9C,SAAO,OAAO,UAAU;SAClB;EAIN,IAAI,YAAY,IAAI,QAAQ,uBAAuB,uBAAuB;AAE1E,OAAK,MAAM,SAAS,kBAAkB;GAEpC,MAAM,QAAQ,IAAI,OAAO,QAAQ,MAAM,kBAAkB,KAAK;AAC9D,eAAY,UAAU,QAAQ,OAAO,aAAa;;AAGpD,SAAO;;;;;;;;;;;;AAaX,SAAgB,aAAa,SAAiB,OAAwB;AACpE,KAAI;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;AAExC,SACE,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;AAMtC,SAJiB,aAAa,UACH,aAAa,UAChB,WAAW"}
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.3.0";
2
+ var version = "0.4.0";
3
3
  //#endregion
4
4
  export { version };
5
5
 
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@opentelemetry/browser-instrumentation",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "OpenTelemetry browser instrumentations.",
5
5
  "keywords": [
6
6
  "opentelemetry",
7
7
  "browser",
8
8
  "web",
9
9
  "instrumentation",
10
+ "console",
10
11
  "navigation-timing",
11
12
  "user-action",
12
13
  "web-vitals",
13
- "resource-timing"
14
+ "resource-timing",
15
+ "navigation"
14
16
  ],
15
17
  "homepage": "https://github.com/open-telemetry/opentelemetry-browser",
16
18
  "bugs": "https://github.com/open-telemetry/opentelemetry-browser/issues",
@@ -27,6 +29,8 @@
27
29
  "#instrumentation-test-utils": "./src/test-utils/index.ts"
28
30
  },
29
31
  "exports": {
32
+ "./experimental/navigation": "./dist/navigation/index.js",
33
+ "./experimental/console": "./dist/console/index.js",
30
34
  "./experimental/navigation-timing": "./dist/navigation-timing/index.js",
31
35
  "./experimental/user-action": "./dist/user-action/index.js",
32
36
  "./experimental/web-vitals": "./dist/web-vitals/index.js",
@@ -44,15 +48,15 @@
44
48
  "test:coverage": "vitest --coverage"
45
49
  },
46
50
  "dependencies": {
47
- "@opentelemetry/api-logs": "^0.215.0",
48
- "@opentelemetry/instrumentation": "^0.215.0",
51
+ "@opentelemetry/api-logs": "^0.216.0",
52
+ "@opentelemetry/instrumentation": "^0.216.0",
49
53
  "web-vitals": "^5.2.0"
50
54
  },
51
55
  "peerDependencies": {
52
- "@opentelemetry/api": "^1.9.0"
56
+ "@opentelemetry/api": "^1.9.1"
53
57
  },
54
58
  "devDependencies": {
55
- "@opentelemetry/sdk-logs": "^0.215.0"
59
+ "@opentelemetry/sdk-logs": "0.216.0"
56
60
  },
57
61
  "publishConfig": {
58
62
  "access": "public"