@blueking/open-telemetry 0.0.1 → 0.0.4
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/package.json +5 -3
- package/playground/App.vue +0 -148
- package/playground/bk-ot.ts +0 -110
- package/playground/components/DemoLogPanel.vue +0 -89
- package/playground/composables/use-demo-log.ts +0 -39
- package/playground/index.html +0 -13
- package/playground/main.ts +0 -36
- package/playground/mock/index.ts +0 -43
- package/playground/pages/BlankScreenDemo.vue +0 -97
- package/playground/pages/BusinessDemo.vue +0 -74
- package/playground/pages/CustomPluginDemo.vue +0 -104
- package/playground/pages/ErrorDemo.vue +0 -75
- package/playground/pages/Home.vue +0 -133
- package/playground/pages/HttpDemo.vue +0 -120
- package/playground/pages/LongTaskDemo.vue +0 -66
- package/playground/pages/WebSocketDemo.vue +0 -100
- package/playground/router/index.ts +0 -92
- package/playground/style.css +0 -180
- package/src/browser.ts +0 -44
- package/src/core/config.ts +0 -327
- package/src/core/plugin.ts +0 -97
- package/src/core/processor.ts +0 -74
- package/src/core/route-observer.ts +0 -128
- package/src/core/sampling.ts +0 -64
- package/src/core/sdk.ts +0 -327
- package/src/core/url.ts +0 -66
- package/src/index.ts +0 -55
- package/src/plugins/blank-screen.ts +0 -170
- package/src/plugins/common.ts +0 -131
- package/src/plugins/csp-violation.ts +0 -74
- package/src/plugins/device.ts +0 -91
- package/src/plugins/error.ts +0 -211
- package/src/plugins/http-body.ts +0 -437
- package/src/plugins/long-task.ts +0 -99
- package/src/plugins/page-view.ts +0 -83
- package/src/plugins/route-timing.ts +0 -89
- package/src/plugins/session.ts +0 -127
- package/src/plugins/web-vitals.ts +0 -159
- package/src/plugins/websocket.ts +0 -179
- package/tsconfig.app.json +0 -27
- package/tsconfig.dts.json +0 -13
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -70
package/src/plugins/error.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
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
|
-
};
|
package/src/plugins/http-body.ts
DELETED
|
@@ -1,437 +0,0 @@
|
|
|
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 { shouldIgnoreUrl } from '../core/url';
|
|
30
|
-
|
|
31
|
-
import type { BkOTHttpBodyConfig, BkOTHttpBodyRedactPayload } from '../core/config';
|
|
32
|
-
import type { BkOTPlugin, BkOTRuntimeContext } from '../core/plugin';
|
|
33
|
-
import type { Attributes } from '@opentelemetry/api';
|
|
34
|
-
|
|
35
|
-
interface BodySnapshot {
|
|
36
|
-
body: string;
|
|
37
|
-
contentType?: string;
|
|
38
|
-
truncated: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface ReportHttpBodyOptions {
|
|
42
|
-
context: BkOTRuntimeContext;
|
|
43
|
-
duration: number;
|
|
44
|
-
error?: unknown;
|
|
45
|
-
method: string;
|
|
46
|
-
request?: BodySnapshot;
|
|
47
|
-
response?: BodySnapshot;
|
|
48
|
-
status?: number;
|
|
49
|
-
url: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const HTTP_BODY_MAX_SIZE = 10 * 1024;
|
|
53
|
-
|
|
54
|
-
type HttpBodyInput = BodyInit | Document | null | undefined;
|
|
55
|
-
|
|
56
|
-
const truncateBody = (body: string, maxBodySize: number): Pick<BodySnapshot, 'body' | 'truncated'> => {
|
|
57
|
-
if (body.length <= maxBodySize) {
|
|
58
|
-
return {
|
|
59
|
-
body,
|
|
60
|
-
truncated: false,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
body: body.slice(0, maxBodySize),
|
|
65
|
-
truncated: true,
|
|
66
|
-
};
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const getHeaderValue = (headers: HeadersInit | undefined, key: string) => {
|
|
70
|
-
if (!headers) {
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
if (headers instanceof Headers) {
|
|
74
|
-
return headers.get(key) || undefined;
|
|
75
|
-
}
|
|
76
|
-
const normalizedKey = key.toLowerCase();
|
|
77
|
-
if (Array.isArray(headers)) {
|
|
78
|
-
return headers.find(([itemKey]) => itemKey.toLowerCase() === normalizedKey)?.[1];
|
|
79
|
-
}
|
|
80
|
-
const matchedKey = Object.keys(headers).find(item => item.toLowerCase() === normalizedKey);
|
|
81
|
-
return matchedKey ? headers[matchedKey] : undefined;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const stringifyFormData = (body: FormData) => {
|
|
85
|
-
const entries: Record<string, string> = {};
|
|
86
|
-
body.forEach((value, key) => {
|
|
87
|
-
entries[key] =
|
|
88
|
-
value instanceof File ? `[File name=${value.name} type=${value.type || 'unknown'} size=${value.size}]` : value;
|
|
89
|
-
});
|
|
90
|
-
return JSON.stringify(entries);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const stringifyBody = async (body: HttpBodyInput): Promise<string> => {
|
|
94
|
-
if (body == null) {
|
|
95
|
-
return '';
|
|
96
|
-
}
|
|
97
|
-
if (typeof body === 'string') {
|
|
98
|
-
return body;
|
|
99
|
-
}
|
|
100
|
-
if (body instanceof URLSearchParams) {
|
|
101
|
-
return body.toString();
|
|
102
|
-
}
|
|
103
|
-
if (body instanceof FormData) {
|
|
104
|
-
return stringifyFormData(body);
|
|
105
|
-
}
|
|
106
|
-
if (body instanceof Blob) {
|
|
107
|
-
return body.text();
|
|
108
|
-
}
|
|
109
|
-
if (body instanceof ArrayBuffer) {
|
|
110
|
-
return new TextDecoder().decode(body);
|
|
111
|
-
}
|
|
112
|
-
if (ArrayBuffer.isView(body)) {
|
|
113
|
-
return new TextDecoder().decode(body);
|
|
114
|
-
}
|
|
115
|
-
if (body instanceof Document) {
|
|
116
|
-
return new XMLSerializer().serializeToString(body);
|
|
117
|
-
}
|
|
118
|
-
return String(body);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const createBodySnapshot = async (
|
|
122
|
-
body: HttpBodyInput,
|
|
123
|
-
maxBodySize: number,
|
|
124
|
-
contentType?: string,
|
|
125
|
-
): Promise<BodySnapshot | undefined> => {
|
|
126
|
-
const rawBody = await stringifyBody(body);
|
|
127
|
-
if (!rawBody) {
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
return {
|
|
131
|
-
...truncateBody(rawBody, maxBodySize),
|
|
132
|
-
contentType,
|
|
133
|
-
};
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const safeCreateBodySnapshot = async (body: HttpBodyInput, maxBodySize: number, contentType?: string) => {
|
|
137
|
-
try {
|
|
138
|
-
return await createBodySnapshot(body, maxBodySize, contentType);
|
|
139
|
-
} catch {
|
|
140
|
-
return undefined;
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const getFetchInputUrl = (input: RequestInfo | URL) => {
|
|
145
|
-
if (input instanceof Request) {
|
|
146
|
-
return input.url;
|
|
147
|
-
}
|
|
148
|
-
return String(input);
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const getFetchMethod = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
152
|
-
if (init?.method) {
|
|
153
|
-
return init.method;
|
|
154
|
-
}
|
|
155
|
-
if (input instanceof Request) {
|
|
156
|
-
return input.method;
|
|
157
|
-
}
|
|
158
|
-
return 'GET';
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const getFetchRequestBody = async (input: RequestInfo | URL, init: RequestInit | undefined, maxBodySize: number) => {
|
|
162
|
-
if (init?.body) {
|
|
163
|
-
return safeCreateBodySnapshot(
|
|
164
|
-
init.body,
|
|
165
|
-
maxBodySize,
|
|
166
|
-
getHeaderValue(init.headers as Headers | undefined, 'content-type'),
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
if (input instanceof Request) {
|
|
170
|
-
try {
|
|
171
|
-
return safeCreateBodySnapshot(
|
|
172
|
-
await input.clone().text(),
|
|
173
|
-
maxBodySize,
|
|
174
|
-
input.headers.get('content-type') || undefined,
|
|
175
|
-
);
|
|
176
|
-
} catch {
|
|
177
|
-
return undefined;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return undefined;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const getResponseBody = async (response: Response, maxBodySize: number) => {
|
|
184
|
-
try {
|
|
185
|
-
const clonedResponse = response.clone();
|
|
186
|
-
return safeCreateBodySnapshot(
|
|
187
|
-
clonedResponse.body ? await clonedResponse.text() : '',
|
|
188
|
-
maxBodySize,
|
|
189
|
-
response.headers.get('content-type') || undefined,
|
|
190
|
-
);
|
|
191
|
-
} catch {
|
|
192
|
-
return undefined;
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const getXhrResponseBody = (xhr: XMLHttpRequest, maxBodySize: number): BodySnapshot | undefined => {
|
|
197
|
-
if (xhr.responseType === '' || xhr.responseType === 'text') {
|
|
198
|
-
return {
|
|
199
|
-
...truncateBody(xhr.responseText || '', maxBodySize),
|
|
200
|
-
contentType: xhr.getResponseHeader('content-type') || undefined,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
if (xhr.responseType === 'json') {
|
|
204
|
-
return {
|
|
205
|
-
...truncateBody(JSON.stringify(xhr.response), maxBodySize),
|
|
206
|
-
contentType: xhr.getResponseHeader('content-type') || undefined,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return undefined;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const redactBody = (
|
|
213
|
-
config: Required<BkOTHttpBodyConfig>,
|
|
214
|
-
payload: Omit<BkOTHttpBodyRedactPayload, 'body' | 'truncated'>,
|
|
215
|
-
snapshot?: BodySnapshot,
|
|
216
|
-
) => {
|
|
217
|
-
if (!snapshot) {
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
return config.redact({
|
|
221
|
-
...payload,
|
|
222
|
-
body: snapshot.body,
|
|
223
|
-
truncated: snapshot.truncated,
|
|
224
|
-
});
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const reportHttpBody = ({
|
|
228
|
-
context,
|
|
229
|
-
duration,
|
|
230
|
-
error,
|
|
231
|
-
method,
|
|
232
|
-
request,
|
|
233
|
-
response,
|
|
234
|
-
status,
|
|
235
|
-
url,
|
|
236
|
-
}: ReportHttpBodyOptions) => {
|
|
237
|
-
const config = context.config.rum.httpBody;
|
|
238
|
-
if (!config) {
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const isError = Boolean(error) || (typeof status === 'number' && status >= 400);
|
|
243
|
-
const normalizedMethod = method.toUpperCase();
|
|
244
|
-
const requestBody = isError
|
|
245
|
-
? redactBody(
|
|
246
|
-
config,
|
|
247
|
-
{
|
|
248
|
-
contentType: request?.contentType,
|
|
249
|
-
method: normalizedMethod,
|
|
250
|
-
status,
|
|
251
|
-
type: 'request',
|
|
252
|
-
url,
|
|
253
|
-
},
|
|
254
|
-
request,
|
|
255
|
-
)
|
|
256
|
-
: undefined;
|
|
257
|
-
const responseBody = isError
|
|
258
|
-
? redactBody(
|
|
259
|
-
config,
|
|
260
|
-
{
|
|
261
|
-
contentType: response?.contentType,
|
|
262
|
-
method: normalizedMethod,
|
|
263
|
-
status,
|
|
264
|
-
type: 'response',
|
|
265
|
-
url,
|
|
266
|
-
},
|
|
267
|
-
response,
|
|
268
|
-
)
|
|
269
|
-
: undefined;
|
|
270
|
-
const attributes: Attributes = {
|
|
271
|
-
...context.config.getPageAttributes(),
|
|
272
|
-
'http.duration': duration,
|
|
273
|
-
'http.request.method': normalizedMethod,
|
|
274
|
-
'url.full': context.config.redactUrl(url),
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
if (typeof status === 'number') {
|
|
278
|
-
attributes['http.response.status_code'] = status;
|
|
279
|
-
}
|
|
280
|
-
if (requestBody != null) {
|
|
281
|
-
attributes['http.request.body'] = requestBody;
|
|
282
|
-
}
|
|
283
|
-
if (responseBody != null) {
|
|
284
|
-
attributes['http.response.body'] = responseBody;
|
|
285
|
-
}
|
|
286
|
-
if (isError) {
|
|
287
|
-
attributes['bk.rum.http_body.request.truncated'] = request?.truncated ?? false;
|
|
288
|
-
attributes['bk.rum.http_body.response.truncated'] = response?.truncated ?? false;
|
|
289
|
-
}
|
|
290
|
-
if (error instanceof Error) {
|
|
291
|
-
attributes['exception.message'] = error.message;
|
|
292
|
-
attributes['exception.type'] = error.name;
|
|
293
|
-
attributes['exception.stacktrace'] = error.stack ?? '';
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
context.emitLog({
|
|
297
|
-
severityNumber: isError ? SeverityNumber.ERROR : SeverityNumber.INFO,
|
|
298
|
-
severityText: isError ? 'ERROR' : 'INFO',
|
|
299
|
-
body: isError ? 'HTTP request completed with error' : 'HTTP request completed',
|
|
300
|
-
attributes,
|
|
301
|
-
});
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
export const createHttpBodyPlugin = (option: false | Required<BkOTHttpBodyConfig>): BkOTPlugin => {
|
|
305
|
-
const teardownCallbacks: Array<() => void> = [];
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
name: 'http-body',
|
|
309
|
-
enabled: Boolean(option),
|
|
310
|
-
init(context) {
|
|
311
|
-
if (!option || typeof window === 'undefined') {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const maxBodySize = option.maxBodySize || HTTP_BODY_MAX_SIZE;
|
|
316
|
-
const originalFetch = window.fetch;
|
|
317
|
-
if (typeof originalFetch === 'function') {
|
|
318
|
-
window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
319
|
-
const url = getFetchInputUrl(input);
|
|
320
|
-
if (shouldIgnoreUrl(context.config, url)) {
|
|
321
|
-
return originalFetch(input, init);
|
|
322
|
-
}
|
|
323
|
-
const method = getFetchMethod(input, init);
|
|
324
|
-
const request = await getFetchRequestBody(input, init, maxBodySize);
|
|
325
|
-
const startTime = performance.now();
|
|
326
|
-
try {
|
|
327
|
-
const response = await originalFetch(input, init);
|
|
328
|
-
const duration = performance.now() - startTime;
|
|
329
|
-
const responseBody = response.status >= 400 ? await getResponseBody(response, maxBodySize) : undefined;
|
|
330
|
-
reportHttpBody({
|
|
331
|
-
context,
|
|
332
|
-
duration,
|
|
333
|
-
method,
|
|
334
|
-
request,
|
|
335
|
-
response: responseBody,
|
|
336
|
-
status: response.status,
|
|
337
|
-
url,
|
|
338
|
-
});
|
|
339
|
-
return response;
|
|
340
|
-
} catch (error) {
|
|
341
|
-
reportHttpBody({
|
|
342
|
-
context,
|
|
343
|
-
duration: performance.now() - startTime,
|
|
344
|
-
error,
|
|
345
|
-
method,
|
|
346
|
-
request,
|
|
347
|
-
url,
|
|
348
|
-
});
|
|
349
|
-
throw error;
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
teardownCallbacks.push(() => {
|
|
353
|
-
window.fetch = originalFetch;
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const originalOpen = XMLHttpRequest.prototype.open;
|
|
358
|
-
const originalSend = XMLHttpRequest.prototype.send;
|
|
359
|
-
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
360
|
-
XMLHttpRequest.prototype.open = function open(
|
|
361
|
-
this: XMLHttpRequest,
|
|
362
|
-
method: string,
|
|
363
|
-
url: string | URL,
|
|
364
|
-
...args: [async?: boolean, username?: null | string, password?: null | string]
|
|
365
|
-
) {
|
|
366
|
-
this.__bkOtHttpBodyMeta__ = {
|
|
367
|
-
method,
|
|
368
|
-
startTime: performance.now(),
|
|
369
|
-
url: String(url),
|
|
370
|
-
};
|
|
371
|
-
return originalOpen.call(this, method, url, ...args);
|
|
372
|
-
};
|
|
373
|
-
XMLHttpRequest.prototype.setRequestHeader = function setRequestHeader(
|
|
374
|
-
this: XMLHttpRequest,
|
|
375
|
-
name: string,
|
|
376
|
-
value: string,
|
|
377
|
-
) {
|
|
378
|
-
this.__bkOtHttpBodyRequestHeaders__ = {
|
|
379
|
-
...(this.__bkOtHttpBodyRequestHeaders__ ?? {}),
|
|
380
|
-
[name]: value,
|
|
381
|
-
};
|
|
382
|
-
return originalSetRequestHeader.call(this, name, value);
|
|
383
|
-
};
|
|
384
|
-
XMLHttpRequest.prototype.send = function send(
|
|
385
|
-
this: XMLHttpRequest,
|
|
386
|
-
body?: Document | null | XMLHttpRequestBodyInit,
|
|
387
|
-
) {
|
|
388
|
-
void safeCreateBodySnapshot(
|
|
389
|
-
body,
|
|
390
|
-
maxBodySize,
|
|
391
|
-
getHeaderValue(this.__bkOtHttpBodyRequestHeaders__, 'content-type'),
|
|
392
|
-
).then(request => {
|
|
393
|
-
this.__bkOtHttpBodyRequest__ = request;
|
|
394
|
-
});
|
|
395
|
-
this.addEventListener('loadend', () => {
|
|
396
|
-
const meta = this.__bkOtHttpBodyMeta__;
|
|
397
|
-
if (!meta || shouldIgnoreUrl(context.config, meta.url)) {
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
const responseBody = this.status >= 400 ? getXhrResponseBody(this, maxBodySize) : undefined;
|
|
401
|
-
reportHttpBody({
|
|
402
|
-
context,
|
|
403
|
-
duration: performance.now() - meta.startTime,
|
|
404
|
-
method: meta.method,
|
|
405
|
-
request: this.__bkOtHttpBodyRequest__,
|
|
406
|
-
response: responseBody,
|
|
407
|
-
status: this.status,
|
|
408
|
-
url: meta.url,
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
return originalSend.call(this, body);
|
|
412
|
-
};
|
|
413
|
-
teardownCallbacks.push(() => {
|
|
414
|
-
XMLHttpRequest.prototype.open = originalOpen;
|
|
415
|
-
XMLHttpRequest.prototype.send = originalSend;
|
|
416
|
-
XMLHttpRequest.prototype.setRequestHeader = originalSetRequestHeader;
|
|
417
|
-
});
|
|
418
|
-
},
|
|
419
|
-
shutdown() {
|
|
420
|
-
while (teardownCallbacks.length) {
|
|
421
|
-
teardownCallbacks.pop()?.();
|
|
422
|
-
}
|
|
423
|
-
},
|
|
424
|
-
};
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
declare global {
|
|
428
|
-
interface XMLHttpRequest {
|
|
429
|
-
__bkOtHttpBodyRequest__?: BodySnapshot;
|
|
430
|
-
__bkOtHttpBodyRequestHeaders__?: Record<string, string>;
|
|
431
|
-
__bkOtHttpBodyMeta__?: {
|
|
432
|
-
method: string;
|
|
433
|
-
startTime: number;
|
|
434
|
-
url: string;
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
}
|