@blueking/open-telemetry 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +0 -0
  2. package/dist/bk-rum.global.js +11 -0
  3. package/dist/bk-rum.global.js.map +1 -0
  4. package/dist/index.js +1186 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +49 -0
  7. package/playground/App.vue +148 -0
  8. package/playground/bk-ot.ts +110 -0
  9. package/playground/components/DemoLogPanel.vue +89 -0
  10. package/playground/composables/use-demo-log.ts +39 -0
  11. package/playground/index.html +13 -0
  12. package/playground/main.ts +36 -0
  13. package/playground/mock/index.ts +43 -0
  14. package/playground/pages/BlankScreenDemo.vue +97 -0
  15. package/playground/pages/BusinessDemo.vue +74 -0
  16. package/playground/pages/CustomPluginDemo.vue +104 -0
  17. package/playground/pages/ErrorDemo.vue +75 -0
  18. package/playground/pages/Home.vue +133 -0
  19. package/playground/pages/HttpDemo.vue +120 -0
  20. package/playground/pages/LongTaskDemo.vue +66 -0
  21. package/playground/pages/WebSocketDemo.vue +100 -0
  22. package/playground/router/index.ts +92 -0
  23. package/playground/style.css +180 -0
  24. package/src/browser.ts +44 -0
  25. package/src/core/config.ts +327 -0
  26. package/src/core/plugin.ts +97 -0
  27. package/src/core/processor.ts +74 -0
  28. package/src/core/route-observer.ts +128 -0
  29. package/src/core/sampling.ts +64 -0
  30. package/src/core/sdk.ts +327 -0
  31. package/src/core/url.ts +66 -0
  32. package/src/index.ts +55 -0
  33. package/src/plugins/blank-screen.ts +170 -0
  34. package/src/plugins/common.ts +131 -0
  35. package/src/plugins/csp-violation.ts +74 -0
  36. package/src/plugins/device.ts +91 -0
  37. package/src/plugins/error.ts +211 -0
  38. package/src/plugins/http-body.ts +437 -0
  39. package/src/plugins/long-task.ts +99 -0
  40. package/src/plugins/page-view.ts +83 -0
  41. package/src/plugins/route-timing.ts +89 -0
  42. package/src/plugins/session.ts +127 -0
  43. package/src/plugins/web-vitals.ts +159 -0
  44. package/src/plugins/websocket.ts +179 -0
  45. package/tsconfig.app.json +27 -0
  46. package/tsconfig.dts.json +13 -0
  47. package/tsconfig.json +7 -0
  48. package/tsconfig.node.json +26 -0
  49. package/vite.config.ts +70 -0
@@ -0,0 +1,131 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* eslint-disable @typescript-eslint/naming-convention */
3
+ /*
4
+ * Tencent is pleased to support the open source community by making
5
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
6
+ *
7
+ * Copyright (C) 2017-2025 Tencent. All rights reserved.
8
+ *
9
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
10
+ *
11
+ * License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
12
+ *
13
+ * ---------------------------------------------------
14
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
15
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
16
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
17
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
18
+ *
19
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
20
+ * the Software.
21
+ *
22
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
23
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
25
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
26
+ * IN THE SOFTWARE.
27
+ */
28
+
29
+ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/instrumentation';
30
+
31
+ import { shouldIgnoreUrl, shouldPropagateTraceHeader } from '../core/url';
32
+
33
+ import type { BkOTInstrumentationsConfig } from '../core/config';
34
+ import type { BkOTPlugin, BkOTRuntimeContext } from '../core/plugin';
35
+
36
+ const URL_KEYS = ['http.url', 'url.full'];
37
+
38
+ // OTel 官方 instrumentation 内部对每个 URL 调用 matcher.test(url),
39
+ // 这里把"用户函数 + 自身 endpoint 过滤"包装成具备 .test 的对象,是与 OTel 一致的扩展用法
40
+ const toUrlPredicateMatcher = (predicate: (url: string) => boolean): RegExp =>
41
+ ({ test: predicate }) as unknown as RegExp;
42
+
43
+ // 对官方 instrumentation 创建的 span,统一附加 page attributes 与 URL 脱敏
44
+ const decorateSpan = (
45
+ context: BkOTRuntimeContext,
46
+ span: {
47
+ attributes?: Record<string, any>;
48
+ setAttribute: (key: string, value: any) => void;
49
+ setAttributes: (attrs: Record<string, any>) => void;
50
+ },
51
+ ) => {
52
+ span.setAttributes(context.applyRedact(context.config.getPageAttributes()));
53
+ for (const key of URL_KEYS) {
54
+ const value = span.attributes?.[key];
55
+ if (typeof value === 'string') {
56
+ const redactedAttributes = context.applyRedact({ [key]: context.config.redactUrl(value) });
57
+ const redactedUrl = redactedAttributes[key];
58
+ if (typeof redactedUrl === 'string') {
59
+ span.setAttribute(key, redactedUrl);
60
+ }
61
+ }
62
+ }
63
+ };
64
+
65
+ export const createCommonInstrumentationsPlugin = (option: BkOTInstrumentationsConfig): BkOTPlugin => {
66
+ const instrumentations: Instrumentation[] = [];
67
+
68
+ return {
69
+ name: 'official-instrumentations',
70
+ enabled: Boolean(option.documentLoad || option.fetch || option.xhr || option.userInteraction),
71
+ async init(context) {
72
+ const loadedInstrumentations: Instrumentation[] = [];
73
+ const ignoreUrls = [toUrlPredicateMatcher(url => shouldIgnoreUrl(context.config, url))];
74
+ const propagateTraceHeaderCorsUrls = [
75
+ toUrlPredicateMatcher(url => shouldPropagateTraceHeader(context.config, url)),
76
+ ];
77
+
78
+ if (option.documentLoad) {
79
+ const { DocumentLoadInstrumentation } = await import('@opentelemetry/instrumentation-document-load');
80
+ loadedInstrumentations.push(
81
+ new DocumentLoadInstrumentation({
82
+ semconvStabilityOptIn: 'http',
83
+ }),
84
+ );
85
+ }
86
+
87
+ if (option.fetch) {
88
+ const { FetchInstrumentation } = await import('@opentelemetry/instrumentation-fetch');
89
+ loadedInstrumentations.push(
90
+ new FetchInstrumentation({
91
+ applyCustomAttributesOnSpan: span => decorateSpan(context, span as any),
92
+ ignoreUrls,
93
+ propagateTraceHeaderCorsUrls,
94
+ semconvStabilityOptIn: 'http',
95
+ ignoreNetworkEvents: false,
96
+ }),
97
+ );
98
+ }
99
+
100
+ if (option.xhr) {
101
+ const { XMLHttpRequestInstrumentation } = await import('@opentelemetry/instrumentation-xml-http-request');
102
+ loadedInstrumentations.push(
103
+ new XMLHttpRequestInstrumentation({
104
+ applyCustomAttributesOnSpan: span => decorateSpan(context, span as any),
105
+ ignoreUrls,
106
+ propagateTraceHeaderCorsUrls,
107
+ semconvStabilityOptIn: 'http',
108
+ ignoreNetworkEvents: false,
109
+ }),
110
+ );
111
+ }
112
+
113
+ if (option.userInteraction) {
114
+ const { UserInteractionInstrumentation } = await import('@opentelemetry/instrumentation-user-interaction');
115
+ const userInteractionConfig =
116
+ typeof option.userInteraction === 'object'
117
+ ? { eventNames: option.userInteraction.eventNames as Array<keyof HTMLElementEventMap> }
118
+ : undefined;
119
+ loadedInstrumentations.push(new UserInteractionInstrumentation(userInteractionConfig));
120
+ }
121
+
122
+ instrumentations.push(...loadedInstrumentations);
123
+ registerInstrumentations({ instrumentations: loadedInstrumentations });
124
+ },
125
+ shutdown() {
126
+ while (instrumentations.length) {
127
+ instrumentations.pop()?.disable?.();
128
+ }
129
+ },
130
+ };
131
+ };
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making
3
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
4
+ *
5
+ * Copyright (C) 2017-2025 Tencent. All rights reserved.
6
+ *
7
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
8
+ *
9
+ * License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
10
+ *
11
+ * ---------------------------------------------------
12
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
13
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
14
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
15
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
16
+ *
17
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
18
+ * the Software.
19
+ *
20
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
21
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
23
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
+ * IN THE SOFTWARE.
25
+ */
26
+
27
+ import { SeverityNumber } from '@opentelemetry/api-logs';
28
+
29
+ import type { BkOTRumConfig } from '../core/config';
30
+ import type { BkOTPlugin } from '../core/plugin';
31
+
32
+ /**
33
+ * 监听 CSP 违规事件并以 log 形式上报,便于排查脚本/资源被 CSP 拦截的问题。
34
+ */
35
+ export const createCspViolationPlugin = (enabled: BkOTRumConfig['cspViolation']): BkOTPlugin => {
36
+ let teardown: (() => void) | undefined;
37
+
38
+ return {
39
+ name: 'csp-violation',
40
+ enabled: Boolean(enabled),
41
+ init(context) {
42
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
43
+ return;
44
+ }
45
+
46
+ const handler = (event: SecurityPolicyViolationEvent) => {
47
+ context.emitLog({
48
+ severityNumber: SeverityNumber.WARN,
49
+ severityText: 'WARN',
50
+ body: 'csp.violation',
51
+ attributes: {
52
+ ...context.config.getPageAttributes(),
53
+ 'csp.blocked_uri': context.config.redactUrl(event.blockedURI || ''),
54
+ 'csp.violated_directive': event.violatedDirective,
55
+ 'csp.effective_directive': event.effectiveDirective,
56
+ 'csp.original_policy': event.originalPolicy,
57
+ 'csp.disposition': event.disposition,
58
+ 'csp.source_file': context.config.redactUrl(event.sourceFile || ''),
59
+ 'csp.line_number': event.lineNumber,
60
+ 'csp.column_number': event.columnNumber,
61
+ 'csp.status_code': event.statusCode,
62
+ },
63
+ });
64
+ };
65
+
66
+ document.addEventListener('securitypolicyviolation', handler);
67
+ teardown = () => document.removeEventListener('securitypolicyviolation', handler);
68
+ },
69
+ shutdown() {
70
+ teardown?.();
71
+ teardown = undefined;
72
+ },
73
+ };
74
+ };
@@ -0,0 +1,91 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making
3
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
4
+ *
5
+ * Copyright (C) 2017-2025 Tencent. All rights reserved.
6
+ *
7
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
8
+ *
9
+ * License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
10
+ *
11
+ * ---------------------------------------------------
12
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
13
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
14
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
15
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
16
+ *
17
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
18
+ * the Software.
19
+ *
20
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
21
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
23
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
+ * IN THE SOFTWARE.
25
+ */
26
+
27
+ import type { BkOTRumConfig } from '../core/config';
28
+ import type { BkOTPlugin } from '../core/plugin';
29
+ import type { Attributes } from '@opentelemetry/api';
30
+
31
+ // 复用旧 session 插件的 storageKey,避免升级后丢失现网累计的设备标识
32
+ const DEFAULT_DEVICE_STORAGE_KEY = 'bk_ot_session_id';
33
+
34
+ const createId = () => {
35
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
36
+ return crypto.randomUUID();
37
+ }
38
+ return `device-${Date.now()}-${Math.random().toString(16).slice(2)}`;
39
+ };
40
+
41
+ const readDeviceId = (storageKey: string) => {
42
+ try {
43
+ const existed = window.localStorage.getItem(storageKey);
44
+ if (existed) {
45
+ return existed;
46
+ }
47
+ const deviceId = createId();
48
+ window.localStorage.setItem(storageKey, deviceId);
49
+ return deviceId;
50
+ } catch {
51
+ return createId();
52
+ }
53
+ };
54
+
55
+ const getViewportAttributes = (): Attributes => {
56
+ if (typeof window === 'undefined') {
57
+ return {};
58
+ }
59
+ const connection = (
60
+ navigator as Navigator & {
61
+ connection?: { effectiveType?: string; type?: string };
62
+ }
63
+ ).connection;
64
+
65
+ return {
66
+ 'browser.viewport.width': window.innerWidth,
67
+ 'browser.viewport.height': window.innerHeight,
68
+ 'browser.screen.width': window.screen?.width,
69
+ 'browser.screen.height': window.screen?.height,
70
+ 'network.effective_type': connection?.effectiveType ?? connection?.type,
71
+ };
72
+ };
73
+
74
+ /**
75
+ * 设备级永久标识 + 视口 / 网络元数据。
76
+ * 与 session 插件区分:device 跨会话持久,session 有过期与续期。
77
+ */
78
+ export const createDevicePlugin = (option: BkOTRumConfig['device']): BkOTPlugin => ({
79
+ name: 'device',
80
+ enabled: Boolean(option),
81
+ init(context) {
82
+ const storageKey =
83
+ typeof option === 'object' ? (option.storageKey ?? DEFAULT_DEVICE_STORAGE_KEY) : DEFAULT_DEVICE_STORAGE_KEY;
84
+ const deviceId = typeof window === 'undefined' ? createId() : readDeviceId(storageKey);
85
+
86
+ context.setRuntimeAttributes({
87
+ 'device.id': deviceId,
88
+ ...getViewportAttributes(),
89
+ });
90
+ },
91
+ });
@@ -0,0 +1,211 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making
3
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
4
+ *
5
+ * Copyright (C) 2017-2025 Tencent. All rights reserved.
6
+ *
7
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
8
+ *
9
+ * License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
10
+ *
11
+ * ---------------------------------------------------
12
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
13
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
14
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
15
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
16
+ *
17
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
18
+ * the Software.
19
+ *
20
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
21
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
23
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
+ * IN THE SOFTWARE.
25
+ */
26
+
27
+ import { SpanStatusCode, trace } from '@opentelemetry/api';
28
+ import { SeverityNumber } from '@opentelemetry/api-logs';
29
+
30
+ import type { BkOTRumConfig } from '../core/config';
31
+ import type { BkOTPlugin, BkOTRuntimeContext } from '../core/plugin';
32
+
33
+ const DEFAULT_WINDOW_MS = 60_000;
34
+ const DEFAULT_MAX_PER_WINDOW = 5;
35
+
36
+ const getErrorMessage = (value: unknown) => {
37
+ if (value instanceof Error) {
38
+ return value.message;
39
+ }
40
+ if (typeof value === 'string') {
41
+ return value;
42
+ }
43
+ try {
44
+ return JSON.stringify(value);
45
+ } catch {
46
+ return String(value);
47
+ }
48
+ };
49
+
50
+ const getErrorStack = (value: unknown) => (value instanceof Error ? (value.stack ?? '') : '');
51
+
52
+ // 简易 djb2 hash,足够区分常见 error 字符串组合
53
+ const hashString = (input: string): string => {
54
+ let hash = 5381;
55
+ for (let i = 0; i < input.length; i++) {
56
+ hash = (hash * 33) ^ input.charCodeAt(i);
57
+ }
58
+ return (hash >>> 0).toString(36);
59
+ };
60
+
61
+ interface ThrottleEntry {
62
+ count: number;
63
+ windowStart: number;
64
+ }
65
+
66
+ const createThrottle = (windowMs: number, maxPerWindow: number) => {
67
+ const records = new Map<string, ThrottleEntry>();
68
+ return {
69
+ /** 返回 true 表示允许上报;false 表示触发节流被抛弃 */
70
+ allow(key: string): boolean {
71
+ const now = Date.now();
72
+ const entry = records.get(key);
73
+ if (!entry || now - entry.windowStart > windowMs) {
74
+ records.set(key, { count: 1, windowStart: now });
75
+ return true;
76
+ }
77
+ if (entry.count >= maxPerWindow) {
78
+ return false;
79
+ }
80
+ entry.count += 1;
81
+ return true;
82
+ },
83
+ };
84
+ };
85
+
86
+ interface EmitErrorOptions {
87
+ context: BkOTRuntimeContext;
88
+ error: unknown;
89
+ exceptionType?: string;
90
+ extra?: Record<string, unknown>;
91
+ source: string;
92
+ spanName: string;
93
+ throttle: ReturnType<typeof createThrottle>;
94
+ }
95
+
96
+ const emitError = ({ context, error, exceptionType, extra = {}, source, spanName, throttle }: EmitErrorOptions) => {
97
+ const message = getErrorMessage(error);
98
+ const stack = getErrorStack(error);
99
+ const throttleKey = hashString(`${source}|${message}|${stack.slice(0, 256)}`);
100
+
101
+ if (!throttle.allow(throttleKey)) {
102
+ return;
103
+ }
104
+
105
+ const activeSpan = trace.getActiveSpan();
106
+ const exceptionLike = error instanceof Error ? error : { message, name: exceptionType ?? source };
107
+ const attributes = {
108
+ ...context.config.getPageAttributes(),
109
+ ...context.config.getErrorAttributes(),
110
+ 'exception.message': message,
111
+ 'exception.stacktrace': stack,
112
+ 'exception.type': error instanceof Error ? error.name : (exceptionType ?? source),
113
+ 'bk.rum.error.source': source,
114
+ 'bk.rum.error.fingerprint': throttleKey,
115
+ ...extra,
116
+ };
117
+ const errorSpan = context.startSpan(spanName, attributes);
118
+
119
+ activeSpan?.recordException(exceptionLike);
120
+ activeSpan?.setStatus({ code: SpanStatusCode.ERROR, message });
121
+ errorSpan.recordException(exceptionLike);
122
+ errorSpan.setStatus({ code: SpanStatusCode.ERROR, message });
123
+ errorSpan.end();
124
+ context.emitLog({
125
+ severityNumber: SeverityNumber.ERROR,
126
+ severityText: 'ERROR',
127
+ body: message,
128
+ attributes,
129
+ });
130
+ };
131
+
132
+ export const createErrorPlugin = (option: BkOTRumConfig['error']): BkOTPlugin => {
133
+ const listeners: Array<() => void> = [];
134
+
135
+ return {
136
+ name: 'error',
137
+ enabled: Boolean(option),
138
+ init(context) {
139
+ if (typeof window === 'undefined') {
140
+ return;
141
+ }
142
+
143
+ const windowMs = typeof option === 'object' ? (option.windowMs ?? DEFAULT_WINDOW_MS) : DEFAULT_WINDOW_MS;
144
+ const maxPerWindow =
145
+ typeof option === 'object' ? (option.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW) : DEFAULT_MAX_PER_WINDOW;
146
+ const throttle = createThrottle(windowMs, maxPerWindow);
147
+
148
+ const onError = (event: ErrorEvent) => {
149
+ emitError({
150
+ context,
151
+ throttle,
152
+ spanName: 'browser.error',
153
+ source: 'window.error',
154
+ error: event.error ?? event.message,
155
+ extra: {
156
+ 'code.filepath': event.filename,
157
+ 'code.lineno': event.lineno,
158
+ 'code.column': event.colno,
159
+ },
160
+ });
161
+ };
162
+ const onUnhandledRejection = (event: PromiseRejectionEvent) => {
163
+ emitError({
164
+ context,
165
+ throttle,
166
+ spanName: 'browser.unhandledrejection',
167
+ source: 'unhandledrejection',
168
+ error: event.reason,
169
+ exceptionType: event.reason instanceof Error ? event.reason.name : 'UnhandledRejection',
170
+ });
171
+ };
172
+ const onResourceError = (event: Event) => {
173
+ const target = event.target as EventTarget & {
174
+ href?: string;
175
+ src?: string;
176
+ tagName?: string;
177
+ };
178
+ if (target === window) {
179
+ return;
180
+ }
181
+ const resourceUrl = target.src || target.href || '';
182
+ emitError({
183
+ context,
184
+ throttle,
185
+ spanName: 'browser.resource_error',
186
+ source: 'resource',
187
+ error: new Error(`Resource load failed: ${target.tagName?.toLowerCase() || 'unknown'} ${resourceUrl}`),
188
+ exceptionType: 'ResourceError',
189
+ extra: {
190
+ 'url.full': context.config.redactUrl(resourceUrl),
191
+ 'html.tag': target.tagName || '',
192
+ },
193
+ });
194
+ };
195
+
196
+ window.addEventListener('error', onError);
197
+ window.addEventListener('unhandledrejection', onUnhandledRejection);
198
+ window.addEventListener('error', onResourceError, true);
199
+ listeners.push(
200
+ () => window.removeEventListener('error', onError),
201
+ () => window.removeEventListener('unhandledrejection', onUnhandledRejection),
202
+ () => window.removeEventListener('error', onResourceError, true)
203
+ );
204
+ },
205
+ shutdown() {
206
+ while (listeners.length) {
207
+ listeners.pop()?.();
208
+ }
209
+ },
210
+ };
211
+ };