@opentelemetry/browser-instrumentation 0.3.0 → 0.5.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,10 +13,13 @@ 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)
19
20
  - [Web Vitals](#web-vitals) — automatic instrumentation for Core Web Vitals
21
+ - [Console](#console) — automatic instrumentation for console API calls (log, warn, error, info, debug)
22
+ - [Errors](#errors) — automatic instrumentation for unhandled errors and promise rejections
20
23
 
21
24
  ## Usage
22
25
 
@@ -28,6 +31,8 @@ import {
28
31
  SimpleLogRecordProcessor,
29
32
  } from '@opentelemetry/sdk-logs';
30
33
  import { registerInstrumentations } from '@opentelemetry/instrumentation';
34
+ import { ErrorsInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/errors';
35
+ import { NavigationInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation';
31
36
  import { NavigationTimingInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation-timing';
32
37
  import { ResourceTimingInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/resource-timing';
33
38
  import { UserActionInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/user-action';
@@ -42,6 +47,8 @@ logs.setGlobalLoggerProvider(logProvider);
42
47
 
43
48
  registerInstrumentations({
44
49
  instrumentations: [
50
+ new ErrorsInstrumentation(),
51
+ new NavigationInstrumentation(),
45
52
  new NavigationTimingInstrumentation(),
46
53
  new ResourceTimingInstrumentation(),
47
54
  new UserActionInstrumentation(),
@@ -52,6 +59,62 @@ registerInstrumentations({
52
59
 
53
60
  ---
54
61
 
62
+ ### Navigation
63
+
64
+ ```typescript
65
+ import { NavigationInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation';
66
+ ```
67
+
68
+ 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`.
69
+
70
+ #### Configuration
71
+
72
+ ```typescript
73
+ import {
74
+ NavigationInstrumentation,
75
+ defaultSanitizeUrl,
76
+ } from '@opentelemetry/browser-instrumentation/experimental/navigation';
77
+
78
+ new NavigationInstrumentation({
79
+ // Use window.navigation (Navigation API) when available instead of
80
+ // patching history.pushState / history.replaceState. Default: false.
81
+ useNavigationApiIfAvailable: true,
82
+
83
+ // Rewrite the captured URL before it is emitted. Useful for stripping
84
+ // path segments, query parameters, or tokens that should not be exported.
85
+ sanitizeUrl: (url) => defaultSanitizeUrl(url),
86
+
87
+ // Mutate the log record before it is emitted (e.g. attach custom attributes).
88
+ applyCustomLogRecordData: (logRecord) => {
89
+ logRecord.attributes = {
90
+ ...logRecord.attributes,
91
+ 'app.route.id': '...',
92
+ };
93
+ },
94
+ });
95
+ ```
96
+
97
+ | Option | Type | Default | Description |
98
+ |--------|------|---------|-------------|
99
+ | `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. |
100
+ | `sanitizeUrl` | `(url: string) => string` | — | Called before the URL is written to `url.full`. |
101
+ | `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. |
102
+
103
+ `defaultSanitizeUrl` is exported for composition — it redacts `user:password@` credentials and a set of common sensitive query parameters (`api_key`, `token`, `password`, etc.).
104
+
105
+ #### Captured Attributes
106
+
107
+ Each `browser.navigation` event includes:
108
+
109
+ | Attribute | Description |
110
+ |-----------|-------------|
111
+ | `url.full` | The destination URL (after `sanitizeUrl` if configured). |
112
+ | `browser.navigation.same_document` | `true` for SPA route changes; `false` for full-page loads. |
113
+ | `browser.navigation.hash_change` | `true` when the navigation only adds or changes the URL fragment. |
114
+ | `browser.navigation.type` | One of `push`, `replace`, `reload`, `traverse` (omitted for the initial hard navigation). |
115
+
116
+ ---
117
+
55
118
  ### Navigation Timing
56
119
 
57
120
  ```typescript
@@ -162,6 +225,67 @@ Provides automatic instrumentation for [Core Web Vitals](https://web.dev/vitals/
162
225
  | `includeRawAttribution` | `boolean` | `false` | When true, sets the log record body to the JSON-stringified `web-vitals` attribution object. |
163
226
  | `applyCustomLogRecordData` | `(logRecord: LogRecord) => void` | — | Hook to modify log records before they are emitted. |
164
227
 
228
+ ### Console
229
+
230
+ ```typescript
231
+ import { ConsoleInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/console';
232
+ ```
233
+
234
+ Provides automatic instrumentation for browser console API calls. By default captures `log`, `warn`, `error`, `info`, and `debug` methods.
235
+
236
+ #### Configuration
237
+
238
+ ```typescript
239
+ new ConsoleInstrumentation({
240
+ // Specify which console methods to capture (default: log, warn, error, info, debug)
241
+ logMethods: ['error', 'warn'],
242
+ });
243
+ ```
244
+ #### Captured Attributes
245
+ Each `browser.console` event includes the following attributes:
246
+
247
+ | Attribute | Description | Example |
248
+ |-----------|-------------|---------|
249
+ | `browser.console.method` | The console method that was called | `error`, `warn`, `log`, `info`, `debug` |
250
+
251
+ ---
252
+
253
+ ### Errors
254
+
255
+ ```typescript
256
+ import { ErrorsInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/errors';
257
+ ```
258
+
259
+ Emits an `exception` event for every uncaught error (`window.addEventListener('error', ...)`) and unhandled promise rejection (`window.addEventListener('unhandledrejection', ...)`).
260
+
261
+ #### Configuration
262
+
263
+ ```typescript
264
+ new ErrorsInstrumentation({
265
+ // Return extra attributes to attach to the emitted log record.
266
+ applyCustomAttributes: (error) => ({
267
+ 'app.error.severity':
268
+ error instanceof Error && error.name === 'ValidationError'
269
+ ? 'warning'
270
+ : 'error',
271
+ }),
272
+ });
273
+ ```
274
+
275
+ | Option | Type | Default | Description |
276
+ |--------|------|---------|-------------|
277
+ | `applyCustomAttributes` | `(error: Error \| string) => Attributes` | — | Returns extra attributes to merge onto the emitted log record. Errors thrown from this hook are caught and logged via the instrumentation diag logger. |
278
+
279
+ #### Captured Attributes
280
+
281
+ Each `exception` event includes:
282
+
283
+ | Attribute | Description |
284
+ |-----------|-------------|
285
+ | `exception.type` | The error's `name` (omitted when the thrown value is a string). |
286
+ | `exception.message` | The error's `message`, or the thrown string itself. |
287
+ | `exception.stacktrace` | The error's `stack` (omitted when the thrown value is a string). |
288
+
165
289
  ## Useful links
166
290
 
167
291
  - For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
@@ -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,3 @@
1
+ import { ApplyCustomAttributesFunction, ErrorsInstrumentationConfig } from "./types.js";
2
+ import { ErrorsInstrumentation } from "./instrumentation.js";
3
+ export { type ApplyCustomAttributesFunction, ErrorsInstrumentation, type ErrorsInstrumentationConfig };
@@ -0,0 +1,2 @@
1
+ import { ErrorsInstrumentation } from "./instrumentation.js";
2
+ export { ErrorsInstrumentation };
@@ -0,0 +1,17 @@
1
+ import { ErrorsInstrumentationConfig } from "./types.js";
2
+ import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
+
4
+ //#region src/errors/instrumentation.d.ts
5
+ declare class ErrorsInstrumentation extends InstrumentationBase<ErrorsInstrumentationConfig> {
6
+ private _isEnabled;
7
+ private _onErrorHandler?;
8
+ constructor(config?: ErrorsInstrumentationConfig);
9
+ protected init(): never[];
10
+ enable(): void;
11
+ disable(): void;
12
+ private _onError;
13
+ private _applyCustomAttributes;
14
+ }
15
+ //#endregion
16
+ export { ErrorsInstrumentation };
17
+ //# sourceMappingURL=instrumentation.d.ts.map
@@ -0,0 +1,66 @@
1
+ import { version } from "../package.js";
2
+ import { SeverityNumber } from "@opentelemetry/api-logs";
3
+ import { InstrumentationBase, safeExecuteInTheMiddle } from "@opentelemetry/instrumentation";
4
+ import { ATTR_EXCEPTION_MESSAGE, ATTR_EXCEPTION_STACKTRACE, ATTR_EXCEPTION_TYPE } from "@opentelemetry/semantic-conventions";
5
+ //#region src/errors/instrumentation.ts
6
+ const EXCEPTION_EVENT_NAME = "exception";
7
+ var ErrorsInstrumentation = class extends InstrumentationBase {
8
+ constructor(config = {}) {
9
+ super("@opentelemetry/browser-instrumentation/errors", version, config);
10
+ }
11
+ init() {
12
+ return [];
13
+ }
14
+ enable() {
15
+ if (this._isEnabled) return;
16
+ this._isEnabled = true;
17
+ this._onErrorHandler = (event) => this._onError(event);
18
+ window.addEventListener("error", this._onErrorHandler);
19
+ window.addEventListener("unhandledrejection", this._onErrorHandler);
20
+ }
21
+ disable() {
22
+ if (!this._isEnabled) return;
23
+ this._isEnabled = false;
24
+ if (this._onErrorHandler) {
25
+ window.removeEventListener("error", this._onErrorHandler);
26
+ window.removeEventListener("unhandledrejection", this._onErrorHandler);
27
+ this._onErrorHandler = void 0;
28
+ }
29
+ }
30
+ _onError(event) {
31
+ const error = "reason" in event ? event.reason : event.error;
32
+ if (error == null) return;
33
+ let errorAttributes;
34
+ if (typeof error === "string") errorAttributes = { [ATTR_EXCEPTION_MESSAGE]: error };
35
+ else errorAttributes = {
36
+ [ATTR_EXCEPTION_TYPE]: error.name,
37
+ [ATTR_EXCEPTION_MESSAGE]: error.message,
38
+ [ATTR_EXCEPTION_STACKTRACE]: error.stack
39
+ };
40
+ const customAttributes = this._applyCustomAttributes(error);
41
+ const logRecord = {
42
+ eventName: EXCEPTION_EVENT_NAME,
43
+ severityNumber: SeverityNumber.ERROR,
44
+ attributes: {
45
+ ...errorAttributes,
46
+ ...customAttributes
47
+ }
48
+ };
49
+ this.logger.emit(logRecord);
50
+ }
51
+ _applyCustomAttributes(error) {
52
+ const hook = this.getConfig().applyCustomAttributes;
53
+ if (!hook) return {};
54
+ let result = {};
55
+ safeExecuteInTheMiddle(() => {
56
+ result = hook(error);
57
+ }, (err) => {
58
+ if (err) this._diag.error("applyCustomAttributes hook failed", err);
59
+ }, true);
60
+ return result;
61
+ }
62
+ };
63
+ //#endregion
64
+ export { ErrorsInstrumentation };
65
+
66
+ //# sourceMappingURL=instrumentation.js.map
@@ -0,0 +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;AACpD,QAAM,iDAAiD,SAAS,OAAO;;CAGzE,OAA0B;AACxB,SAAO,EAAE;;CAGX,SAAwB;AACtB,MAAI,KAAK,WACP;AAEF,OAAK,aAAa;AAElB,OAAK,mBAAmB,UAAU,KAAK,SAAS,MAAM;AACtD,SAAO,iBAAiB,SAAS,KAAK,gBAAgB;AACtD,SAAO,iBAAiB,sBAAsB,KAAK,gBAAgB;;CAGrE,UAAyB;AACvB,MAAI,CAAC,KAAK,WACR;AAEF,OAAK,aAAa;AAElB,MAAI,KAAK,iBAAiB;AACxB,UAAO,oBAAoB,SAAS,KAAK,gBAAgB;AACzD,UAAO,oBAAoB,sBAAsB,KAAK,gBAAgB;AACtE,QAAK,kBAAkB,KAAA;;;CAI3B,SAAiB,OAAiD;EAChE,MAAM,QACJ,YAAY,QAAQ,MAAM,SAAS,MAAM;AAE3C,MAAI,SAAS,KACX;EAGF,IAAI;AACJ,MAAI,OAAO,UAAU,SACnB,mBAAkB,GAAG,yBAAyB,OAAO;MAErD,mBAAkB;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;AAED,OAAK,OAAO,KAAK,UAAU;;CAG7B,uBAA+B,OAAmC;EAChE,MAAM,OAAO,KAAK,WAAW,CAAC;AAC9B,MAAI,CAAC,KACH,QAAO,EAAE;EAEX,IAAI,SAAqB,EAAE;AAC3B,+BACQ;AACJ,YAAS,KAAK,MAAM;MAErB,QAAQ;AACP,OAAI,IACF,MAAK,MAAM,MAAM,qCAAqC,IAAI;KAG9D,KACD;AACD,SAAO"}
@@ -0,0 +1,19 @@
1
+ import { Attributes } from "@opentelemetry/api";
2
+ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
+
4
+ //#region src/errors/types.d.ts
5
+ type ApplyCustomAttributesFunction = (error: Error | string) => Attributes;
6
+ /**
7
+ * ErrorsInstrumentation Configuration
8
+ */
9
+ interface ErrorsInstrumentationConfig extends InstrumentationConfig {
10
+ /**
11
+ * Optional callback invoked for each captured error or unhandled rejection.
12
+ * Returned attributes are merged onto the emitted log record (after the
13
+ * standard `exception.*` attributes, so they may override them).
14
+ */
15
+ applyCustomAttributes?: ApplyCustomAttributesFunction;
16
+ }
17
+ //#endregion
18
+ export { ApplyCustomAttributesFunction, ErrorsInstrumentationConfig };
19
+ //# 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":"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"}
@@ -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.5.0";
3
3
  //#endregion
4
4
  export { version };
5
5
 
@@ -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 '#instrumentation-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;AACxD,QACE,sDACA,SACA,OACD;;CAGH,OAA0B;AACxB,SAAO,EAAE;;CAGX,8BAAsC,OAAgC;AACpE,UAAQ,MAAM,QAAd;GACE,KAAK,EACH,QAAO;GACT,KAAK,EACH,QAAO;GACT,KAAK,EACH,QAAO;GACT,QACE,QAAO;;;CAIb,QAAgB,OAAmB;EACjC,MAAM,UAAU,MAAM;AAEtB,MAAI,EAAE,mBAAmB,aACvB;AAGF,MAAI,QAAQ,aAAa,WAAW,CAClC;EAGF,MAAM,cAAc,sBAAsB,SAAS;GACjD,uBAAuB;GACvB,mBAAmB;GACpB,CAAC;EACF,MAAM,yBAAiD,EAAE;AAGzD,OAAK,MAAM,QAAQ,QAAQ,WACzB,KAAI,KAAK,KAAK,WAAW,8BAA8B,CACrD,wBACE,KAAK,KAAK,MAAM,GAAqC,IACnD,KAAK;AAIb,OAAK,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;AACtC,MAAI,CAAC,KAAK,gBACR,MAAK,kBAAkB,KAAK,QAAQ,KAAK,KAAK;AAGhD,MAAI,oBAAoB,SAAS,QAAQ,CACvC,UAAS,iBAAiB,SAAS,KAAK,iBAAiB,KAAK;;CAIlE,UAAyB;AACvB,MAAI,KAAK,gBACP,UAAS,oBAAoB,SAAS,KAAK,iBAAiB,KAAK"}
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;AACxD,QACE,sDACA,SACA,OACD;;CAGH,OAA0B;AACxB,SAAO,EAAE;;CAGX,8BAAsC,OAAgC;AACpE,UAAQ,MAAM,QAAd;GACE,KAAK,EACH,QAAO;GACT,KAAK,EACH,QAAO;GACT,KAAK,EACH,QAAO;GACT,QACE,QAAO;;;CAIb,QAAgB,OAAmB;EACjC,MAAM,UAAU,MAAM;AAEtB,MAAI,EAAE,mBAAmB,aACvB;AAGF,MAAI,QAAQ,aAAa,WAAW,CAClC;EAGF,MAAM,cAAc,sBAAsB,SAAS;GACjD,uBAAuB;GACvB,mBAAmB;GACpB,CAAC;EACF,MAAM,yBAAiD,EAAE;AAGzD,OAAK,MAAM,QAAQ,QAAQ,WACzB,KAAI,KAAK,KAAK,WAAW,8BAA8B,CACrD,wBACE,KAAK,KAAK,MAAM,GAAqC,IACnD,KAAK;AAIb,OAAK,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;AACtC,MAAI,CAAC,KAAK,gBACR,MAAK,kBAAkB,KAAK,QAAQ,KAAK,KAAK;AAGhD,MAAI,oBAAoB,SAAS,QAAQ,CACvC,UAAS,iBAAiB,SAAS,KAAK,iBAAiB,KAAK;;CAIlE,UAAyB;AACvB,MAAI,KAAK,gBACP,UAAS,oBAAoB,SAAS,KAAK,iBAAiB,KAAK"}
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "@opentelemetry/browser-instrumentation",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "OpenTelemetry browser instrumentations.",
5
5
  "keywords": [
6
6
  "opentelemetry",
7
7
  "browser",
8
8
  "web",
9
9
  "instrumentation",
10
+ "console",
11
+ "errors",
12
+ "navigation",
10
13
  "navigation-timing",
14
+ "resource-timing",
11
15
  "user-action",
12
- "web-vitals",
13
- "resource-timing"
16
+ "web-vitals"
14
17
  ],
15
18
  "homepage": "https://github.com/open-telemetry/opentelemetry-browser",
16
19
  "bugs": "https://github.com/open-telemetry/opentelemetry-browser/issues",
@@ -23,14 +26,17 @@
23
26
  },
24
27
  "type": "module",
25
28
  "imports": {
26
- "#instrumentation-utils": "./src/utils/index.ts",
27
- "#instrumentation-test-utils": "./src/test-utils/index.ts"
29
+ "#utils": "./src/utils/index.ts",
30
+ "#utils/test": "./src/utils/test/index.ts"
28
31
  },
29
32
  "exports": {
33
+ "./experimental/console": "./dist/console/index.js",
34
+ "./experimental/errors": "./dist/errors/index.js",
35
+ "./experimental/navigation": "./dist/navigation/index.js",
30
36
  "./experimental/navigation-timing": "./dist/navigation-timing/index.js",
37
+ "./experimental/resource-timing": "./dist/resource-timing/index.js",
31
38
  "./experimental/user-action": "./dist/user-action/index.js",
32
- "./experimental/web-vitals": "./dist/web-vitals/index.js",
33
- "./experimental/resource-timing": "./dist/resource-timing/index.js"
39
+ "./experimental/web-vitals": "./dist/web-vitals/index.js"
34
40
  },
35
41
  "files": [
36
42
  "dist"
@@ -44,15 +50,16 @@
44
50
  "test:coverage": "vitest --coverage"
45
51
  },
46
52
  "dependencies": {
47
- "@opentelemetry/api-logs": "^0.215.0",
48
- "@opentelemetry/instrumentation": "^0.215.0",
53
+ "@opentelemetry/api-logs": "^0.217.0",
54
+ "@opentelemetry/instrumentation": "^0.217.0",
55
+ "@opentelemetry/semantic-conventions": "^1.40.0",
49
56
  "web-vitals": "^5.2.0"
50
57
  },
51
58
  "peerDependencies": {
52
- "@opentelemetry/api": "^1.9.0"
59
+ "@opentelemetry/api": "^1.9.1"
53
60
  },
54
61
  "devDependencies": {
55
- "@opentelemetry/sdk-logs": "^0.215.0"
62
+ "@opentelemetry/sdk-logs": "0.217.0"
56
63
  },
57
64
  "publishConfig": {
58
65
  "access": "public"