@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,64 @@
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 { NormalizedBkOTConfig } from './config';
28
+
29
+ const SAMPLE_STORAGE_KEY = '__bk_ot_sampled__';
30
+
31
+ // 在 sessionStorage 不可用时降级到模块内单例缓存,避免同会话内多次结果不一致
32
+ const memoryCache = new Map<string, boolean>();
33
+
34
+ const readMemory = (key: string) => memoryCache.get(key);
35
+ const writeMemory = (key: string, value: boolean) => {
36
+ memoryCache.set(key, value);
37
+ return value;
38
+ };
39
+
40
+ export const isBkOTSampled = ({ sampleRate }: Pick<NormalizedBkOTConfig, 'sampleRate'>) => {
41
+ if (sampleRate <= 0) {
42
+ return false;
43
+ }
44
+ if (sampleRate >= 1) {
45
+ return true;
46
+ }
47
+
48
+ try {
49
+ const cached = sessionStorage.getItem(SAMPLE_STORAGE_KEY);
50
+ if (cached) {
51
+ return cached === '1';
52
+ }
53
+ const sampled = Math.random() < sampleRate;
54
+ sessionStorage.setItem(SAMPLE_STORAGE_KEY, sampled ? '1' : '0');
55
+ writeMemory(SAMPLE_STORAGE_KEY, sampled);
56
+ return sampled;
57
+ } catch {
58
+ const cached = readMemory(SAMPLE_STORAGE_KEY);
59
+ if (typeof cached === 'boolean') {
60
+ return cached;
61
+ }
62
+ return writeMemory(SAMPLE_STORAGE_KEY, Math.random() < sampleRate);
63
+ }
64
+ };
@@ -0,0 +1,327 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /*
3
+ * Tencent is pleased to support the open source community by making
4
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
5
+ *
6
+ * Copyright (C) 2017-2025 Tencent. All rights reserved.
7
+ *
8
+ * 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
9
+ *
10
+ * License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
11
+ *
12
+ * ---------------------------------------------------
13
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
14
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
15
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
16
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
17
+ *
18
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
19
+ * the Software.
20
+ *
21
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
22
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
24
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
25
+ * IN THE SOFTWARE.
26
+ */
27
+
28
+ import {
29
+ type Attributes,
30
+ type Span,
31
+ context,
32
+ metrics,
33
+ propagation,
34
+ SpanKind,
35
+ SpanStatusCode,
36
+ trace,
37
+ } from '@opentelemetry/api';
38
+ import { logs } from '@opentelemetry/api-logs';
39
+ import { ZoneContextManager } from '@opentelemetry/context-zone';
40
+ import { W3CTraceContextPropagator } from '@opentelemetry/core';
41
+ import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
42
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
43
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
44
+ import { resourceFromAttributes } from '@opentelemetry/resources';
45
+ import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs';
46
+ import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
47
+ import { BatchSpanProcessor, ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';
48
+ import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
49
+
50
+ import { createBlankScreenPlugin } from '../plugins/blank-screen';
51
+ import { createCommonInstrumentationsPlugin } from '../plugins/common';
52
+ import { createCspViolationPlugin } from '../plugins/csp-violation';
53
+ import { createDevicePlugin } from '../plugins/device';
54
+ import { createErrorPlugin } from '../plugins/error';
55
+ import { createHttpBodyPlugin } from '../plugins/http-body';
56
+ import { createLongTaskPlugin } from '../plugins/long-task';
57
+ import { createPageViewPlugin } from '../plugins/page-view';
58
+ import { createRouteTimingPlugin } from '../plugins/route-timing';
59
+ import { createSessionPlugin } from '../plugins/session';
60
+ import { createWebVitalsPlugin } from '../plugins/web-vitals';
61
+ import { createWebSocketPlugin } from '../plugins/websocket';
62
+ import { type BkOTConfig, type BkOTCustomEventPayload, type NormalizedBkOTConfig, normalizeConfig } from './config';
63
+ import { type BkOTPlugin, type BkOTRuntimeContext, PluginManager } from './plugin';
64
+ import { FilteringSpanProcessor } from './processor';
65
+ import { isBkOTSampled } from './sampling';
66
+
67
+ import type { LogRecord } from '@opentelemetry/api-logs';
68
+
69
+ export interface BkOTInstance extends BkOTRuntimeContext {
70
+ flush: () => Promise<void>;
71
+ reportCustomEvent: (payload: BkOTCustomEventPayload) => void;
72
+ shutdown: () => Promise<void>;
73
+ start: () => Promise<void>;
74
+ }
75
+
76
+ const createExporterOptions = (config: NormalizedBkOTConfig['traces']) => ({
77
+ url: config.endpoint,
78
+ headers: config.headers,
79
+ concurrencyLimit: config.concurrencyLimit,
80
+ timeoutMillis: config.timeoutMillis,
81
+ });
82
+
83
+ type DebugSignal = 'logs' | 'metrics' | 'traces';
84
+
85
+ const BK_OT_INSTRUMENTATION_NAME = 'bk-rum';
86
+
87
+ /**
88
+ * 用 Proxy 包装一层 debug 旁路,避免直接 mutate 第三方 exporter 实例(其内部可能含 #private fields)。
89
+ */
90
+ const wrapWithDebug = <TExporter extends { export: (...args: any[]) => any }>(
91
+ exporter: TExporter,
92
+ signal: DebugSignal,
93
+ enabled: boolean,
94
+ ): TExporter => {
95
+ if (!enabled) {
96
+ return exporter;
97
+ }
98
+ return new Proxy(exporter, {
99
+ get(target, prop, receiver) {
100
+ if (prop === 'export') {
101
+ return (records: unknown, callback: unknown) => {
102
+ globalThis.console?.log(`[bk-ot][${signal}]`, records);
103
+ return (target as { export: (...args: unknown[]) => unknown }).export(records, callback);
104
+ };
105
+ }
106
+ const value = Reflect.get(target, prop, receiver);
107
+ return typeof value === 'function' ? value.bind(target) : value;
108
+ },
109
+ });
110
+ };
111
+
112
+ // 负责给原生 fetch/xhr 注入 W3C traceparent 头;仅 SDK 启用时装载,避免 disabled 状态仍 patch 全局 API
113
+ const getCorePlugins = (config: NormalizedBkOTConfig): BkOTPlugin[] => [
114
+ createCommonInstrumentationsPlugin(config.instrumentations),
115
+ ];
116
+
117
+ // 仅在采样命中时启用的"会主动产生数据"的 RUM 插件
118
+ const getRumPlugins = (config: NormalizedBkOTConfig): BkOTPlugin[] => [
119
+ createDevicePlugin(config.rum.device),
120
+ createHttpBodyPlugin(config.rum.httpBody),
121
+ createSessionPlugin(config.rum.session),
122
+ createPageViewPlugin(config.rum.pageView),
123
+ createErrorPlugin(config.rum.error),
124
+ createWebVitalsPlugin(config.rum.webVitals),
125
+ createBlankScreenPlugin(config.rum.blankScreen),
126
+ createWebSocketPlugin(config.rum.websocket),
127
+ createLongTaskPlugin(config.rum.longTask),
128
+ createCspViolationPlugin(config.rum.cspViolation),
129
+ createRouteTimingPlugin(config.rum.routeTiming),
130
+ ];
131
+
132
+ type SdkLifecyclePhase = 'idle' | 'started' | 'stopped';
133
+
134
+ export const createBkOT = (inputConfig: BkOTConfig): BkOTInstance => {
135
+ const config = normalizeConfig(inputConfig);
136
+ const sampled = config.enabled && isBkOTSampled(config);
137
+ const resource = resourceFromAttributes(config.resourceAttributes);
138
+ const spanExporter = wrapWithDebug(
139
+ new OTLPTraceExporter(createExporterOptions(config.traces)),
140
+ 'traces',
141
+ config.debug,
142
+ );
143
+ const metricExporter = wrapWithDebug(
144
+ new OTLPMetricExporter(createExporterOptions(config.metrics)),
145
+ 'metrics',
146
+ config.debug,
147
+ );
148
+ const logExporter = wrapWithDebug(new OTLPLogExporter(createExporterOptions(config.logs)), 'logs', config.debug);
149
+ const batchSpanProcessor = new BatchSpanProcessor(spanExporter, config.spanBatch);
150
+ const spanProcessor = new FilteringSpanProcessor(batchSpanProcessor, config);
151
+ const metricReader = new PeriodicExportingMetricReader({
152
+ exporter: metricExporter,
153
+ exportIntervalMillis: config.metricIntervalMillis,
154
+ });
155
+ const logProcessor = new BatchLogRecordProcessor(logExporter);
156
+ const tracerProvider = new WebTracerProvider({
157
+ resource,
158
+ sampler: new ParentBasedSampler({
159
+ root: new TraceIdRatioBasedSampler(sampled ? 1 : 0),
160
+ }),
161
+ spanProcessors: [spanProcessor],
162
+ });
163
+ const meterProvider = new MeterProvider({
164
+ resource,
165
+ readers: [metricReader],
166
+ });
167
+ const loggerProvider = new LoggerProvider({
168
+ resource,
169
+ processors: [logProcessor],
170
+ });
171
+ const tracer = tracerProvider.getTracer(BK_OT_INSTRUMENTATION_NAME);
172
+ const meter = meterProvider.getMeter(BK_OT_INSTRUMENTATION_NAME);
173
+ const logger = loggerProvider.getLogger(BK_OT_INSTRUMENTATION_NAME);
174
+ const runtimeAttributes: Attributes = {
175
+ 'bk.rum.sampled': sampled,
176
+ };
177
+ // 核心插件在 SDK 启用时装载;RUM 数据上报类插件仅在采样命中时装载
178
+ const pluginManager = new PluginManager([
179
+ ...(config.enabled ? getCorePlugins(config) : []),
180
+ ...(sampled ? [...getRumPlugins(config), ...config.plugins] : []),
181
+ ]);
182
+ const teardownCallbacks: Array<() => void> = [];
183
+ let phase: SdkLifecyclePhase = 'idle';
184
+
185
+ const getRuntimeAttributes = () => ({ ...runtimeAttributes });
186
+ const setRuntimeAttributes = (attributes: Attributes) => {
187
+ Object.assign(runtimeAttributes, attributes);
188
+ };
189
+ const applyRedact = (attributes: Attributes) => config.redactAttributes(attributes);
190
+ const startSpan = (name: string, attributes: Attributes = {}): Span =>
191
+ tracer.startSpan(name, {
192
+ kind: SpanKind.INTERNAL,
193
+ attributes: applyRedact({
194
+ ...getRuntimeAttributes(),
195
+ ...attributes,
196
+ }),
197
+ });
198
+ const emitLog = (record: LogRecord) => {
199
+ const merged: LogRecord = {
200
+ ...record,
201
+ attributes: applyRedact({
202
+ ...getRuntimeAttributes(),
203
+ ...((record.attributes ?? {}) as Attributes),
204
+ }),
205
+ };
206
+ logger.emit(merged);
207
+ };
208
+
209
+ const instance: BkOTInstance = {
210
+ config,
211
+ tracer,
212
+ meter,
213
+ logger,
214
+ applyRedact,
215
+ emitLog,
216
+ getRuntimeAttributes,
217
+ setRuntimeAttributes,
218
+ startSpan,
219
+ reportCustomEvent({ attributes = {}, error, name }) {
220
+ if (!sampled) {
221
+ // 未采样时静默丢弃;如需调试请把 debug 选项打开
222
+ if (config.debug) {
223
+ globalThis.console?.warn('[bk-ot] custom event dropped (not sampled):', name);
224
+ }
225
+ return;
226
+ }
227
+
228
+ const span = startSpan(`custom.${name}`, {
229
+ ...config.getPageAttributes(),
230
+ ...config.getCustomAttributes(),
231
+ ...attributes,
232
+ 'rum.custom.name': name,
233
+ });
234
+
235
+ if (error) {
236
+ span.recordException(error);
237
+ span.setStatus({
238
+ code: SpanStatusCode.ERROR,
239
+ message: error.message,
240
+ });
241
+ }
242
+
243
+ span.end();
244
+ },
245
+ async start() {
246
+ if (phase === 'started') {
247
+ return;
248
+ }
249
+ if (phase === 'stopped') {
250
+ // shutdown 已经销毁了 provider/processor,此处禁止再次启动
251
+ throw new Error('[bk-ot] instance has been shut down and cannot be restarted; create a new instance instead.');
252
+ }
253
+ if (!config.enabled) {
254
+ phase = 'started';
255
+ return;
256
+ }
257
+ propagation.setGlobalPropagator(new W3CTraceContextPropagator());
258
+ tracerProvider.register({
259
+ contextManager: new ZoneContextManager(),
260
+ });
261
+ metrics.setGlobalMeterProvider(meterProvider);
262
+ logs.setGlobalLoggerProvider(loggerProvider);
263
+ try {
264
+ await pluginManager.start(instance);
265
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
266
+ const flushOnPageHide = () => {
267
+ void instance.flush();
268
+ };
269
+ const flushOnHidden = () => {
270
+ if (document.visibilityState === 'hidden') {
271
+ void instance.flush();
272
+ }
273
+ };
274
+ window.addEventListener('pagehide', flushOnPageHide);
275
+ document.addEventListener('visibilitychange', flushOnHidden);
276
+ teardownCallbacks.push(
277
+ () => window.removeEventListener('pagehide', flushOnPageHide),
278
+ () => document.removeEventListener('visibilitychange', flushOnHidden),
279
+ );
280
+ }
281
+ } catch (error) {
282
+ while (teardownCallbacks.length) {
283
+ teardownCallbacks.pop()?.();
284
+ }
285
+ await pluginManager.shutdown();
286
+ trace.disable();
287
+ metrics.disable();
288
+ context.disable();
289
+ throw error;
290
+ }
291
+ phase = 'started';
292
+ },
293
+ async flush() {
294
+ await pluginManager.flush();
295
+ await tracerProvider.forceFlush();
296
+ await meterProvider.forceFlush();
297
+ await loggerProvider.forceFlush();
298
+ },
299
+ async shutdown() {
300
+ if (phase === 'stopped') {
301
+ return;
302
+ }
303
+ await instance.flush();
304
+ while (teardownCallbacks.length) {
305
+ teardownCallbacks.pop()?.();
306
+ }
307
+ await pluginManager.shutdown();
308
+ await tracerProvider.shutdown();
309
+ await meterProvider.shutdown();
310
+ await loggerProvider.shutdown();
311
+ trace.disable();
312
+ metrics.disable();
313
+ context.disable();
314
+ phase = 'stopped';
315
+ },
316
+ };
317
+
318
+ if (config.autoStart) {
319
+ void instance.start().catch(error => {
320
+ globalThis.console?.warn('[bk-ot] auto start failed:', error);
321
+ });
322
+ }
323
+
324
+ return instance;
325
+ };
326
+
327
+ export const initBkOT = createBkOT;
@@ -0,0 +1,66 @@
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 { NormalizedBkOTConfig } from './config';
28
+
29
+ const getBaseUrl = () => (typeof window === 'undefined' ? 'http://localhost' : window.location.origin);
30
+
31
+ const toAbsoluteUrl = (url: string) => {
32
+ try {
33
+ return new URL(url, getBaseUrl()).href;
34
+ } catch {
35
+ return url;
36
+ }
37
+ };
38
+
39
+ // 去掉 trailing slash,便于 path 边界对齐比较
40
+ const stripTrailingSlash = (value: string) => (value.length > 1 ? value.replace(/\/+$/, '') : value);
41
+
42
+ // 比较两个 URL 是否指向同一资源(忽略 query/fragment 与末尾斜杠)
43
+ const isSameResource = (left: string, right: string) => {
44
+ const stripped = (value: string) => stripTrailingSlash(value.replace(/[?#].*$/, ''));
45
+ return stripped(left) === stripped(right);
46
+ };
47
+
48
+ const isSameOrigin = (url: string) => {
49
+ try {
50
+ return new URL(url, getBaseUrl()).origin === getBaseUrl();
51
+ } catch {
52
+ return false;
53
+ }
54
+ };
55
+
56
+ export const isBkOTEndpoint = (config: NormalizedBkOTConfig, url: string) => {
57
+ const targetUrl = toAbsoluteUrl(url);
58
+
59
+ return [config.traces.endpoint, config.metrics.endpoint, config.logs.endpoint].some(endpoint =>
60
+ isSameResource(targetUrl, toAbsoluteUrl(endpoint))
61
+ );
62
+ };
63
+
64
+ export const shouldIgnoreUrl = (config: NormalizedBkOTConfig, url: string) => isBkOTEndpoint(config, url);
65
+
66
+ export const shouldPropagateTraceHeader = (_config: NormalizedBkOTConfig, url: string) => isSameOrigin(url);
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
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
+ export {
28
+ type BkOTAttributesConfig,
29
+ type BkOTAttributeValue,
30
+ type BkOTBatchConfig,
31
+ type BkOTConfig,
32
+ type BkOTCustomEventPayload,
33
+ type BkOTHttpBodyConfig,
34
+ type BkOTHttpBodyRedactPayload,
35
+ type BkOTRedactConfig,
36
+ type BkOTRumConfig,
37
+ normalizeConfig,
38
+ type NormalizedBkOTConfig,
39
+ type SignalExporterConfig,
40
+ } from './core/config';
41
+ export { type BkOTPlugin, type BkOTRuntimeContext, createPlugin, type PluginManager } from './core/plugin';
42
+ export { FilteringSpanProcessor } from './core/processor';
43
+ export { type BkOTInstance, createBkOT, initBkOT } from './core/sdk';
44
+ export { createBlankScreenPlugin } from './plugins/blank-screen';
45
+ export { createCommonInstrumentationsPlugin } from './plugins/common';
46
+ export { createCspViolationPlugin } from './plugins/csp-violation';
47
+ export { createDevicePlugin } from './plugins/device';
48
+ export { createErrorPlugin } from './plugins/error';
49
+ export { createHttpBodyPlugin } from './plugins/http-body';
50
+ export { createLongTaskPlugin } from './plugins/long-task';
51
+ export { createPageViewPlugin } from './plugins/page-view';
52
+ export { createRouteTimingPlugin } from './plugins/route-timing';
53
+ export { createSessionPlugin } from './plugins/session';
54
+ export { createWebVitalsPlugin } from './plugins/web-vitals';
55
+ export { createWebSocketPlugin } from './plugins/websocket';
@@ -0,0 +1,170 @@
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
+ const DEFAULT_SELECTOR = 'body';
33
+ const DEFAULT_CHECK_DELAY = 3000;
34
+ const DEFAULT_THRESHOLD = 0.8;
35
+ // 常见 loading mask / spinner 选择器,命中说明页面正在加载而非空白
36
+ const DEFAULT_LOADING_SELECTORS = [
37
+ '[data-loading="true"]',
38
+ '[aria-busy="true"]',
39
+ '.loading',
40
+ '.is-loading',
41
+ '.spinner',
42
+ '.skeleton',
43
+ '.bk-loading',
44
+ ];
45
+
46
+ const SAMPLE_POINTS: Array<[number, number]> = [
47
+ [0.5, 0.5],
48
+ [0.25, 0.25],
49
+ [0.75, 0.25],
50
+ [0.25, 0.75],
51
+ [0.75, 0.75],
52
+ ];
53
+
54
+ const getElementSelector = (element: Element | null) => {
55
+ if (!element) {
56
+ return '';
57
+ }
58
+ const tag = element.tagName.toLowerCase();
59
+ if (element.id) {
60
+ return `${tag}#${element.id}`;
61
+ }
62
+ return tag;
63
+ };
64
+
65
+ const matchesAny = (element: Element | null, selectors: string[]) => {
66
+ if (!element) return false;
67
+ return selectors.some(selector => {
68
+ try {
69
+ return element.matches(selector) || !!element.closest(selector);
70
+ } catch {
71
+ return false;
72
+ }
73
+ });
74
+ };
75
+
76
+ // 页面就绪后再开始计时,避免 SPA / 慢加载场景下检测时机过早
77
+ const whenDocumentReady = (callback: () => void) => {
78
+ if (typeof document === 'undefined') return;
79
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
80
+ callback();
81
+ return;
82
+ }
83
+ const handler = () => {
84
+ document.removeEventListener('DOMContentLoaded', handler);
85
+ callback();
86
+ };
87
+ document.addEventListener('DOMContentLoaded', handler);
88
+ };
89
+
90
+ export const createBlankScreenPlugin = (option: BkOTRumConfig['blankScreen']): BkOTPlugin => {
91
+ let timer: number | undefined;
92
+
93
+ return {
94
+ name: 'blank-screen',
95
+ enabled: Boolean(option),
96
+ init(context) {
97
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
98
+ return;
99
+ }
100
+
101
+ const optionObj = typeof option === 'object' ? option : {};
102
+ const rootSelector = optionObj.rootSelector ?? DEFAULT_SELECTOR;
103
+ const checkDelay = optionObj.checkDelay ?? DEFAULT_CHECK_DELAY;
104
+ const threshold = optionObj.threshold ?? DEFAULT_THRESHOLD;
105
+ const loadingSelectors = [...DEFAULT_LOADING_SELECTORS, ...(optionObj.ignoreSelectors ?? [])];
106
+
107
+ // 仅创建一次 counter,避免每次回调都触发一次 meter lookup
108
+ const counter = context.meter.createCounter('browser.blank_screen.count', {
109
+ description: 'Number of suspected blank screen detections',
110
+ });
111
+
112
+ whenDocumentReady(() => {
113
+ timer = window.setTimeout(() => {
114
+ const root = document.querySelector(rootSelector);
115
+ let emptyCount = 0;
116
+ let loadingCount = 0;
117
+
118
+ for (const [x, y] of SAMPLE_POINTS) {
119
+ const element = document.elementFromPoint(window.innerWidth * x, window.innerHeight * y);
120
+ if (matchesAny(element, loadingSelectors)) {
121
+ loadingCount += 1;
122
+ continue;
123
+ }
124
+ if (element === root || element === document.body || element === document.documentElement) {
125
+ emptyCount += 1;
126
+ }
127
+ }
128
+
129
+ const score = emptyCount / SAMPLE_POINTS.length;
130
+ // 触发 loading mask 的样本算"非空",但若全部点都是 loading,则视为待定不上报
131
+ if (loadingCount === SAMPLE_POINTS.length) {
132
+ return;
133
+ }
134
+ const isBlank = score >= threshold;
135
+ const attributes = {
136
+ 'bk.rum.blank_screen.score': score,
137
+ 'bk.rum.blank_screen.threshold': threshold,
138
+ 'bk.rum.blank_screen.root': rootSelector,
139
+ 'bk.rum.blank_screen.detected': isBlank,
140
+ 'bk.rum.blank_screen.center_element': getElementSelector(
141
+ document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2),
142
+ ),
143
+ 'bk.rum.blank_screen.dom_node_count': document.body?.getElementsByTagName('*').length ?? 0,
144
+ };
145
+
146
+ if (isBlank) {
147
+ counter.add(
148
+ 1,
149
+ context.applyRedact({
150
+ // 仅放进低基数维度,center_element 等高基数字段只走 log
151
+ 'bk.rum.blank_screen.root': rootSelector,
152
+ }),
153
+ );
154
+ context.emitLog({
155
+ severityNumber: SeverityNumber.ERROR,
156
+ severityText: 'ERROR',
157
+ body: 'browser.blank_screen',
158
+ attributes,
159
+ });
160
+ }
161
+ }, checkDelay);
162
+ });
163
+ },
164
+ shutdown() {
165
+ if (timer !== undefined && typeof window !== 'undefined') {
166
+ window.clearTimeout(timer);
167
+ }
168
+ },
169
+ };
170
+ };