@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.
- package/README.md +0 -0
- package/dist/bk-rum.global.js +11 -0
- package/dist/bk-rum.global.js.map +1 -0
- package/dist/index.js +1186 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/playground/App.vue +148 -0
- package/playground/bk-ot.ts +110 -0
- package/playground/components/DemoLogPanel.vue +89 -0
- package/playground/composables/use-demo-log.ts +39 -0
- package/playground/index.html +13 -0
- package/playground/main.ts +36 -0
- package/playground/mock/index.ts +43 -0
- package/playground/pages/BlankScreenDemo.vue +97 -0
- package/playground/pages/BusinessDemo.vue +74 -0
- package/playground/pages/CustomPluginDemo.vue +104 -0
- package/playground/pages/ErrorDemo.vue +75 -0
- package/playground/pages/Home.vue +133 -0
- package/playground/pages/HttpDemo.vue +120 -0
- package/playground/pages/LongTaskDemo.vue +66 -0
- package/playground/pages/WebSocketDemo.vue +100 -0
- package/playground/router/index.ts +92 -0
- package/playground/style.css +180 -0
- package/src/browser.ts +44 -0
- package/src/core/config.ts +327 -0
- package/src/core/plugin.ts +97 -0
- package/src/core/processor.ts +74 -0
- package/src/core/route-observer.ts +128 -0
- package/src/core/sampling.ts +64 -0
- package/src/core/sdk.ts +327 -0
- package/src/core/url.ts +66 -0
- package/src/index.ts +55 -0
- package/src/plugins/blank-screen.ts +170 -0
- package/src/plugins/common.ts +131 -0
- package/src/plugins/csp-violation.ts +74 -0
- package/src/plugins/device.ts +91 -0
- package/src/plugins/error.ts +211 -0
- package/src/plugins/http-body.ts +437 -0
- package/src/plugins/long-task.ts +99 -0
- package/src/plugins/page-view.ts +83 -0
- package/src/plugins/route-timing.ts +89 -0
- package/src/plugins/session.ts +127 -0
- package/src/plugins/web-vitals.ts +159 -0
- package/src/plugins/websocket.ts +179 -0
- package/tsconfig.app.json +27 -0
- package/tsconfig.dts.json +13 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- 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
|
+
};
|
package/src/core/sdk.ts
ADDED
|
@@ -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;
|
package/src/core/url.ts
ADDED
|
@@ -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
|
+
};
|