@opentelemetry/browser-instrumentation 0.1.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 ADDED
@@ -0,0 +1,123 @@
1
+ # @opentelemetry/browser-instrumentation
2
+
3
+ [![NPM Published Version][npm-img]][npm-url]
4
+ [![Apache License][license-image]][license-image]
5
+
6
+ OpenTelemetry browser instrumentations, available as subpath exports under `./experimental/*`.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @opentelemetry/browser-instrumentation
12
+ ```
13
+
14
+ ## Instrumentations
15
+
16
+ - [Navigation Timing](#navigation-timing) — automatic instrumentation for navigation timing
17
+ - [User Action](#user-action) — automatic instrumentation for user actions (clicks)
18
+ - [Web Vitals](#web-vitals) — automatic instrumentation for Core Web Vitals
19
+
20
+ ## Usage
21
+
22
+ ```typescript
23
+ import { logs } from '@opentelemetry/api-logs';
24
+ import {
25
+ ConsoleLogRecordExporter,
26
+ LoggerProvider,
27
+ SimpleLogRecordProcessor,
28
+ } from '@opentelemetry/sdk-logs';
29
+ import { registerInstrumentations } from '@opentelemetry/instrumentation';
30
+ import { NavigationTimingInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation-timing';
31
+ import { UserActionInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/user-action';
32
+ import { WebVitalsInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/web-vitals';
33
+
34
+ const logProvider = new LoggerProvider({
35
+ processors: [
36
+ new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()),
37
+ ],
38
+ });
39
+ logs.setGlobalLoggerProvider(logProvider);
40
+
41
+ registerInstrumentations({
42
+ instrumentations: [
43
+ new NavigationTimingInstrumentation(),
44
+ new UserActionInstrumentation(),
45
+ new WebVitalsInstrumentation(),
46
+ ],
47
+ });
48
+ ```
49
+
50
+ ---
51
+
52
+ ### Navigation Timing
53
+
54
+ ```typescript
55
+ import { NavigationTimingInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation-timing';
56
+ ```
57
+
58
+ Provides automatic instrumentation for [Navigation Timing](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming) in web applications.
59
+
60
+ ---
61
+
62
+ ### User Action
63
+
64
+ ```typescript
65
+ import { UserActionInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/user-action';
66
+ ```
67
+
68
+ Provides automatic instrumentation for user actions in web applications.
69
+
70
+ Compatible with OpenTelemetry JS API and SDK `1.0+`.
71
+
72
+ #### Configuration
73
+
74
+ By default the instrumentation captures `click` events. You can configure which events to capture by passing an options object:
75
+
76
+ ```typescript
77
+ new UserActionInstrumentation({
78
+ autoCapturedActions: [], // default is ['click']
79
+ });
80
+ ```
81
+
82
+ #### Additional Attributes
83
+
84
+ Data attributes with the prefix `data-otel-` on the target element will be added as additional attributes to the generated log record. For example:
85
+
86
+ ```html
87
+ <button id="btn1" data-otel-user-id="12345" data-otel-feature="signup">
88
+ Sign Up
89
+ </button>
90
+ ```
91
+
92
+ ---
93
+
94
+ ### Web Vitals
95
+
96
+ ```typescript
97
+ import { WebVitalsInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/web-vitals';
98
+ ```
99
+
100
+ Provides automatic instrumentation for [Core Web Vitals](https://web.dev/vitals/) using the [`web-vitals`](https://github.com/GoogleChrome/web-vitals) library.
101
+
102
+ #### Configuration
103
+
104
+ | Option | Type | Default | Description |
105
+ |--------|------|---------|-------------|
106
+ | `includeRawAttribution` | `boolean` | `false` | When true, sets the log record body to the JSON-stringified `web-vitals` attribution object. |
107
+ | `applyCustomLogRecordData` | `(logRecord: LogRecord) => void` | — | Hook to modify log records before they are emitted. |
108
+
109
+ ## Useful links
110
+
111
+ - For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
112
+ - For more about OpenTelemetry Browser: <https://github.com/open-telemetry/opentelemetry-browser>
113
+ - For help or feedback on this project, join us in [GitHub Discussions][discussions-url]
114
+
115
+ ## License
116
+
117
+ Apache 2.0 - See [LICENSE][license-url] for more information.
118
+
119
+ [discussions-url]: https://github.com/open-telemetry/opentelemetry-browser/discussions/landing
120
+ [license-url]: https://github.com/open-telemetry/opentelemetry-browser/blob/main/LICENSE
121
+ [license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat
122
+ [npm-url]: https://www.npmjs.com/package/@opentelemetry/browser-instrumentation
123
+ [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fbrowser-instrumentation.svg
@@ -0,0 +1,2 @@
1
+ import { NavigationTimingInstrumentation } from "./instrumentation.js";
2
+ export { NavigationTimingInstrumentation };
@@ -0,0 +1,2 @@
1
+ import { NavigationTimingInstrumentation } from "./instrumentation.js";
2
+ export { NavigationTimingInstrumentation };
@@ -0,0 +1,41 @@
1
+ import { NavigationTimingInstrumentationConfig } from "./types.js";
2
+ import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
+
4
+ //#region src/navigation-timing/instrumentation.d.ts
5
+ /**
6
+ * This class automatically instruments navigation timing within the browser.
7
+ */
8
+ declare class NavigationTimingInstrumentation extends InstrumentationBase<NavigationTimingInstrumentationConfig> {
9
+ private _lastEntry?;
10
+ private _didEmit;
11
+ private _completeDelayTimeoutId?;
12
+ private _retryCount;
13
+ private _isEnabled;
14
+ private _onLoad;
15
+ private _onPageHide;
16
+ constructor(config?: NavigationTimingInstrumentationConfig);
17
+ protected init(): never[];
18
+ enable(): void;
19
+ disable(): void;
20
+ private _getLatestNavigationEntry;
21
+ private _calculateBackoffDelay;
22
+ /**
23
+ * Attempts to emit the navigation timing event.
24
+ *
25
+ * - Emits immediately if a complete `PerformanceNavigationTiming` entry is available.
26
+ * - If the page is still loading, waits for `window.load` and retries.
27
+ * - If the page is already loaded but the entry is not finalized yet, schedules one
28
+ * deferred re-check (to allow the browser to populate the timing fields).
29
+ *
30
+ * This method can be called multiple times (from `enable()`, the load handler, or the
31
+ * deferred timeout), so it must be safe to re-enter.
32
+ */
33
+ private _tryEmitOrSchedule;
34
+ private _handleUnload;
35
+ private _emitAndCleanup;
36
+ private _emitNavigationTiming;
37
+ private _unsubscribeAll;
38
+ }
39
+ //#endregion
40
+ export { NavigationTimingInstrumentation };
41
+ //# sourceMappingURL=instrumentation.d.ts.map
@@ -0,0 +1,144 @@
1
+ import { ATTR_NAVIGATION_CONNECT_END, ATTR_NAVIGATION_CONNECT_START, ATTR_NAVIGATION_DECODED_BODY_SIZE, ATTR_NAVIGATION_DOMAIN_LOOKUP_END, ATTR_NAVIGATION_DOMAIN_LOOKUP_START, ATTR_NAVIGATION_DOM_COMPLETE, ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END, ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START, ATTR_NAVIGATION_DOM_INTERACTIVE, ATTR_NAVIGATION_DURATION, ATTR_NAVIGATION_ENCODED_BODY_SIZE, ATTR_NAVIGATION_FETCH_START, ATTR_NAVIGATION_LOAD_EVENT_END, ATTR_NAVIGATION_LOAD_EVENT_START, ATTR_NAVIGATION_REDIRECT_COUNT, ATTR_NAVIGATION_REQUEST_START, ATTR_NAVIGATION_RESPONSE_END, ATTR_NAVIGATION_RESPONSE_START, ATTR_NAVIGATION_SECURE_CONNECTION_START, ATTR_NAVIGATION_TRANSFER_SIZE, ATTR_NAVIGATION_TYPE, ATTR_NAVIGATION_UNLOAD_EVENT_END, ATTR_NAVIGATION_UNLOAD_EVENT_START, ATTR_NAVIGATION_URL, NAVIGATION_TIMING_EVENT_NAME } from "./semconv.js";
2
+ import { SeverityNumber } from "@opentelemetry/api-logs";
3
+ import { InstrumentationBase } from "@opentelemetry/instrumentation";
4
+ //#region src/navigation-timing/instrumentation.ts
5
+ const BASE_DELAY_MS = 50;
6
+ const MAX_RETRIES = 5;
7
+ /**
8
+ * This class automatically instruments navigation timing within the browser.
9
+ */
10
+ var NavigationTimingInstrumentation = class extends InstrumentationBase {
11
+ _lastEntry;
12
+ _didEmit = false;
13
+ _completeDelayTimeoutId;
14
+ _retryCount = 0;
15
+ _isEnabled = false;
16
+ _onLoad = () => {
17
+ this._tryEmitOrSchedule();
18
+ };
19
+ _onPageHide = () => {
20
+ this._handleUnload();
21
+ };
22
+ constructor(config = {}) {
23
+ super("@opentelemetry/instrumentation-navigation-timing", "0.1.0", config);
24
+ }
25
+ init() {
26
+ return [];
27
+ }
28
+ enable() {
29
+ if (this._isEnabled) return;
30
+ this._isEnabled = true;
31
+ this._tryEmitOrSchedule();
32
+ if (this._didEmit) return;
33
+ window.addEventListener("pagehide", this._onPageHide);
34
+ }
35
+ disable() {
36
+ this._isEnabled = false;
37
+ this._unsubscribeAll();
38
+ this._lastEntry = void 0;
39
+ this._didEmit = false;
40
+ this._retryCount = 0;
41
+ }
42
+ _getLatestNavigationEntry() {
43
+ const entries = performance?.getEntriesByType?.("navigation");
44
+ if (!entries || entries.length === 0) return;
45
+ return entries[entries.length - 1];
46
+ }
47
+ _calculateBackoffDelay() {
48
+ return this._retryCount * BASE_DELAY_MS;
49
+ }
50
+ /**
51
+ * Attempts to emit the navigation timing event.
52
+ *
53
+ * - Emits immediately if a complete `PerformanceNavigationTiming` entry is available.
54
+ * - If the page is still loading, waits for `window.load` and retries.
55
+ * - If the page is already loaded but the entry is not finalized yet, schedules one
56
+ * deferred re-check (to allow the browser to populate the timing fields).
57
+ *
58
+ * This method can be called multiple times (from `enable()`, the load handler, or the
59
+ * deferred timeout), so it must be safe to re-enter.
60
+ */
61
+ _tryEmitOrSchedule() {
62
+ if (this._didEmit) return;
63
+ const entry = this._getLatestNavigationEntry();
64
+ if (entry) this._lastEntry = entry;
65
+ if (entry && entry.loadEventEnd > 0) {
66
+ this._emitAndCleanup(entry);
67
+ return;
68
+ }
69
+ if (document.readyState !== "complete") {
70
+ window.addEventListener("load", this._onLoad, { once: true });
71
+ return;
72
+ }
73
+ if (this._completeDelayTimeoutId !== void 0 || this._retryCount > MAX_RETRIES) return;
74
+ else {
75
+ const delay = this._calculateBackoffDelay();
76
+ this._retryCount++;
77
+ this._completeDelayTimeoutId = window.setTimeout(() => {
78
+ this._completeDelayTimeoutId = void 0;
79
+ this._tryEmitOrSchedule();
80
+ }, delay);
81
+ }
82
+ }
83
+ _handleUnload() {
84
+ if (this._didEmit) return;
85
+ const entry = this._getLatestNavigationEntry() ?? this._lastEntry;
86
+ if (!entry) {
87
+ this._unsubscribeAll();
88
+ return;
89
+ }
90
+ this._emitAndCleanup(entry);
91
+ }
92
+ _emitAndCleanup(entry) {
93
+ if (this._didEmit) return;
94
+ this._didEmit = true;
95
+ this._emitNavigationTiming(entry);
96
+ this._lastEntry = void 0;
97
+ this._unsubscribeAll();
98
+ }
99
+ _emitNavigationTiming(entry) {
100
+ if (!entry) return;
101
+ this.logger.emit({
102
+ eventName: NAVIGATION_TIMING_EVENT_NAME,
103
+ severityNumber: SeverityNumber.INFO,
104
+ attributes: {
105
+ [ATTR_NAVIGATION_TYPE]: entry.type,
106
+ [ATTR_NAVIGATION_URL]: entry.name,
107
+ [ATTR_NAVIGATION_DURATION]: entry.duration,
108
+ [ATTR_NAVIGATION_DOM_COMPLETE]: entry.domComplete,
109
+ [ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END]: entry.domContentLoadedEventEnd,
110
+ [ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START]: entry.domContentLoadedEventStart,
111
+ [ATTR_NAVIGATION_DOM_INTERACTIVE]: entry.domInteractive,
112
+ [ATTR_NAVIGATION_LOAD_EVENT_END]: entry.loadEventEnd,
113
+ [ATTR_NAVIGATION_LOAD_EVENT_START]: entry.loadEventStart,
114
+ [ATTR_NAVIGATION_REDIRECT_COUNT]: entry.redirectCount,
115
+ [ATTR_NAVIGATION_UNLOAD_EVENT_END]: entry.unloadEventEnd,
116
+ [ATTR_NAVIGATION_UNLOAD_EVENT_START]: entry.unloadEventStart,
117
+ [ATTR_NAVIGATION_FETCH_START]: entry.fetchStart,
118
+ [ATTR_NAVIGATION_DOMAIN_LOOKUP_START]: entry.domainLookupStart,
119
+ [ATTR_NAVIGATION_DOMAIN_LOOKUP_END]: entry.domainLookupEnd,
120
+ [ATTR_NAVIGATION_CONNECT_START]: entry.connectStart,
121
+ [ATTR_NAVIGATION_CONNECT_END]: entry.connectEnd,
122
+ [ATTR_NAVIGATION_SECURE_CONNECTION_START]: entry.secureConnectionStart,
123
+ [ATTR_NAVIGATION_REQUEST_START]: entry.requestStart,
124
+ [ATTR_NAVIGATION_RESPONSE_START]: entry.responseStart,
125
+ [ATTR_NAVIGATION_RESPONSE_END]: entry.responseEnd,
126
+ [ATTR_NAVIGATION_TRANSFER_SIZE]: entry.transferSize,
127
+ [ATTR_NAVIGATION_ENCODED_BODY_SIZE]: entry.encodedBodySize,
128
+ [ATTR_NAVIGATION_DECODED_BODY_SIZE]: entry.decodedBodySize
129
+ }
130
+ });
131
+ }
132
+ _unsubscribeAll() {
133
+ if (this._completeDelayTimeoutId) {
134
+ clearTimeout(this._completeDelayTimeoutId);
135
+ this._completeDelayTimeoutId = void 0;
136
+ }
137
+ window.removeEventListener("load", this._onLoad);
138
+ window.removeEventListener("pagehide", this._onPageHide);
139
+ }
140
+ };
141
+ //#endregion
142
+ export { NavigationTimingInstrumentation };
143
+
144
+ //# sourceMappingURL=instrumentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/navigation-timing/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport {\n ATTR_NAVIGATION_CONNECT_END,\n ATTR_NAVIGATION_CONNECT_START,\n ATTR_NAVIGATION_DECODED_BODY_SIZE,\n ATTR_NAVIGATION_DOM_COMPLETE,\n ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END,\n ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START,\n ATTR_NAVIGATION_DOM_INTERACTIVE,\n ATTR_NAVIGATION_DOMAIN_LOOKUP_END,\n ATTR_NAVIGATION_DOMAIN_LOOKUP_START,\n ATTR_NAVIGATION_DURATION,\n ATTR_NAVIGATION_ENCODED_BODY_SIZE,\n ATTR_NAVIGATION_FETCH_START,\n ATTR_NAVIGATION_LOAD_EVENT_END,\n ATTR_NAVIGATION_LOAD_EVENT_START,\n ATTR_NAVIGATION_REDIRECT_COUNT,\n ATTR_NAVIGATION_REQUEST_START,\n ATTR_NAVIGATION_RESPONSE_END,\n ATTR_NAVIGATION_RESPONSE_START,\n ATTR_NAVIGATION_SECURE_CONNECTION_START,\n ATTR_NAVIGATION_TRANSFER_SIZE,\n ATTR_NAVIGATION_TYPE,\n ATTR_NAVIGATION_UNLOAD_EVENT_END,\n ATTR_NAVIGATION_UNLOAD_EVENT_START,\n ATTR_NAVIGATION_URL,\n NAVIGATION_TIMING_EVENT_NAME,\n} from './semconv.ts';\nimport type { NavigationTimingInstrumentationConfig } from './types.ts';\n\nconst BASE_DELAY_MS = 50;\nconst MAX_RETRIES = 5;\n\n/**\n * This class automatically instruments navigation timing within the browser.\n */\nexport class NavigationTimingInstrumentation extends InstrumentationBase<NavigationTimingInstrumentationConfig> {\n private _lastEntry?: PerformanceNavigationTiming;\n private _didEmit = false;\n private _completeDelayTimeoutId?: number;\n private _retryCount = 0;\n private _isEnabled = false;\n\n private _onLoad = () => {\n this._tryEmitOrSchedule();\n };\n\n private _onPageHide = () => {\n this._handleUnload();\n };\n\n constructor(config: NavigationTimingInstrumentationConfig = {}) {\n super('@opentelemetry/instrumentation-navigation-timing', '0.1.0', 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 // Try emitting immediately (e.g. when enabled after load),\n // otherwise schedule for `load` or fall back to unload.\n this._tryEmitOrSchedule();\n if (this._didEmit) {\n return;\n }\n\n window.addEventListener('pagehide', this._onPageHide);\n }\n\n override disable(): void {\n this._isEnabled = false;\n this._unsubscribeAll();\n this._lastEntry = undefined;\n this._didEmit = false;\n this._retryCount = 0;\n }\n\n private _getLatestNavigationEntry(): PerformanceNavigationTiming | undefined {\n const entries = performance?.getEntriesByType?.('navigation') as\n | PerformanceNavigationTiming[]\n | undefined;\n if (!entries || entries.length === 0) {\n return;\n }\n\n return entries[entries.length - 1];\n }\n\n private _calculateBackoffDelay(): number {\n return this._retryCount * BASE_DELAY_MS;\n }\n\n /**\n * Attempts to emit the navigation timing event.\n *\n * - Emits immediately if a complete `PerformanceNavigationTiming` entry is available.\n * - If the page is still loading, waits for `window.load` and retries.\n * - If the page is already loaded but the entry is not finalized yet, schedules one\n * deferred re-check (to allow the browser to populate the timing fields).\n *\n * This method can be called multiple times (from `enable()`, the load handler, or the\n * deferred timeout), so it must be safe to re-enter.\n */\n private _tryEmitOrSchedule(): void {\n if (this._didEmit) {\n return;\n }\n\n const entry = this._getLatestNavigationEntry();\n if (entry) {\n this._lastEntry = entry;\n }\n\n // Prefer emitting a \"complete\" navigation entry.\n if (entry && entry.loadEventEnd > 0) {\n this._emitAndCleanup(entry);\n return;\n }\n\n // If the document is still loading, wait for `load` and try again.\n if (document.readyState !== 'complete') {\n window.addEventListener('load', this._onLoad, { once: true });\n return;\n }\n\n // If the document is already complete but navigation timings are not finalized yet,\n // schedule a deferred re-check with linear backoff to allow the browser to finish\n // populating the entry.\n if (\n this._completeDelayTimeoutId !== undefined ||\n this._retryCount > MAX_RETRIES\n ) {\n return;\n } else {\n const delay = this._calculateBackoffDelay();\n this._retryCount++;\n\n this._completeDelayTimeoutId = window.setTimeout(() => {\n this._completeDelayTimeoutId = undefined;\n this._tryEmitOrSchedule();\n }, delay);\n }\n }\n\n private _handleUnload(): void {\n if (this._didEmit) {\n return;\n }\n\n const entry = this._getLatestNavigationEntry() ?? this._lastEntry;\n if (!entry) {\n this._unsubscribeAll();\n return;\n }\n\n // Emit even if partial (e.g. loadEventEnd === 0).\n this._emitAndCleanup(entry);\n }\n\n private _emitAndCleanup(entry: PerformanceNavigationTiming): void {\n if (this._didEmit) {\n return;\n }\n\n this._didEmit = true;\n\n this._emitNavigationTiming(entry);\n this._lastEntry = undefined;\n this._unsubscribeAll();\n }\n\n private _emitNavigationTiming(entry: PerformanceNavigationTiming) {\n if (!entry) {\n return;\n }\n\n this.logger.emit({\n eventName: NAVIGATION_TIMING_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_NAVIGATION_TYPE]: entry.type,\n [ATTR_NAVIGATION_URL]: entry.name,\n [ATTR_NAVIGATION_DURATION]: entry.duration,\n [ATTR_NAVIGATION_DOM_COMPLETE]: entry.domComplete,\n [ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END]:\n entry.domContentLoadedEventEnd,\n [ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START]:\n entry.domContentLoadedEventStart,\n [ATTR_NAVIGATION_DOM_INTERACTIVE]: entry.domInteractive,\n [ATTR_NAVIGATION_LOAD_EVENT_END]: entry.loadEventEnd,\n [ATTR_NAVIGATION_LOAD_EVENT_START]: entry.loadEventStart,\n [ATTR_NAVIGATION_REDIRECT_COUNT]: entry.redirectCount,\n [ATTR_NAVIGATION_UNLOAD_EVENT_END]: entry.unloadEventEnd,\n [ATTR_NAVIGATION_UNLOAD_EVENT_START]: entry.unloadEventStart,\n [ATTR_NAVIGATION_FETCH_START]: entry.fetchStart,\n [ATTR_NAVIGATION_DOMAIN_LOOKUP_START]: entry.domainLookupStart,\n [ATTR_NAVIGATION_DOMAIN_LOOKUP_END]: entry.domainLookupEnd,\n [ATTR_NAVIGATION_CONNECT_START]: entry.connectStart,\n [ATTR_NAVIGATION_CONNECT_END]: entry.connectEnd,\n [ATTR_NAVIGATION_SECURE_CONNECTION_START]: entry.secureConnectionStart,\n [ATTR_NAVIGATION_REQUEST_START]: entry.requestStart,\n [ATTR_NAVIGATION_RESPONSE_START]: entry.responseStart,\n [ATTR_NAVIGATION_RESPONSE_END]: entry.responseEnd,\n [ATTR_NAVIGATION_TRANSFER_SIZE]: entry.transferSize,\n [ATTR_NAVIGATION_ENCODED_BODY_SIZE]: entry.encodedBodySize,\n [ATTR_NAVIGATION_DECODED_BODY_SIZE]: entry.decodedBodySize,\n },\n });\n }\n\n private _unsubscribeAll(): void {\n if (this._completeDelayTimeoutId) {\n clearTimeout(this._completeDelayTimeoutId);\n this._completeDelayTimeoutId = undefined;\n }\n\n window.removeEventListener('load', this._onLoad);\n window.removeEventListener('pagehide', this._onPageHide);\n }\n}\n"],"mappings":";;;;AAoCA,MAAM,gBAAgB;AACtB,MAAM,cAAc;;;;AAKpB,IAAa,kCAAb,cAAqD,oBAA2D;CAC9G;CACA,WAAmB;CACnB;CACA,cAAsB;CACtB,aAAqB;CAErB,gBAAwB;AACtB,OAAK,oBAAoB;;CAG3B,oBAA4B;AAC1B,OAAK,eAAe;;CAGtB,YAAY,SAAgD,EAAE,EAAE;AAC9D,QAAM,oDAAoD,SAAS,OAAO;;CAG5E,OAA0B;AACxB,SAAO,EAAE;;CAGX,SAAwB;AACtB,MAAI,KAAK,WACP;AAEF,OAAK,aAAa;AAIlB,OAAK,oBAAoB;AACzB,MAAI,KAAK,SACP;AAGF,SAAO,iBAAiB,YAAY,KAAK,YAAY;;CAGvD,UAAyB;AACvB,OAAK,aAAa;AAClB,OAAK,iBAAiB;AACtB,OAAK,aAAa,KAAA;AAClB,OAAK,WAAW;AAChB,OAAK,cAAc;;CAGrB,4BAA6E;EAC3E,MAAM,UAAU,aAAa,mBAAmB,aAAa;AAG7D,MAAI,CAAC,WAAW,QAAQ,WAAW,EACjC;AAGF,SAAO,QAAQ,QAAQ,SAAS;;CAGlC,yBAAyC;AACvC,SAAO,KAAK,cAAc;;;;;;;;;;;;;CAc5B,qBAAmC;AACjC,MAAI,KAAK,SACP;EAGF,MAAM,QAAQ,KAAK,2BAA2B;AAC9C,MAAI,MACF,MAAK,aAAa;AAIpB,MAAI,SAAS,MAAM,eAAe,GAAG;AACnC,QAAK,gBAAgB,MAAM;AAC3B;;AAIF,MAAI,SAAS,eAAe,YAAY;AACtC,UAAO,iBAAiB,QAAQ,KAAK,SAAS,EAAE,MAAM,MAAM,CAAC;AAC7D;;AAMF,MACE,KAAK,4BAA4B,KAAA,KACjC,KAAK,cAAc,YAEnB;OACK;GACL,MAAM,QAAQ,KAAK,wBAAwB;AAC3C,QAAK;AAEL,QAAK,0BAA0B,OAAO,iBAAiB;AACrD,SAAK,0BAA0B,KAAA;AAC/B,SAAK,oBAAoB;MACxB,MAAM;;;CAIb,gBAA8B;AAC5B,MAAI,KAAK,SACP;EAGF,MAAM,QAAQ,KAAK,2BAA2B,IAAI,KAAK;AACvD,MAAI,CAAC,OAAO;AACV,QAAK,iBAAiB;AACtB;;AAIF,OAAK,gBAAgB,MAAM;;CAG7B,gBAAwB,OAA0C;AAChE,MAAI,KAAK,SACP;AAGF,OAAK,WAAW;AAEhB,OAAK,sBAAsB,MAAM;AACjC,OAAK,aAAa,KAAA;AAClB,OAAK,iBAAiB;;CAGxB,sBAA8B,OAAoC;AAChE,MAAI,CAAC,MACH;AAGF,OAAK,OAAO,KAAK;GACf,WAAW;GACX,gBAAgB,eAAe;GAC/B,YAAY;KACT,uBAAuB,MAAM;KAC7B,sBAAsB,MAAM;KAC5B,2BAA2B,MAAM;KACjC,+BAA+B,MAAM;KACrC,+CACC,MAAM;KACP,iDACC,MAAM;KACP,kCAAkC,MAAM;KACxC,iCAAiC,MAAM;KACvC,mCAAmC,MAAM;KACzC,iCAAiC,MAAM;KACvC,mCAAmC,MAAM;KACzC,qCAAqC,MAAM;KAC3C,8BAA8B,MAAM;KACpC,sCAAsC,MAAM;KAC5C,oCAAoC,MAAM;KAC1C,gCAAgC,MAAM;KACtC,8BAA8B,MAAM;KACpC,0CAA0C,MAAM;KAChD,gCAAgC,MAAM;KACtC,iCAAiC,MAAM;KACvC,+BAA+B,MAAM;KACrC,gCAAgC,MAAM;KACtC,oCAAoC,MAAM;KAC1C,oCAAoC,MAAM;IAC5C;GACF,CAAC;;CAGJ,kBAAgC;AAC9B,MAAI,KAAK,yBAAyB;AAChC,gBAAa,KAAK,wBAAwB;AAC1C,QAAK,0BAA0B,KAAA;;AAGjC,SAAO,oBAAoB,QAAQ,KAAK,QAAQ;AAChD,SAAO,oBAAoB,YAAY,KAAK,YAAY"}
@@ -0,0 +1,102 @@
1
+ //#region src/navigation-timing/semconv.ts
2
+ const NAVIGATION_TIMING_EVENT_NAME = "browser.navigation_timing";
3
+ /**
4
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
5
+ */
6
+ const ATTR_NAVIGATION_TYPE = "navigation.type";
7
+ /**
8
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
9
+ */
10
+ const ATTR_NAVIGATION_URL = "navigation.url";
11
+ /**
12
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
13
+ */
14
+ const ATTR_NAVIGATION_DURATION = "navigation.duration";
15
+ /**
16
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
17
+ */
18
+ const ATTR_NAVIGATION_DOM_COMPLETE = "navigation.dom_complete";
19
+ /**
20
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
21
+ */
22
+ const ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END = "navigation.dom_content_loaded_event_end";
23
+ /**
24
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
25
+ */
26
+ const ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START = "navigation.dom_content_loaded_event_start";
27
+ /**
28
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
29
+ */
30
+ const ATTR_NAVIGATION_DOM_INTERACTIVE = "navigation.dom_interactive";
31
+ /**
32
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
33
+ */
34
+ const ATTR_NAVIGATION_LOAD_EVENT_END = "navigation.load_event_end";
35
+ /**
36
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
37
+ */
38
+ const ATTR_NAVIGATION_LOAD_EVENT_START = "navigation.load_event_start";
39
+ /**
40
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
41
+ */
42
+ const ATTR_NAVIGATION_REDIRECT_COUNT = "navigation.redirect_count";
43
+ /**
44
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
45
+ */
46
+ const ATTR_NAVIGATION_UNLOAD_EVENT_END = "navigation.unload_event_end";
47
+ /**
48
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
49
+ */
50
+ const ATTR_NAVIGATION_UNLOAD_EVENT_START = "navigation.unload_event_start";
51
+ /**
52
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
53
+ */
54
+ const ATTR_NAVIGATION_FETCH_START = "navigation.fetch_start";
55
+ /**
56
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
57
+ */
58
+ const ATTR_NAVIGATION_DOMAIN_LOOKUP_START = "navigation.domain_lookup_start";
59
+ /**
60
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
61
+ */
62
+ const ATTR_NAVIGATION_DOMAIN_LOOKUP_END = "navigation.domain_lookup_end";
63
+ /**
64
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
65
+ */
66
+ const ATTR_NAVIGATION_CONNECT_START = "navigation.connect_start";
67
+ /**
68
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
69
+ */
70
+ const ATTR_NAVIGATION_CONNECT_END = "navigation.connect_end";
71
+ /**
72
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
73
+ */
74
+ const ATTR_NAVIGATION_SECURE_CONNECTION_START = "navigation.secure_connection_start";
75
+ /**
76
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
77
+ */
78
+ const ATTR_NAVIGATION_REQUEST_START = "navigation.request_start";
79
+ /**
80
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
81
+ */
82
+ const ATTR_NAVIGATION_RESPONSE_START = "navigation.response_start";
83
+ /**
84
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
85
+ */
86
+ const ATTR_NAVIGATION_RESPONSE_END = "navigation.response_end";
87
+ /**
88
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
89
+ */
90
+ const ATTR_NAVIGATION_TRANSFER_SIZE = "navigation.transfer_size";
91
+ /**
92
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
93
+ */
94
+ const ATTR_NAVIGATION_ENCODED_BODY_SIZE = "navigation.encoded_body_size";
95
+ /**
96
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
97
+ */
98
+ const ATTR_NAVIGATION_DECODED_BODY_SIZE = "navigation.decoded_body_size";
99
+ //#endregion
100
+ export { ATTR_NAVIGATION_CONNECT_END, ATTR_NAVIGATION_CONNECT_START, ATTR_NAVIGATION_DECODED_BODY_SIZE, ATTR_NAVIGATION_DOMAIN_LOOKUP_END, ATTR_NAVIGATION_DOMAIN_LOOKUP_START, ATTR_NAVIGATION_DOM_COMPLETE, ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END, ATTR_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START, ATTR_NAVIGATION_DOM_INTERACTIVE, ATTR_NAVIGATION_DURATION, ATTR_NAVIGATION_ENCODED_BODY_SIZE, ATTR_NAVIGATION_FETCH_START, ATTR_NAVIGATION_LOAD_EVENT_END, ATTR_NAVIGATION_LOAD_EVENT_START, ATTR_NAVIGATION_REDIRECT_COUNT, ATTR_NAVIGATION_REQUEST_START, ATTR_NAVIGATION_RESPONSE_END, ATTR_NAVIGATION_RESPONSE_START, ATTR_NAVIGATION_SECURE_CONNECTION_START, ATTR_NAVIGATION_TRANSFER_SIZE, ATTR_NAVIGATION_TYPE, ATTR_NAVIGATION_UNLOAD_EVENT_END, ATTR_NAVIGATION_UNLOAD_EVENT_START, ATTR_NAVIGATION_URL, NAVIGATION_TIMING_EVENT_NAME };
101
+
102
+ //# sourceMappingURL=semconv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semconv.js","names":[],"sources":["../../src/navigation-timing/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 NAVIGATION_TIMING_EVENT_NAME = 'browser.navigation_timing';\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_NAVIGATION_TYPE = 'navigation.type';\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_NAVIGATION_URL = 'navigation.url';\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_NAVIGATION_DURATION = 'navigation.duration';\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_NAVIGATION_DOM_COMPLETE = 'navigation.dom_complete';\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_NAVIGATION_DOM_CONTENT_LOADED_EVENT_END =\n 'navigation.dom_content_loaded_event_end';\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_NAVIGATION_DOM_CONTENT_LOADED_EVENT_START =\n 'navigation.dom_content_loaded_event_start';\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_NAVIGATION_DOM_INTERACTIVE = 'navigation.dom_interactive';\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_NAVIGATION_LOAD_EVENT_END = 'navigation.load_event_end';\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_NAVIGATION_LOAD_EVENT_START = 'navigation.load_event_start';\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_NAVIGATION_REDIRECT_COUNT = 'navigation.redirect_count';\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_NAVIGATION_UNLOAD_EVENT_END = 'navigation.unload_event_end';\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_NAVIGATION_UNLOAD_EVENT_START =\n 'navigation.unload_event_start';\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_NAVIGATION_FETCH_START = 'navigation.fetch_start';\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_NAVIGATION_DOMAIN_LOOKUP_START =\n 'navigation.domain_lookup_start';\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_NAVIGATION_DOMAIN_LOOKUP_END = 'navigation.domain_lookup_end';\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_NAVIGATION_CONNECT_START = 'navigation.connect_start';\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_NAVIGATION_CONNECT_END = 'navigation.connect_end';\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_NAVIGATION_SECURE_CONNECTION_START =\n 'navigation.secure_connection_start';\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_NAVIGATION_REQUEST_START = 'navigation.request_start';\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_NAVIGATION_RESPONSE_START = 'navigation.response_start';\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_NAVIGATION_RESPONSE_END = 'navigation.response_end';\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_NAVIGATION_TRANSFER_SIZE = 'navigation.transfer_size';\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_NAVIGATION_ENCODED_BODY_SIZE = 'navigation.encoded_body_size';\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_NAVIGATION_DECODED_BODY_SIZE = 'navigation.decoded_body_size';\n"],"mappings":";AAWA,MAAa,+BAA+B;;;;AAK5C,MAAa,uBAAuB;;;;AAKpC,MAAa,sBAAsB;;;;AAKnC,MAAa,2BAA2B;;;;AAKxC,MAAa,+BAA+B;;;;AAK5C,MAAa,+CACX;;;;AAKF,MAAa,iDACX;;;;AAKF,MAAa,kCAAkC;;;;AAK/C,MAAa,iCAAiC;;;;AAK9C,MAAa,mCAAmC;;;;AAKhD,MAAa,iCAAiC;;;;AAK9C,MAAa,mCAAmC;;;;AAKhD,MAAa,qCACX;;;;AAKF,MAAa,8BAA8B;;;;AAK3C,MAAa,sCACX;;;;AAKF,MAAa,oCAAoC;;;;AAKjD,MAAa,gCAAgC;;;;AAK7C,MAAa,8BAA8B;;;;AAK3C,MAAa,0CACX;;;;AAKF,MAAa,gCAAgC;;;;AAK7C,MAAa,iCAAiC;;;;AAK9C,MAAa,+BAA+B;;;;AAK5C,MAAa,gCAAgC;;;;AAK7C,MAAa,oCAAoC;;;;AAKjD,MAAa,oCAAoC"}
@@ -0,0 +1,10 @@
1
+ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
2
+
3
+ //#region src/navigation-timing/types.d.ts
4
+ /**
5
+ * NavigationTimingInstrumentation Configuration
6
+ */
7
+ interface NavigationTimingInstrumentationConfig extends InstrumentationConfig {}
8
+ //#endregion
9
+ export { NavigationTimingInstrumentationConfig };
10
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ import { UserActionInstrumentation } from "./instrumentation.js";
2
+ export { UserActionInstrumentation };
@@ -0,0 +1,2 @@
1
+ import { UserActionInstrumentation } from "./instrumentation.js";
2
+ export { UserActionInstrumentation };
@@ -0,0 +1,19 @@
1
+ import { UserActionInstrumentationConfig } from "./types.js";
2
+ import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
+
4
+ //#region src/user-action/instrumentation.d.ts
5
+ /**
6
+ * This class automatically instruments different User Actions within the browser.
7
+ */
8
+ declare class UserActionInstrumentation extends InstrumentationBase<UserActionInstrumentationConfig> {
9
+ private _onClickHandler?;
10
+ constructor(config?: UserActionInstrumentationConfig);
11
+ protected init(): never[];
12
+ private _getMouseButtonFromMouseEvent;
13
+ private onClick;
14
+ enable(): void;
15
+ disable(): void;
16
+ }
17
+ //#endregion
18
+ export { UserActionInstrumentation };
19
+ //# sourceMappingURL=instrumentation.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { getElementCSSSelector } from "../web-utils/dist/getElementCSSSelector.js";
2
+ import { ATTR_CSS_SELECTOR, ATTR_MOUSE_EVENT_BUTTON, ATTR_PAGE_X, ATTR_PAGE_Y, ATTR_TAGS, ATTR_TAG_NAME, CLICK_EVENT_NAME } from "./semconv.js";
3
+ import { SeverityNumber } from "@opentelemetry/api-logs";
4
+ import { InstrumentationBase } from "@opentelemetry/instrumentation";
5
+ //#region src/user-action/instrumentation.ts
6
+ const DEFAULT_AUTO_CAPTURED_ACTIONS = ["click"];
7
+ const OTEL_ELEMENT_ATTRIBUTE_PREFIX = "data-otel-";
8
+ /**
9
+ * This class automatically instruments different User Actions within the browser.
10
+ */
11
+ var UserActionInstrumentation = class extends InstrumentationBase {
12
+ constructor(config = {}) {
13
+ super("@opentelemetry/instrumentation-user-action", "0.1.0", config);
14
+ }
15
+ init() {
16
+ return [];
17
+ }
18
+ _getMouseButtonFromMouseEvent(event) {
19
+ switch (event.button) {
20
+ case 0: return "left";
21
+ case 1: return "middle";
22
+ case 2: return "right";
23
+ default: return "left";
24
+ }
25
+ }
26
+ onClick(event) {
27
+ const element = event.target;
28
+ if (!(element instanceof HTMLElement)) return;
29
+ if (element.hasAttribute("disabled")) return;
30
+ const cssSelector = getElementCSSSelector(element, {
31
+ useIdForTargetElement: true,
32
+ useIdForAncestors: true
33
+ });
34
+ const otelPrefixedAttributes = {};
35
+ for (const attr of element.attributes) if (attr.name.startsWith(OTEL_ELEMENT_ATTRIBUTE_PREFIX)) otelPrefixedAttributes[attr.name.slice(10)] = attr.value;
36
+ this.logger.emit({
37
+ severityNumber: SeverityNumber.INFO,
38
+ eventName: CLICK_EVENT_NAME,
39
+ attributes: {
40
+ [ATTR_PAGE_X]: event.pageX,
41
+ [ATTR_PAGE_Y]: event.pageY,
42
+ [ATTR_TAG_NAME]: element.tagName,
43
+ [ATTR_TAGS]: otelPrefixedAttributes,
44
+ [ATTR_MOUSE_EVENT_BUTTON]: this._getMouseButtonFromMouseEvent(event),
45
+ [ATTR_CSS_SELECTOR]: cssSelector
46
+ }
47
+ });
48
+ }
49
+ enable() {
50
+ const autoCapturedActions = this._config.autoCapturedActions ?? DEFAULT_AUTO_CAPTURED_ACTIONS;
51
+ if (!this._onClickHandler) this._onClickHandler = this.onClick.bind(this);
52
+ if (autoCapturedActions.includes("click")) document.addEventListener("click", this._onClickHandler, true);
53
+ }
54
+ disable() {
55
+ if (this._onClickHandler) document.removeEventListener("click", this._onClickHandler, true);
56
+ }
57
+ };
58
+ //#endregion
59
+ export { UserActionInstrumentation };
60
+
61
+ //# sourceMappingURL=instrumentation.js.map
@@ -0,0 +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 '@opentelemetry/web-utils';\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('@opentelemetry/instrumentation-user-action', '0.1.0', config);\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":";;;;;AAuBA,MAAM,gCAA0D,CAAC,QAAQ;AACzE,MAAM,gCAAgC;;;;AAKtC,IAAa,4BAAb,cAA+C,oBAAqD;CAGlG,YAAY,SAA0C,EAAE,EAAE;AACxD,QAAM,8CAA8C,SAAS,OAAO;;CAGtE,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"}
@@ -0,0 +1,42 @@
1
+ //#region src/user-action/semconv.ts
2
+ const CLICK_EVENT_NAME = "browser.user_action.click";
3
+ /**
4
+ * @example left
5
+ *
6
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
7
+ */
8
+ const ATTR_MOUSE_EVENT_BUTTON = "browser.mouse_event.button";
9
+ /**
10
+ * @example 10
11
+ *
12
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
13
+ */
14
+ const ATTR_PAGE_X = "browser.page.x";
15
+ /**
16
+ * @example 10
17
+ *
18
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
19
+ */
20
+ const ATTR_PAGE_Y = "browser.page.y";
21
+ /**
22
+ * @example "BUTTON"
23
+ *
24
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
25
+ */
26
+ const ATTR_TAG_NAME = "browser.tag_name";
27
+ /**
28
+ * @example {"id": "123", "name": "Name"}
29
+ *
30
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
31
+ */
32
+ const ATTR_TAGS = "browser.element.attributes";
33
+ /**
34
+ * @example "#main > div:nth-child(2) > button.submit"
35
+ *
36
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
37
+ */
38
+ const ATTR_CSS_SELECTOR = "browser.css_selector";
39
+ //#endregion
40
+ export { ATTR_CSS_SELECTOR, ATTR_MOUSE_EVENT_BUTTON, ATTR_PAGE_X, ATTR_PAGE_Y, ATTR_TAGS, ATTR_TAG_NAME, CLICK_EVENT_NAME };
41
+
42
+ //# sourceMappingURL=semconv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semconv.js","names":[],"sources":["../../src/user-action/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 CLICK_EVENT_NAME = 'browser.user_action.click';\n\n/**\n * @example left\n *\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_MOUSE_EVENT_BUTTON = 'browser.mouse_event.button';\n\n/**\n * @example 10\n *\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_PAGE_X = 'browser.page.x';\n\n/**\n * @example 10\n *\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_PAGE_Y = 'browser.page.y';\n\n/**\n * @example \"BUTTON\"\n *\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_TAG_NAME = 'browser.tag_name';\n\n/**\n * @example {\"id\": \"123\", \"name\": \"Name\"}\n *\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_TAGS = 'browser.element.attributes';\n\n/**\n * @example \"#main > div:nth-child(2) > button.submit\"\n *\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_CSS_SELECTOR = 'browser.css_selector';\n"],"mappings":";AAWA,MAAa,mBAAmB;;;;;;AAOhC,MAAa,0BAA0B;;;;;;AAOvC,MAAa,cAAc;;;;;;AAO3B,MAAa,cAAc;;;;;;AAO3B,MAAa,gBAAgB;;;;;;AAO7B,MAAa,YAAY;;;;;;AAOzB,MAAa,oBAAoB"}
@@ -0,0 +1,13 @@
1
+ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
2
+
3
+ //#region src/user-action/types.d.ts
4
+ type AutoCapturedUserAction = 'click';
5
+ /**
6
+ * UserActionInstrumentation Configuration
7
+ */
8
+ interface UserActionInstrumentationConfig extends InstrumentationConfig {
9
+ autoCapturedActions?: AutoCapturedUserAction[];
10
+ }
11
+ //#endregion
12
+ export { UserActionInstrumentationConfig };
13
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,43 @@
1
+ //#region ../web-utils/dist/getElementCSSSelector.js
2
+ /**
3
+ * Generates the CSS selector of a given element in the DOM tree.
4
+ *
5
+ * @example #main > div:nth-child(2) > button.submit
6
+ * @example #unique-id
7
+ */
8
+ const getElementCSSSelector = (element, { useIdForTargetElement = false, useIdForAncestors = false } = {}) => {
9
+ if (element.nodeType === Node.DOCUMENT_NODE) return "";
10
+ const htmlElement = element;
11
+ const nodeValue = getNodeSelector(htmlElement, useIdForTargetElement || useIdForAncestors);
12
+ if (nodeValue.startsWith("#")) return nodeValue;
13
+ const parent = htmlElement.parentElement;
14
+ const parentSelector = parent ? getElementCSSSelector(parent, {
15
+ useIdForAncestors,
16
+ useIdForTargetElement: false
17
+ }) : "";
18
+ return parentSelector ? `${parentSelector} > ${nodeValue}` : nodeValue;
19
+ };
20
+ const getNodeSelector = (element, useElementId = false) => {
21
+ if (element.nodeType !== Node.ELEMENT_NODE) return "";
22
+ const htmlElement = element;
23
+ const id = htmlElement.getAttribute("id");
24
+ if (useElementId && id) {
25
+ if (htmlElement.ownerDocument.querySelectorAll(`#${CSS.escape(id)}`).length === 1) return `#${CSS.escape(id)}`;
26
+ }
27
+ let selector = getFullClassSelector(htmlElement);
28
+ const index = getNthChild(htmlElement);
29
+ if (index > 0) selector += `:nth-child(${index})`;
30
+ return selector;
31
+ };
32
+ const getNthChild = (element) => {
33
+ if (!element.parentElement) return 0;
34
+ const selector = getFullClassSelector(element);
35
+ const siblings = Array.from(element.parentElement.children).filter((sibling) => getFullClassSelector(sibling) === selector);
36
+ if (siblings.length > 1) return siblings.indexOf(element) + 1;
37
+ return 0;
38
+ };
39
+ const getFullClassSelector = (element) => element.localName + (element.classList.length > 0 ? Array.from(element.classList).map((cls) => `.${CSS.escape(cls)}`).join("") : "");
40
+ //#endregion
41
+ export { getElementCSSSelector };
42
+
43
+ //# sourceMappingURL=getElementCSSSelector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getElementCSSSelector.js","names":[],"sources":["../../../../web-utils/dist/getElementCSSSelector.js"],"sourcesContent":["//#region src/getElementCSSSelector.ts\n/**\n* Generates the CSS selector of a given element in the DOM tree.\n*\n* @example #main > div:nth-child(2) > button.submit\n* @example #unique-id\n*/\nconst getElementCSSSelector = (element, { useIdForTargetElement = false, useIdForAncestors = false } = {}) => {\n\tif (element.nodeType === Node.DOCUMENT_NODE) return \"\";\n\tconst htmlElement = element;\n\tconst nodeValue = getNodeSelector(htmlElement, useIdForTargetElement || useIdForAncestors);\n\tif (nodeValue.startsWith(\"#\")) return nodeValue;\n\tconst parent = htmlElement.parentElement;\n\tconst parentSelector = parent ? getElementCSSSelector(parent, {\n\t\tuseIdForAncestors,\n\t\tuseIdForTargetElement: false\n\t}) : \"\";\n\treturn parentSelector ? `${parentSelector} > ${nodeValue}` : nodeValue;\n};\nconst getNodeSelector = (element, useElementId = false) => {\n\tif (element.nodeType !== Node.ELEMENT_NODE) return \"\";\n\tconst htmlElement = element;\n\tconst id = htmlElement.getAttribute(\"id\");\n\tif (useElementId && id) {\n\t\tif (htmlElement.ownerDocument.querySelectorAll(`#${CSS.escape(id)}`).length === 1) return `#${CSS.escape(id)}`;\n\t}\n\tlet selector = getFullClassSelector(htmlElement);\n\tconst index = getNthChild(htmlElement);\n\tif (index > 0) selector += `:nth-child(${index})`;\n\treturn selector;\n};\nconst getNthChild = (element) => {\n\tif (!element.parentElement) return 0;\n\tconst selector = getFullClassSelector(element);\n\tconst siblings = Array.from(element.parentElement.children).filter((sibling) => getFullClassSelector(sibling) === selector);\n\tif (siblings.length > 1) return siblings.indexOf(element) + 1;\n\treturn 0;\n};\nconst getFullClassSelector = (element) => element.localName + (element.classList.length > 0 ? Array.from(element.classList).map((cls) => `.${CSS.escape(cls)}`).join(\"\") : \"\");\n//#endregion\nexport { getElementCSSSelector };\n\n//# sourceMappingURL=getElementCSSSelector.js.map"],"mappings":";;;;;;;AAOA,MAAM,yBAAyB,SAAS,EAAE,wBAAwB,OAAO,oBAAoB,UAAU,EAAE,KAAK;AAC7G,KAAI,QAAQ,aAAa,KAAK,cAAe,QAAO;CACpD,MAAM,cAAc;CACpB,MAAM,YAAY,gBAAgB,aAAa,yBAAyB,kBAAkB;AAC1F,KAAI,UAAU,WAAW,IAAI,CAAE,QAAO;CACtC,MAAM,SAAS,YAAY;CAC3B,MAAM,iBAAiB,SAAS,sBAAsB,QAAQ;EAC7D;EACA,uBAAuB;EACvB,CAAC,GAAG;AACL,QAAO,iBAAiB,GAAG,eAAe,KAAK,cAAc;;AAE9D,MAAM,mBAAmB,SAAS,eAAe,UAAU;AAC1D,KAAI,QAAQ,aAAa,KAAK,aAAc,QAAO;CACnD,MAAM,cAAc;CACpB,MAAM,KAAK,YAAY,aAAa,KAAK;AACzC,KAAI,gBAAgB;MACf,YAAY,cAAc,iBAAiB,IAAI,IAAI,OAAO,GAAG,GAAG,CAAC,WAAW,EAAG,QAAO,IAAI,IAAI,OAAO,GAAG;;CAE7G,IAAI,WAAW,qBAAqB,YAAY;CAChD,MAAM,QAAQ,YAAY,YAAY;AACtC,KAAI,QAAQ,EAAG,aAAY,cAAc,MAAM;AAC/C,QAAO;;AAER,MAAM,eAAe,YAAY;AAChC,KAAI,CAAC,QAAQ,cAAe,QAAO;CACnC,MAAM,WAAW,qBAAqB,QAAQ;CAC9C,MAAM,WAAW,MAAM,KAAK,QAAQ,cAAc,SAAS,CAAC,QAAQ,YAAY,qBAAqB,QAAQ,KAAK,SAAS;AAC3H,KAAI,SAAS,SAAS,EAAG,QAAO,SAAS,QAAQ,QAAQ,GAAG;AAC5D,QAAO;;AAER,MAAM,wBAAwB,YAAY,QAAQ,aAAa,QAAQ,UAAU,SAAS,IAAI,MAAM,KAAK,QAAQ,UAAU,CAAC,KAAK,QAAQ,IAAI,IAAI,OAAO,IAAI,GAAG,CAAC,KAAK,GAAG,GAAG"}
@@ -0,0 +1,3 @@
1
+ import { WebVitalsInstrumentationConfig } from "./types.js";
2
+ import { WebVitalsInstrumentation } from "./instrumentation.js";
3
+ export { WebVitalsInstrumentation, type WebVitalsInstrumentationConfig };
@@ -0,0 +1,2 @@
1
+ import { WebVitalsInstrumentation } from "./instrumentation.js";
2
+ export { WebVitalsInstrumentation };
@@ -0,0 +1,40 @@
1
+ import { WebVitalsInstrumentationConfig } from "./types.js";
2
+ import { InstrumentationBase } from "@opentelemetry/instrumentation";
3
+
4
+ //#region src/web-vitals/instrumentation.d.ts
5
+ /**
6
+ * Instrumentation for Core Web Vitals using the `web-vitals` library.
7
+ * https://github.com/GoogleChrome/web-vitals
8
+ *
9
+ * Note: The `web-vitals` library does not support removing listeners once
10
+ * registered. Calling `disable()` will stop emitting logs, but the underlying
11
+ * listeners remain active. Calling `enable()` again will resume emission.
12
+ */
13
+ declare class WebVitalsInstrumentation extends InstrumentationBase<WebVitalsInstrumentationConfig> {
14
+ private _isEnabled;
15
+ private _listenersRegistered;
16
+ private _applyCustomLogRecordData?;
17
+ private _includeRawAttribution;
18
+ constructor(config?: WebVitalsInstrumentationConfig);
19
+ protected init(): never[];
20
+ /**
21
+ * Enables the instrumentation and registers web-vitals listeners.
22
+ * Listeners are registered only once. If disabled, subsequent calls resume emission.
23
+ */
24
+ enable(): void;
25
+ /**
26
+ * Disables the instrumentation, pausing log emission.
27
+ * Listeners remain active due to web-vitals library limitations.
28
+ */
29
+ disable(): void;
30
+ /**
31
+ * Gets the timestamp for a metric based on attribution timing.
32
+ * Returns undefined to let OTel use the current time for metrics without
33
+ * specific timing information.
34
+ */
35
+ private _getTimestampForMetric;
36
+ private _emitWebVital;
37
+ }
38
+ //#endregion
39
+ export { WebVitalsInstrumentation };
40
+ //# sourceMappingURL=instrumentation.d.ts.map
@@ -0,0 +1,99 @@
1
+ import { ATTR_WEB_VITAL_DELTA, ATTR_WEB_VITAL_ID, ATTR_WEB_VITAL_NAME, ATTR_WEB_VITAL_NAVIGATION_TYPE, ATTR_WEB_VITAL_RATING, ATTR_WEB_VITAL_VALUE, WEB_VITAL_EVENT_NAME } from "./semconv.js";
2
+ import { SeverityNumber } from "@opentelemetry/api-logs";
3
+ import { InstrumentationBase, safeExecuteInTheMiddle } from "@opentelemetry/instrumentation";
4
+ import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals/attribution";
5
+ //#region src/web-vitals/instrumentation.ts
6
+ /**
7
+ * Instrumentation for Core Web Vitals using the `web-vitals` library.
8
+ * https://github.com/GoogleChrome/web-vitals
9
+ *
10
+ * Note: The `web-vitals` library does not support removing listeners once
11
+ * registered. Calling `disable()` will stop emitting logs, but the underlying
12
+ * listeners remain active. Calling `enable()` again will resume emission.
13
+ */
14
+ var WebVitalsInstrumentation = class extends InstrumentationBase {
15
+ _applyCustomLogRecordData;
16
+ _includeRawAttribution;
17
+ constructor(config = {}) {
18
+ super("@opentelemetry/instrumentation-web-vitals", "0.1.0", config);
19
+ this._applyCustomLogRecordData = config.applyCustomLogRecordData;
20
+ this._includeRawAttribution = config.includeRawAttribution ?? false;
21
+ }
22
+ init() {
23
+ return [];
24
+ }
25
+ /**
26
+ * Enables the instrumentation and registers web-vitals listeners.
27
+ * Listeners are registered only once. If disabled, subsequent calls resume emission.
28
+ */
29
+ enable() {
30
+ if (typeof PerformanceObserver === "undefined") {
31
+ this._diag.debug("PerformanceObserver not supported, web vitals will not be collected");
32
+ return;
33
+ }
34
+ this._isEnabled = true;
35
+ if (this._listenersRegistered) {
36
+ this._diag.debug("Listeners already registered, resuming emission");
37
+ return;
38
+ }
39
+ this._listenersRegistered = true;
40
+ this._diag.debug(`Registering listeners`);
41
+ onCLS((metric) => this._emitWebVital(metric));
42
+ onINP((metric) => this._emitWebVital(metric));
43
+ onLCP((metric) => this._emitWebVital(metric));
44
+ onFCP((metric) => this._emitWebVital(metric));
45
+ onTTFB((metric) => this._emitWebVital(metric));
46
+ }
47
+ /**
48
+ * Disables the instrumentation, pausing log emission.
49
+ * Listeners remain active due to web-vitals library limitations.
50
+ */
51
+ disable() {
52
+ this._isEnabled = false;
53
+ this._diag.debug("Instrumentation disabled, pausing emission");
54
+ }
55
+ /**
56
+ * Gets the timestamp for a metric based on attribution timing.
57
+ * Returns undefined to let OTel use the current time for metrics without
58
+ * specific timing information.
59
+ */
60
+ _getTimestampForMetric(metric) {
61
+ if (metric.name === "CLS") {
62
+ const { attribution } = metric;
63
+ if (attribution.largestShiftTime !== void 0) return attribution.largestShiftTime;
64
+ return;
65
+ }
66
+ if (metric.name === "INP") {
67
+ const { attribution } = metric;
68
+ return attribution.interactionTime;
69
+ }
70
+ return metric.value;
71
+ }
72
+ _emitWebVital(metric) {
73
+ if (!this._isEnabled) return;
74
+ const attributes = {
75
+ [ATTR_WEB_VITAL_NAME]: metric.name.toLowerCase(),
76
+ [ATTR_WEB_VITAL_VALUE]: metric.value,
77
+ [ATTR_WEB_VITAL_DELTA]: metric.delta,
78
+ [ATTR_WEB_VITAL_RATING]: metric.rating,
79
+ [ATTR_WEB_VITAL_ID]: metric.id,
80
+ [ATTR_WEB_VITAL_NAVIGATION_TYPE]: metric.navigationType
81
+ };
82
+ const timestamp = this._getTimestampForMetric(metric);
83
+ const logRecord = {
84
+ eventName: WEB_VITAL_EVENT_NAME,
85
+ severityNumber: SeverityNumber.INFO,
86
+ attributes,
87
+ ...this._includeRawAttribution ? { body: JSON.stringify(metric.attribution) } : {},
88
+ ...timestamp !== void 0 ? { timestamp } : {}
89
+ };
90
+ if (this._applyCustomLogRecordData) safeExecuteInTheMiddle(() => this._applyCustomLogRecordData?.(logRecord), (error) => {
91
+ if (error) this._diag.error("applyCustomLogRecordData hook failed", error);
92
+ }, true);
93
+ this.logger.emit(logRecord);
94
+ }
95
+ };
96
+ //#endregion
97
+ export { WebVitalsInstrumentation };
98
+
99
+ //# sourceMappingURL=instrumentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/web-vitals/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes } from '@opentelemetry/api';\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport type {\n CLSMetricWithAttribution,\n INPMetricWithAttribution,\n MetricWithAttribution,\n} from 'web-vitals/attribution';\nimport { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals/attribution';\nimport {\n ATTR_WEB_VITAL_DELTA,\n ATTR_WEB_VITAL_ID,\n ATTR_WEB_VITAL_NAME,\n ATTR_WEB_VITAL_NAVIGATION_TYPE,\n ATTR_WEB_VITAL_RATING,\n ATTR_WEB_VITAL_VALUE,\n WEB_VITAL_EVENT_NAME,\n} from './semconv.ts';\nimport type { WebVitalsInstrumentationConfig } from './types.ts';\n\n/**\n * Instrumentation for Core Web Vitals using the `web-vitals` library.\n * https://github.com/GoogleChrome/web-vitals\n *\n * Note: The `web-vitals` library does not support removing listeners once\n * registered. Calling `disable()` will stop emitting logs, but the underlying\n * listeners remain active. Calling `enable()` again will resume emission.\n */\nexport class WebVitalsInstrumentation extends InstrumentationBase<WebVitalsInstrumentationConfig> {\n // Using `declare` is required here: InstrumentationBase calls enable() during\n // construction, and standard field initialization would reset this flag after\n // super() returns, breaking the duplicate-registration guard.\n private declare _isEnabled: boolean;\n private declare _listenersRegistered: boolean;\n private _applyCustomLogRecordData?: (logRecord: LogRecord) => void;\n private _includeRawAttribution: boolean;\n\n constructor(config: WebVitalsInstrumentationConfig = {}) {\n super('@opentelemetry/instrumentation-web-vitals', '0.1.0', config);\n this._applyCustomLogRecordData = config.applyCustomLogRecordData;\n this._includeRawAttribution = config.includeRawAttribution ?? false;\n }\n\n protected override init() {\n return [];\n }\n\n /**\n * Enables the instrumentation and registers web-vitals listeners.\n * Listeners are registered only once. If disabled, subsequent calls resume emission.\n */\n override enable(): void {\n if (typeof PerformanceObserver === 'undefined') {\n this._diag.debug(\n 'PerformanceObserver not supported, web vitals will not be collected',\n );\n return;\n }\n\n this._isEnabled = true;\n\n if (this._listenersRegistered) {\n this._diag.debug('Listeners already registered, resuming emission');\n return;\n }\n\n this._listenersRegistered = true;\n this._diag.debug(`Registering listeners`);\n // CLS is only supported in Chromium. See:\n // https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#browser-support\n onCLS((metric) => this._emitWebVital(metric));\n onINP((metric) => this._emitWebVital(metric));\n onLCP((metric) => this._emitWebVital(metric));\n onFCP((metric) => this._emitWebVital(metric));\n onTTFB((metric) => this._emitWebVital(metric));\n }\n\n /**\n * Disables the instrumentation, pausing log emission.\n * Listeners remain active due to web-vitals library limitations.\n */\n override disable(): void {\n this._isEnabled = false;\n this._diag.debug('Instrumentation disabled, pausing emission');\n }\n\n /**\n * Gets the timestamp for a metric based on attribution timing.\n * Returns undefined to let OTel use the current time for metrics without\n * specific timing information.\n */\n private _getTimestampForMetric(\n metric: MetricWithAttribution,\n ): number | undefined {\n if (metric.name === 'CLS') {\n const { attribution } = metric as CLSMetricWithAttribution;\n if (attribution.largestShiftTime !== undefined) {\n return attribution.largestShiftTime;\n }\n return undefined;\n }\n if (metric.name === 'INP') {\n const { attribution } = metric as INPMetricWithAttribution;\n return attribution.interactionTime;\n }\n // FCP, LCP, TTFB: metric.value is already DOMHighResTimeStamp of the event\n return metric.value;\n }\n\n private _emitWebVital(metric: MetricWithAttribution): void {\n if (!this._isEnabled) {\n return;\n }\n const attributes: Attributes = {\n [ATTR_WEB_VITAL_NAME]: metric.name.toLowerCase(),\n [ATTR_WEB_VITAL_VALUE]: metric.value,\n // `delta` equals `value` on the first emission; subsequent emissions report only the change\n [ATTR_WEB_VITAL_DELTA]: metric.delta,\n [ATTR_WEB_VITAL_RATING]: metric.rating,\n [ATTR_WEB_VITAL_ID]: metric.id,\n [ATTR_WEB_VITAL_NAVIGATION_TYPE]: metric.navigationType,\n };\n\n const timestamp = this._getTimestampForMetric(metric);\n\n const logRecord: LogRecord = {\n eventName: WEB_VITAL_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes,\n ...(this._includeRawAttribution\n ? { body: JSON.stringify(metric.attribution) }\n : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n };\n\n if (this._applyCustomLogRecordData) {\n safeExecuteInTheMiddle(\n () => this._applyCustomLogRecordData?.(logRecord),\n (error) => {\n if (error) {\n this._diag.error('applyCustomLogRecordData hook failed', error);\n }\n },\n true,\n );\n }\n\n this.logger.emit(logRecord);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAqCA,IAAa,2BAAb,cAA8C,oBAAoD;CAMhG;CACA;CAEA,YAAY,SAAyC,EAAE,EAAE;AACvD,QAAM,6CAA6C,SAAS,OAAO;AACnE,OAAK,4BAA4B,OAAO;AACxC,OAAK,yBAAyB,OAAO,yBAAyB;;CAGhE,OAA0B;AACxB,SAAO,EAAE;;;;;;CAOX,SAAwB;AACtB,MAAI,OAAO,wBAAwB,aAAa;AAC9C,QAAK,MAAM,MACT,sEACD;AACD;;AAGF,OAAK,aAAa;AAElB,MAAI,KAAK,sBAAsB;AAC7B,QAAK,MAAM,MAAM,kDAAkD;AACnE;;AAGF,OAAK,uBAAuB;AAC5B,OAAK,MAAM,MAAM,wBAAwB;AAGzC,SAAO,WAAW,KAAK,cAAc,OAAO,CAAC;AAC7C,SAAO,WAAW,KAAK,cAAc,OAAO,CAAC;AAC7C,SAAO,WAAW,KAAK,cAAc,OAAO,CAAC;AAC7C,SAAO,WAAW,KAAK,cAAc,OAAO,CAAC;AAC7C,UAAQ,WAAW,KAAK,cAAc,OAAO,CAAC;;;;;;CAOhD,UAAyB;AACvB,OAAK,aAAa;AAClB,OAAK,MAAM,MAAM,6CAA6C;;;;;;;CAQhE,uBACE,QACoB;AACpB,MAAI,OAAO,SAAS,OAAO;GACzB,MAAM,EAAE,gBAAgB;AACxB,OAAI,YAAY,qBAAqB,KAAA,EACnC,QAAO,YAAY;AAErB;;AAEF,MAAI,OAAO,SAAS,OAAO;GACzB,MAAM,EAAE,gBAAgB;AACxB,UAAO,YAAY;;AAGrB,SAAO,OAAO;;CAGhB,cAAsB,QAAqC;AACzD,MAAI,CAAC,KAAK,WACR;EAEF,MAAM,aAAyB;IAC5B,sBAAsB,OAAO,KAAK,aAAa;IAC/C,uBAAuB,OAAO;IAE9B,uBAAuB,OAAO;IAC9B,wBAAwB,OAAO;IAC/B,oBAAoB,OAAO;IAC3B,iCAAiC,OAAO;GAC1C;EAED,MAAM,YAAY,KAAK,uBAAuB,OAAO;EAErD,MAAM,YAAuB;GAC3B,WAAW;GACX,gBAAgB,eAAe;GAC/B;GACA,GAAI,KAAK,yBACL,EAAE,MAAM,KAAK,UAAU,OAAO,YAAY,EAAE,GAC5C,EAAE;GACN,GAAI,cAAc,KAAA,IAAY,EAAE,WAAW,GAAG,EAAE;GACjD;AAED,MAAI,KAAK,0BACP,8BACQ,KAAK,4BAA4B,UAAU,GAChD,UAAU;AACT,OAAI,MACF,MAAK,MAAM,MAAM,wCAAwC,MAAM;KAGnE,KACD;AAGH,OAAK,OAAO,KAAK,UAAU"}
@@ -0,0 +1,30 @@
1
+ //#region src/web-vitals/semconv.ts
2
+ const WEB_VITAL_EVENT_NAME = "browser.web_vital";
3
+ /**
4
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
5
+ */
6
+ const ATTR_WEB_VITAL_NAME = "browser.web_vital.name";
7
+ /**
8
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
9
+ */
10
+ const ATTR_WEB_VITAL_VALUE = "browser.web_vital.value";
11
+ /**
12
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
13
+ */
14
+ const ATTR_WEB_VITAL_RATING = "browser.web_vital.rating";
15
+ /**
16
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
17
+ */
18
+ const ATTR_WEB_VITAL_DELTA = "browser.web_vital.delta";
19
+ /**
20
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
21
+ */
22
+ const ATTR_WEB_VITAL_ID = "browser.web_vital.id";
23
+ /**
24
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
25
+ */
26
+ const ATTR_WEB_VITAL_NAVIGATION_TYPE = "browser.web_vital.navigation_type";
27
+ //#endregion
28
+ export { ATTR_WEB_VITAL_DELTA, ATTR_WEB_VITAL_ID, ATTR_WEB_VITAL_NAME, ATTR_WEB_VITAL_NAVIGATION_TYPE, ATTR_WEB_VITAL_RATING, ATTR_WEB_VITAL_VALUE, WEB_VITAL_EVENT_NAME };
29
+
30
+ //# sourceMappingURL=semconv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semconv.js","names":[],"sources":["../../src/web-vitals/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 WEB_VITAL_EVENT_NAME = 'browser.web_vital';\n\n// Core metric attributes\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_WEB_VITAL_NAME = 'browser.web_vital.name';\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_WEB_VITAL_VALUE = 'browser.web_vital.value';\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_WEB_VITAL_RATING = 'browser.web_vital.rating';\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_WEB_VITAL_DELTA = 'browser.web_vital.delta';\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_WEB_VITAL_ID = 'browser.web_vital.id';\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_WEB_VITAL_NAVIGATION_TYPE =\n 'browser.web_vital.navigation_type';\n"],"mappings":";AAWA,MAAa,uBAAuB;;;;AAOpC,MAAa,sBAAsB;;;;AAKnC,MAAa,uBAAuB;;;;AAKpC,MAAa,wBAAwB;;;;AAKrC,MAAa,uBAAuB;;;;AAKpC,MAAa,oBAAoB;;;;AAKjC,MAAa,iCACX"}
@@ -0,0 +1,26 @@
1
+ import { LogRecord } from "@opentelemetry/api-logs";
2
+ import { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
+
4
+ //#region src/web-vitals/types.d.ts
5
+ /**
6
+ * WebVitalsInstrumentation Configuration
7
+ */
8
+ interface WebVitalsInstrumentationConfig extends InstrumentationConfig {
9
+ /**
10
+ * @experimental
11
+ * When true, sets the log record body to the JSON-stringified
12
+ * `web-vitals` attribution object for the metric.
13
+ *
14
+ * Note: `applyCustomLogRecordData` runs after the body is set.
15
+ * If the hook assigns a new `body`, it will overwrite the attribution data.
16
+ */
17
+ includeRawAttribution?: boolean;
18
+ /**
19
+ * Hook to modify log records before they are emitted.
20
+ * Use this to add custom attributes or modify the log record.
21
+ */
22
+ applyCustomLogRecordData?: (logRecord: LogRecord) => void;
23
+ }
24
+ //#endregion
25
+ export { WebVitalsInstrumentationConfig };
26
+ //# sourceMappingURL=types.d.ts.map
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@opentelemetry/browser-instrumentation",
3
+ "version": "0.1.0",
4
+ "description": "OpenTelemetry browser instrumentations.",
5
+ "keywords": [
6
+ "opentelemetry",
7
+ "browser",
8
+ "web",
9
+ "instrumentation",
10
+ "navigation-timing",
11
+ "user-action",
12
+ "web-vitals"
13
+ ],
14
+ "homepage": "https://github.com/open-telemetry/opentelemetry-browser",
15
+ "bugs": "https://github.com/open-telemetry/opentelemetry-browser/issues",
16
+ "license": "Apache-2.0",
17
+ "author": "OpenTelemetry Authors",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/open-telemetry/opentelemetry-browser.git",
21
+ "directory": "packages/instrumentation"
22
+ },
23
+ "type": "module",
24
+ "exports": {
25
+ "./experimental/navigation-timing": "./dist/navigation-timing/index.js",
26
+ "./experimental/user-action": "./dist/user-action/index.js",
27
+ "./experimental/web-vitals": "./dist/web-vitals/index.js"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "watch": "tsdown --watch --no-clean",
35
+ "check-types": "tsc",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "test:coverage": "vitest --coverage"
39
+ },
40
+ "dependencies": {
41
+ "@opentelemetry/api-logs": "^0.213.0",
42
+ "@opentelemetry/instrumentation": "^0.213.0",
43
+ "web-vitals": "^5.1.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@opentelemetry/api": "^1.9.0"
47
+ },
48
+ "devDependencies": {
49
+ "@opentelemetry/test-utils": "*",
50
+ "@opentelemetry/web-utils": "*"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }