@opentelemetry/browser-instrumentation 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/fetch/index.d.ts +3 -0
- package/dist/fetch/index.js +2 -0
- package/dist/fetch/instrumentation.d.ts +36 -0
- package/dist/fetch/instrumentation.js +223 -0
- package/dist/fetch/instrumentation.js.map +1 -0
- package/dist/fetch/semconv.js +13 -0
- package/dist/fetch/semconv.js.map +1 -0
- package/dist/fetch/types.d.ts +52 -0
- package/dist/package.js +1 -1
- package/dist/resource-timing/instrumentation.js +12 -11
- package/dist/resource-timing/instrumentation.js.map +1 -1
- package/dist/utils/ContextRegistry.js +77 -0
- package/dist/utils/ContextRegistry.js.map +1 -0
- package/dist/utils/NetworkContextRegistry.js +23 -0
- package/dist/utils/NetworkContextRegistry.js.map +1 -0
- package/dist/utils/getElementCSSSelector.js.map +1 -1
- package/dist/utils/request.js +131 -0
- package/dist/utils/request.js.map +1 -0
- package/dist/utils/url.js +36 -0
- package/dist/utils/url.js.map +1 -0
- package/dist/xhr/index.d.ts +3 -0
- package/dist/xhr/index.js +2 -0
- package/dist/xhr/instrumentation.d.ts +39 -0
- package/dist/xhr/instrumentation.js +187 -0
- package/dist/xhr/instrumentation.js.map +1 -0
- package/dist/xhr/semconv.js +13 -0
- package/dist/xhr/semconv.js.map +1 -0
- package/dist/xhr/types.d.ts +23 -0
- package/package.json +13 -6
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ npm install @opentelemetry/browser-instrumentation
|
|
|
20
20
|
- [Web Vitals](#web-vitals) — automatic instrumentation for Core Web Vitals
|
|
21
21
|
- [Console](#console) — automatic instrumentation for console API calls (log, warn, error, info, debug)
|
|
22
22
|
- [Errors](#errors) — automatic instrumentation for unhandled errors and promise rejections
|
|
23
|
+
- [Fetch](#fetch) — automatic instrumentation for request using the `fetch` API
|
|
23
24
|
|
|
24
25
|
## Usage
|
|
25
26
|
|
|
@@ -300,6 +301,98 @@ Each `exception` event includes:
|
|
|
300
301
|
| `exception.message` | The error's `message`, or the thrown string itself. |
|
|
301
302
|
| `exception.stacktrace` | The error's `stack` (omitted when the thrown value is a string). |
|
|
302
303
|
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
### Fetch
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { FetchInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/fetch';
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Emits a Span for every HTTP request made using the `window.fetch` browser API. This instrumentation also propagates the context for distributed traces.
|
|
313
|
+
|
|
314
|
+
#### Configuration
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
new FetchInstrumentation({
|
|
318
|
+
ignoreUrls: ['https://example.com/api'],
|
|
319
|
+
propagateTraceHeaderCorsUrls: [/domain.com\/api.*/],
|
|
320
|
+
measureRequestSize: true,
|
|
321
|
+
applyCustomAttributesOnSpan: (span, request, result) => {
|
|
322
|
+
span.setAttribute('foo', 'bar');
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
| Option | Type | Default | Description |
|
|
328
|
+
|--------|------|---------|-------------|
|
|
329
|
+
| `ignoreUrls` | `Array<string \| RegExp>` | — | List of URLs that we do not want its requests instrumented. You can use strings for a single match or regular expressions to match many. |
|
|
330
|
+
| `propagateTraceHeaderCorsUrls` | `Array<string \| RegExp>` | — | List of cross origin URLs to appens trace context headers. You can use strings for a single match or regular expressions to match many. |
|
|
331
|
+
| `measureRequestSize` | `boolean` | — | Set it to `true` to add the request size as a Span attribute. |
|
|
332
|
+
| `sanitizeUrl` | `(url: string) => string` | — | Returns the given URL with sensitive fields `REDACTED`. |
|
|
333
|
+
| `requestHook` | `(span: Span, request: Request) => void` | — | Function that allows you to interact with the Span and the Request object before the request starts. |
|
|
334
|
+
| `applyCustomAttributesOnSpan` | `(span: Span, request: Request, result: Response \| FetchError) => void` | — | Function that allows you to interact with the Span once the request is finished. |
|
|
335
|
+
|
|
336
|
+
#### Captured Attributes
|
|
337
|
+
|
|
338
|
+
Each `fetch` Span includes:
|
|
339
|
+
|
|
340
|
+
| Attribute | Description |
|
|
341
|
+
|-----------|-------------|
|
|
342
|
+
| `http.request.method` | The method of the request in uppercase (GET, POST, PUT, PATCH, QUERY). |
|
|
343
|
+
| `http.request.method_original` | Original HTTP method sent by the client in the request line.. |
|
|
344
|
+
| `url.full` | Absolute URL describing a network resource according to RFC3986 (sanitized if passing `sanitizeUrl` config option). |
|
|
345
|
+
| `server.address` | The hostmane of the request's URL. |
|
|
346
|
+
| `server.port` | The port of the request's URL. |
|
|
347
|
+
| `http.response.status_code` | HTTP response status code. |
|
|
348
|
+
| `error.type` | If request failed. Describes a class of error the operation ended with. |
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
### XHR (XmlHttpRequest)
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { XhrInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/xhr';
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Emits a Span for every HTTP request made using the `window.XmlHttpRequest` browser API. This instrumentation also propagates the context for distributed traces.
|
|
359
|
+
|
|
360
|
+
#### Configuration
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
new XhrInstrumentation({
|
|
364
|
+
ignoreUrls: ['https://example.com/api'],
|
|
365
|
+
propagateTraceHeaderCorsUrls: [/domain.com\/api.*/],
|
|
366
|
+
measureRequestSize: true,
|
|
367
|
+
applyCustomAttributesOnSpan: (span, xhr) => {
|
|
368
|
+
span.setAttribute('foo', 'bar');
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
| Option | Type | Default | Description |
|
|
374
|
+
|--------|------|---------|-------------|
|
|
375
|
+
| `ignoreUrls` | `Array<string \| RegExp>` | — | List of URLs that we do not want its requests instrumented. You can use strings for a single match or regular expressions to match many. |
|
|
376
|
+
| `propagateTraceHeaderCorsUrls` | `Array<string \| RegExp>` | — | List of cross origin URLs to appens trace context headers. You can use strings for a single match or regular expressions to match many. |
|
|
377
|
+
| `measureRequestSize` | `boolean` | — | Set it to `true` to add the request size as a Span attribute. |
|
|
378
|
+
| `sanitizeUrl` | `(url: string) => string` | — | Returns the given URL with sensitive fields `REDACTED`. |
|
|
379
|
+
| `applyCustomAttributesOnSpan` | `(span: Span, request: Request, result: Response \| FetchError) => void` | — | Function that allows you to interact with the Span once the request is finished. |
|
|
380
|
+
|
|
381
|
+
#### Captured Attributes
|
|
382
|
+
|
|
383
|
+
Each `fetch` Span includes:
|
|
384
|
+
|
|
385
|
+
| Attribute | Description |
|
|
386
|
+
|-----------|-------------|
|
|
387
|
+
| `http.request.method` | The method of the request in uppercase (GET, POST, PUT, PATCH, QUERY). |
|
|
388
|
+
| `http.request.method_original` | Original HTTP method sent by the client in the request line.. |
|
|
389
|
+
| `url.full` | Absolute URL describing a network resource according to RFC3986 (sanitized if passing `sanitizeUrl` config option). |
|
|
390
|
+
| `server.address` | The hostmane of the request's URL. |
|
|
391
|
+
| `server.port` | The port of the request's URL. |
|
|
392
|
+
| `http.response.status_code` | HTTP response status code. |
|
|
393
|
+
| `error.type` | If request failed. Describes a class of error the operation ended with. |
|
|
394
|
+
|
|
395
|
+
|
|
303
396
|
## Useful links
|
|
304
397
|
|
|
305
398
|
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { FetchCustomAttributeFunction, FetchError, FetchInstrumentationConfig, FetchRequestHookFunction, FetchResponse } from "./types.js";
|
|
2
|
+
import { FetchInstrumentation } from "./instrumentation.js";
|
|
3
|
+
export { type FetchCustomAttributeFunction, type FetchError, FetchInstrumentation, type FetchInstrumentationConfig, type FetchRequestHookFunction, type FetchResponse };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FetchInstrumentationConfig } from "./types.js";
|
|
2
|
+
import { InstrumentationBase } from "@opentelemetry/instrumentation";
|
|
3
|
+
//#region src/fetch/instrumentation.d.ts
|
|
4
|
+
declare class FetchInstrumentation extends InstrumentationBase<FetchInstrumentationConfig> {
|
|
5
|
+
private _isEnabled;
|
|
6
|
+
private _isFetchPatched;
|
|
7
|
+
private _spanResources;
|
|
8
|
+
constructor(config?: FetchInstrumentationConfig);
|
|
9
|
+
protected init(): never[];
|
|
10
|
+
enable(): void;
|
|
11
|
+
disable(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Patches the constructor of fetch
|
|
14
|
+
*/
|
|
15
|
+
private _patchConstructor;
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new span
|
|
18
|
+
*/
|
|
19
|
+
private _createSpan;
|
|
20
|
+
/**
|
|
21
|
+
* Finish span, add attributes, etc.
|
|
22
|
+
*/
|
|
23
|
+
private _endSpan;
|
|
24
|
+
private _applyAttributesAfterFetch;
|
|
25
|
+
/**
|
|
26
|
+
* Calls the request hook if defined
|
|
27
|
+
*/
|
|
28
|
+
private _callRequestHook;
|
|
29
|
+
/**
|
|
30
|
+
* Add headers for the request and the given context if apply
|
|
31
|
+
*/
|
|
32
|
+
private _addHeaders;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { FetchInstrumentation };
|
|
36
|
+
//# sourceMappingURL=instrumentation.d.ts.map
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { version } from "../package.js";
|
|
2
|
+
import { getNetworkContextRegistry } from "../utils/NetworkContextRegistry.js";
|
|
3
|
+
import { getFetchBodyLength, normalizeHttpRequestMethod } from "../utils/request.js";
|
|
4
|
+
import { matchesUrl, parseUrl, serverPortFromUrl } from "../utils/url.js";
|
|
5
|
+
import { ATTR_HTTP_REQUEST_BODY_SIZE } from "./semconv.js";
|
|
6
|
+
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
7
|
+
import { InstrumentationBase, safeExecuteInTheMiddle } from "@opentelemetry/instrumentation";
|
|
8
|
+
import { ATTR_ERROR_TYPE, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_REQUEST_METHOD_ORIGINAL, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
9
|
+
//#region src/fetch/instrumentation.ts
|
|
10
|
+
const hasBrowserPerformanceAPI = typeof PerformanceObserver !== "undefined";
|
|
11
|
+
var FetchInstrumentation = class extends InstrumentationBase {
|
|
12
|
+
_spanResources = /* @__PURE__ */ new Map();
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
super("@opentelemetry/browser-instrumentation/fetch", version, config);
|
|
15
|
+
}
|
|
16
|
+
init() {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
enable() {
|
|
20
|
+
if (!hasBrowserPerformanceAPI) {
|
|
21
|
+
this._diag.warn("this instrumentation is intended for web usage only, it does not instrument server-side fetch()");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (this._isEnabled) return;
|
|
25
|
+
if (this._isFetchPatched) {
|
|
26
|
+
this._diag.debug("fetch constructor already patched");
|
|
27
|
+
this._isEnabled = true;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
this._wrap(globalThis, "fetch", this._patchConstructor());
|
|
32
|
+
this._isFetchPatched = true;
|
|
33
|
+
this._isEnabled = true;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
this._diag.warn("Failed to patch globalThis.fetch; instrumentation will not be enabled. Another script may have locked globalThis.fetch via Object.defineProperty.", err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
disable() {
|
|
39
|
+
if (!hasBrowserPerformanceAPI) return;
|
|
40
|
+
if (!this._isEnabled) return;
|
|
41
|
+
this._isEnabled = false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Patches the constructor of fetch
|
|
45
|
+
*/
|
|
46
|
+
_patchConstructor() {
|
|
47
|
+
return (original) => {
|
|
48
|
+
const instrumentation = this;
|
|
49
|
+
return function patchConstructor(...args) {
|
|
50
|
+
if (!instrumentation._isEnabled) return original.apply(this, args);
|
|
51
|
+
let trucatedSpan;
|
|
52
|
+
try {
|
|
53
|
+
const instrConfig = instrumentation.getConfig();
|
|
54
|
+
const url = parseUrl(args[0] instanceof Request ? args[0].url : String(args[0])).href;
|
|
55
|
+
if (matchesUrl(url, instrConfig.ignoreUrls)) return original.apply(this, args);
|
|
56
|
+
let options;
|
|
57
|
+
if (args[0] instanceof Request) options = args[1] != null ? new Request(args[0], args[1]) : args[0];
|
|
58
|
+
else options = args[1] || {};
|
|
59
|
+
const createdSpan = instrumentation._createSpan(url, options);
|
|
60
|
+
trucatedSpan = createdSpan;
|
|
61
|
+
if (instrConfig.measureRequestSize) getFetchBodyLength(...args).then((bodyLength) => {
|
|
62
|
+
if (typeof bodyLength === "number") createdSpan.setAttribute(ATTR_HTTP_REQUEST_BODY_SIZE, bodyLength);
|
|
63
|
+
}).catch((error) => {
|
|
64
|
+
instrumentation._diag.warn("getFetchBodyLength", error);
|
|
65
|
+
});
|
|
66
|
+
function endSpanOnError(span, error) {
|
|
67
|
+
instrumentation._applyAttributesAfterFetch(span, options, error);
|
|
68
|
+
instrumentation._endSpan(span, {
|
|
69
|
+
status: error.status,
|
|
70
|
+
error: error.name || error.message,
|
|
71
|
+
aborted: error.name === "AbortError"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function endSpanOnSuccess(span, response) {
|
|
75
|
+
instrumentation._applyAttributesAfterFetch(span, options, response);
|
|
76
|
+
instrumentation._endSpan(span, response);
|
|
77
|
+
}
|
|
78
|
+
function onSuccess(span, response) {
|
|
79
|
+
try {
|
|
80
|
+
const body = response.clone().body;
|
|
81
|
+
if (body) {
|
|
82
|
+
const reader = body.getReader();
|
|
83
|
+
const read = () => {
|
|
84
|
+
reader.read().then(({ done }) => {
|
|
85
|
+
try {
|
|
86
|
+
if (done) endSpanOnSuccess(span, response);
|
|
87
|
+
else read();
|
|
88
|
+
} catch (e) {
|
|
89
|
+
instrumentation._diag.error("Failed to finalize span after body read", e);
|
|
90
|
+
instrumentation._endSpan(span, { status: response.status });
|
|
91
|
+
}
|
|
92
|
+
}, (error) => {
|
|
93
|
+
try {
|
|
94
|
+
endSpanOnError(span, {
|
|
95
|
+
name: error?.name,
|
|
96
|
+
message: error?.message,
|
|
97
|
+
status: response.status
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
instrumentation._diag.error("Failed to end span after body read rejected", e);
|
|
101
|
+
instrumentation._endSpan(span, { status: response.status });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
read();
|
|
106
|
+
} else endSpanOnSuccess(span, response);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
instrumentation._diag.error("Failed to read fetch response body", error);
|
|
109
|
+
instrumentation._endSpan(span, { status: response.status });
|
|
110
|
+
}
|
|
111
|
+
return response;
|
|
112
|
+
}
|
|
113
|
+
function onError(span, error) {
|
|
114
|
+
try {
|
|
115
|
+
endSpanOnError(span, error);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
instrumentation._diag.error("Failed to end span on fetch error", e);
|
|
118
|
+
instrumentation._endSpan(span, {});
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
const fetchContext = trace.setSpan(context.active(), createdSpan);
|
|
123
|
+
return context.with(fetchContext, () => {
|
|
124
|
+
instrumentation._spanResources.set(createdSpan, {
|
|
125
|
+
name: url,
|
|
126
|
+
fetchStart: performance.now()
|
|
127
|
+
});
|
|
128
|
+
instrumentation._callRequestHook(createdSpan, options);
|
|
129
|
+
instrumentation._addHeaders(options, url, fetchContext);
|
|
130
|
+
return original.apply(globalThis, options instanceof Request ? [options] : [url, options]).then(onSuccess.bind(globalThis, createdSpan), onError.bind(globalThis, createdSpan));
|
|
131
|
+
});
|
|
132
|
+
} catch (e) {
|
|
133
|
+
instrumentation._diag.error("Failed to instrument fetch request", e);
|
|
134
|
+
}
|
|
135
|
+
if (trucatedSpan) instrumentation._spanResources.delete(trucatedSpan);
|
|
136
|
+
return original.apply(this, args);
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Creates a new span
|
|
142
|
+
*/
|
|
143
|
+
_createSpan(url, options = {}) {
|
|
144
|
+
const attributes = {};
|
|
145
|
+
const parsedUrl = parseUrl(url);
|
|
146
|
+
const origMethod = options.method;
|
|
147
|
+
const normMethod = normalizeHttpRequestMethod(options.method || "GET");
|
|
148
|
+
const serverPort = serverPortFromUrl(parsedUrl);
|
|
149
|
+
const name = normMethod;
|
|
150
|
+
const { sanitizeUrl } = this.getConfig();
|
|
151
|
+
attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;
|
|
152
|
+
attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url) : url;
|
|
153
|
+
attributes[ATTR_SERVER_ADDRESS] = parsedUrl.hostname;
|
|
154
|
+
if (serverPort) attributes[ATTR_SERVER_PORT] = serverPort;
|
|
155
|
+
if (origMethod && origMethod !== normMethod) attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;
|
|
156
|
+
return this.tracer.startSpan(name, {
|
|
157
|
+
kind: SpanKind.CLIENT,
|
|
158
|
+
attributes
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Finish span, add attributes, etc.
|
|
163
|
+
*/
|
|
164
|
+
_endSpan(span, response) {
|
|
165
|
+
if (response.status !== void 0) span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
166
|
+
if (!response.aborted) {
|
|
167
|
+
if (response.status === void 0 || response.status >= 400 || response.error !== void 0) {
|
|
168
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
169
|
+
const errorType = response.error ?? (response.status !== void 0 ? String(response.status) : void 0);
|
|
170
|
+
if (typeof errorType === "string") span.setAttribute(ATTR_ERROR_TYPE, errorType);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
span.end();
|
|
174
|
+
const resource = this._spanResources.get(span);
|
|
175
|
+
if (resource) {
|
|
176
|
+
this._spanResources.delete(span);
|
|
177
|
+
getNetworkContextRegistry().register(span, {
|
|
178
|
+
key: resource.name,
|
|
179
|
+
startPerfNow: resource.fetchStart,
|
|
180
|
+
endPerfNow: performance.now()
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
_applyAttributesAfterFetch(span, request, result) {
|
|
185
|
+
const applyCustomAttributesOnSpan = this.getConfig().applyCustomAttributesOnSpan;
|
|
186
|
+
if (applyCustomAttributesOnSpan) safeExecuteInTheMiddle(() => applyCustomAttributesOnSpan(span, request, result), (error) => {
|
|
187
|
+
if (!error) return;
|
|
188
|
+
this._diag.error("applyCustomAttributesOnSpan", error);
|
|
189
|
+
}, true);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Calls the request hook if defined
|
|
193
|
+
*/
|
|
194
|
+
_callRequestHook(span, request) {
|
|
195
|
+
const requestHook = this.getConfig().requestHook;
|
|
196
|
+
if (requestHook) safeExecuteInTheMiddle(() => requestHook(span, request), (error) => {
|
|
197
|
+
if (error) this._diag.error("requestHook", error);
|
|
198
|
+
}, true);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Add headers for the request and the given context if apply
|
|
202
|
+
*/
|
|
203
|
+
_addHeaders(options, url, ctx) {
|
|
204
|
+
const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;
|
|
205
|
+
const urlOrigin = parseUrl(url).origin;
|
|
206
|
+
if (location.origin === urlOrigin || matchesUrl(url, urlsToPropagate)) {
|
|
207
|
+
if (options instanceof Request) propagation.inject(ctx, options.headers, { set: (h, k, v) => h.set(k, typeof v === "string" ? v : String(v)) });
|
|
208
|
+
else {
|
|
209
|
+
const headers = new Headers(options.headers);
|
|
210
|
+
propagation.inject(ctx, headers, { set: (h, k, v) => h.set(k, typeof v === "string" ? v : String(v)) });
|
|
211
|
+
options.headers = headers;
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const headers = {};
|
|
215
|
+
propagation.inject(ctx, headers);
|
|
216
|
+
if (Object.keys(headers).length > 0) this._diag.debug("headers inject skipped due to CORS policy");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
export { FetchInstrumentation };
|
|
222
|
+
|
|
223
|
+
//# sourceMappingURL=instrumentation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/fetch/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes, Context, Span } from '@opentelemetry/api';\nimport {\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n} from '@opentelemetry/api';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport {\n ATTR_ERROR_TYPE,\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_REQUEST_METHOD_ORIGINAL,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n} from '@opentelemetry/semantic-conventions';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { getNetworkContextRegistry } from '../utils/NetworkContextRegistry.ts';\nimport {\n getFetchBodyLength,\n normalizeHttpRequestMethod,\n} from '../utils/request.ts';\nimport { matchesUrl, parseUrl, serverPortFromUrl } from '../utils/url.ts';\nimport { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv.ts';\nimport type {\n FetchError,\n FetchInstrumentationConfig,\n FetchResponse,\n} from './types.ts';\n\nconst hasBrowserPerformanceAPI = typeof PerformanceObserver !== 'undefined';\n\nexport class FetchInstrumentation extends InstrumentationBase<FetchInstrumentationConfig> {\n // Note: Intentionally *not* using `_enabled` as the field name to avoid\n // any possible confusion with the `_enabled` field used on the *Node.js*\n // InstrumentationBase class.\n // Also not initializing the fields to `false` because the base class\n // constructor already call `enable` modifying their values and it will\n // set the instrumentations in a base state (enabled, patched but with flags set to false)\n private declare _isEnabled: boolean;\n private declare _isFetchPatched: boolean;\n\n // To keep track of the resources\n private _spanResources: Map<Span, PerformanceResourceTiming> = new Map();\n\n constructor(config: FetchInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/fetch', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (!hasBrowserPerformanceAPI) {\n this._diag.warn(\n 'this instrumentation is intended for web usage only, it does not instrument server-side fetch()',\n );\n return;\n }\n\n if (this._isEnabled) {\n return;\n }\n\n if (this._isFetchPatched) {\n this._diag.debug('fetch constructor already patched');\n this._isEnabled = true;\n return;\n }\n\n try {\n // `_wrap` throws if a third-party script has locked globalThis.fetch via\n // Object.defineProperty(window, 'fetch', { writable: false, ... }).\n this._wrap(globalThis, 'fetch', this._patchConstructor());\n this._isFetchPatched = true;\n this._isEnabled = true;\n } catch (err) {\n this._diag.warn(\n 'Failed to patch globalThis.fetch; instrumentation will not be enabled. ' +\n 'Another script may have locked globalThis.fetch via Object.defineProperty.',\n err,\n );\n }\n }\n\n override disable(): void {\n if (!hasBrowserPerformanceAPI) {\n return;\n }\n\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n }\n\n /**\n * Patches the constructor of fetch\n */\n private _patchConstructor(): (original: typeof fetch) => typeof fetch {\n return (original) => {\n const instrumentation = this;\n\n return function patchConstructor(\n this: typeof globalThis,\n ...args: Parameters<typeof fetch>\n ): Promise<Response> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n\n // Reference to the Span in case there is an exception after creating it (wil not end)\n // It will be used to keep the Map only with the Spans that will be ended\n let trucatedSpan: Span | undefined;\n try {\n const instrConfig = instrumentation.getConfig();\n const url = parseUrl(\n args[0] instanceof Request ? args[0].url : String(args[0]),\n ).href;\n\n const shouldIgnoreUrl = matchesUrl(url, instrConfig.ignoreUrls);\n if (shouldIgnoreUrl) {\n return original.apply(this, args);\n }\n\n // Per the Fetch spec, when fetch() is called with a Request object\n // and a separate init object, the init properties override the\n // Request's properties. Merge them into a new Request so that\n // downstream consumers (hooks, header injection, the actual fetch\n // call) see the correct final values.\n // See: https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#parameters\n let options: Request | RequestInit;\n if (args[0] instanceof Request) {\n options = args[1] != null ? new Request(args[0], args[1]) : args[0];\n } else {\n options = args[1] || {};\n }\n\n const createdSpan = instrumentation._createSpan(url, options);\n trucatedSpan = createdSpan;\n if (instrConfig.measureRequestSize) {\n getFetchBodyLength(...args)\n .then((bodyLength) => {\n if (typeof bodyLength === 'number') {\n createdSpan.setAttribute(\n ATTR_HTTP_REQUEST_BODY_SIZE,\n bodyLength,\n );\n }\n })\n .catch((error) => {\n instrumentation._diag.warn('getFetchBodyLength', error);\n });\n }\n\n function endSpanOnError(span: Span, error: FetchError) {\n instrumentation._applyAttributesAfterFetch(span, options, error);\n instrumentation._endSpan(span, {\n status: error.status,\n error: error.name || error.message,\n aborted: error.name === 'AbortError',\n });\n }\n\n function endSpanOnSuccess(span: Span, response: Response) {\n instrumentation._applyAttributesAfterFetch(span, options, response);\n instrumentation._endSpan(span, response);\n }\n\n function onSuccess(span: Span, response: Response): Response {\n try {\n // Clone the response and eagerly consume the clone to detect\n // when the body transfer completes. The original response is\n // returned to the caller untouched so that it passes internal\n // brand-checks required by APIs such as\n // WebAssembly.compileStreaming.\n // It consumes the entire body even if the user cancels reading it\n // from the original response. But it does work with `AbortController.abort()`\n // because it aborts the underlying fetch cancelling the original and clone streams\n // ref: https://github.com/open-telemetry/opentelemetry-js/pull/6521\n const resClone = response.clone();\n const body = resClone.body;\n if (body) {\n const reader = body.getReader();\n\n const read = (): void => {\n reader.read().then(\n ({ done }) => {\n try {\n if (done) {\n endSpanOnSuccess(span, response);\n } else {\n read();\n }\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to finalize span after body read',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n (error) => {\n try {\n endSpanOnError(span, {\n name: error?.name,\n message: error?.message,\n status: response.status,\n });\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to end span after body read rejected',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n );\n };\n read();\n } else {\n // some older browsers don't have .body implemented\n endSpanOnSuccess(span, response);\n }\n } catch (error) {\n instrumentation._diag.error(\n 'Failed to read fetch response body',\n error,\n );\n instrumentation._endSpan(span, { status: response.status });\n }\n return response;\n }\n\n function onError(span: Span, error: FetchError): never {\n try {\n endSpanOnError(span, error);\n } catch (e: unknown) {\n // endSpanOnError failed — fall back to ending the span directly.\n // No `status`: no response was ever received for this request.\n instrumentation._diag.error(\n 'Failed to end span on fetch error',\n e,\n );\n instrumentation._endSpan(span, {});\n }\n throw error;\n }\n\n const fetchContext = trace.setSpan(context.active(), createdSpan);\n return context.with(fetchContext, () => {\n instrumentation._spanResources.set(createdSpan, {\n name: url,\n fetchStart: performance.now(),\n } as PerformanceResourceTiming);\n\n // Call request hook before injection so hooks cannot tamper with propagation headers.\n // Also, this means the hook will see `options.headers` in the same type as passed in,\n // rather than as a `Headers` instance set by `_addHeaders()`.\n instrumentation._callRequestHook(createdSpan, options);\n instrumentation._addHeaders(options, url, fetchContext);\n\n return original\n .apply(\n globalThis,\n options instanceof Request ? [options] : [url, options],\n )\n .then(\n onSuccess.bind(globalThis, createdSpan),\n onError.bind(globalThis, createdSpan),\n );\n });\n } catch (e: unknown) {\n instrumentation._diag.error('Failed to instrument fetch request', e);\n }\n // Remove the span if it was created and stored before the exception\n if (trucatedSpan) {\n instrumentation._spanResources.delete(trucatedSpan);\n }\n return original.apply(this, args);\n };\n };\n }\n\n /**\n * Creates a new span\n */\n private _createSpan(\n url: string,\n options: Partial<Request | RequestInit> = {},\n ): Span {\n const attributes = {} as Attributes;\n const parsedUrl = parseUrl(url);\n const origMethod = options.method;\n const normMethod = normalizeHttpRequestMethod(options.method || 'GET');\n const serverPort = serverPortFromUrl(parsedUrl);\n const name = normMethod;\n const { sanitizeUrl } = this.getConfig();\n\n attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;\n attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url) : url;\n attributes[ATTR_SERVER_ADDRESS] = parsedUrl.hostname;\n if (serverPort) {\n attributes[ATTR_SERVER_PORT] = serverPort;\n }\n if (origMethod && origMethod !== normMethod) {\n attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;\n }\n\n return this.tracer.startSpan(name, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n }\n\n /**\n * Finish span, add attributes, etc.\n */\n private _endSpan(span: Span, response: FetchResponse) {\n if (response.status !== undefined) {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);\n }\n\n // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status\n // 1xx/2xx/3xx MUST be left unset UNLESS there was another error (e.g. a\n // network error receiving the response body)\n if (!response.aborted) {\n const isErrorStatus =\n response.status === undefined ||\n response.status >= 400 ||\n response.error !== undefined;\n if (isErrorStatus) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n const errorType =\n response.error ??\n (response.status !== undefined ? String(response.status) : undefined);\n if (typeof errorType === 'string') {\n span.setAttribute(ATTR_ERROR_TYPE, errorType);\n }\n }\n }\n\n span.end();\n\n // Register the resource context for other instrumentations\n const resource = this._spanResources.get(span);\n if (resource) {\n this._spanResources.delete(span);\n getNetworkContextRegistry().register(span, {\n key: resource.name,\n startPerfNow: resource.fetchStart,\n endPerfNow: performance.now(),\n });\n }\n }\n\n private _applyAttributesAfterFetch(\n span: Span,\n request: Request | RequestInit,\n result: Response | FetchError,\n ) {\n const applyCustomAttributesOnSpan =\n this.getConfig().applyCustomAttributesOnSpan;\n if (applyCustomAttributesOnSpan) {\n safeExecuteInTheMiddle(\n () => applyCustomAttributesOnSpan(span, request, result),\n (error) => {\n if (!error) {\n return;\n }\n this._diag.error('applyCustomAttributesOnSpan', error);\n },\n true,\n );\n }\n }\n\n /**\n * Calls the request hook if defined\n */\n private _callRequestHook(span: Span, request: Request | RequestInit) {\n const requestHook = this.getConfig().requestHook;\n\n if (requestHook) {\n safeExecuteInTheMiddle(\n () => requestHook(span, request),\n (error) => {\n if (error) {\n this._diag.error('requestHook', error);\n }\n },\n true,\n );\n }\n }\n\n /**\n * Add headers for the request and the given context if apply\n */\n private _addHeaders(\n options: Request | RequestInit,\n url: string,\n ctx: Context,\n ): void {\n // Propagate only if in request goes to same origin or is in the allow list\n const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;\n const urlOrigin = parseUrl(url).origin;\n const sameOrigin = location.origin === urlOrigin;\n const shouldPropagate = sameOrigin || matchesUrl(url, urlsToPropagate);\n\n if (shouldPropagate) {\n if (options instanceof Request) {\n // This mutates `Request.headers` in-place, because it is read-only\n // (per https://developer.mozilla.org/en-US/docs/Web/API/Request/headers),\n // so we cannot (easily) replace it.\n propagation.inject(ctx, options.headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n } else {\n // Otherwise, create a new Headers to avoid mutating the caller's\n // possibly re-used headers.\n const headers = new Headers(options.headers);\n propagation.inject(ctx, headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n options.headers = headers;\n }\n } else {\n const headers: Partial<Record<string, unknown>> = {};\n propagation.inject(ctx, headers);\n if (Object.keys(headers).length > 0) {\n this._diag.debug('headers inject skipped due to CORS policy');\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;AAwCA,MAAM,2BAA2B,OAAO,wBAAwB;AAEhE,IAAa,uBAAb,cAA0C,oBAAgD;CAWxF,iCAA+D,IAAI,IAAI;CAEvE,YAAY,SAAqC,CAAC,GAAG;EACnD,MAAM,gDAAgD,SAAS,MAAM;CACvE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,SAAwB;EACtB,IAAI,CAAC,0BAA0B;GAC7B,KAAK,MAAM,KACT,iGACF;GACA;EACF;EAEA,IAAI,KAAK,YACP;EAGF,IAAI,KAAK,iBAAiB;GACxB,KAAK,MAAM,MAAM,mCAAmC;GACpD,KAAK,aAAa;GAClB;EACF;EAEA,IAAI;GAGF,KAAK,MAAM,YAAY,SAAS,KAAK,kBAAkB,CAAC;GACxD,KAAK,kBAAkB;GACvB,KAAK,aAAa;EACpB,SAAS,KAAK;GACZ,KAAK,MAAM,KACT,qJAEA,GACF;EACF;CACF;CAEA,UAAyB;EACvB,IAAI,CAAC,0BACH;EAGF,IAAI,CAAC,KAAK,YACR;EAEF,KAAK,aAAa;CACpB;;;;CAKA,oBAAsE;EACpE,QAAQ,aAAa;GACnB,MAAM,kBAAkB;GAExB,OAAO,SAAS,iBAEd,GAAG,MACgB;IACnB,IAAI,CAAC,gBAAgB,YACnB,OAAO,SAAS,MAAM,MAAM,IAAI;IAKlC,IAAI;IACJ,IAAI;KACF,MAAM,cAAc,gBAAgB,UAAU;KAC9C,MAAM,MAAM,SACV,KAAK,cAAc,UAAU,KAAK,EAAE,CAAC,MAAM,OAAO,KAAK,EAAE,CAC3D,CAAC,CAAC;KAGF,IADwB,WAAW,KAAK,YAAY,UAClC,GAChB,OAAO,SAAS,MAAM,MAAM,IAAI;KASlC,IAAI;KACJ,IAAI,KAAK,cAAc,SACrB,UAAU,KAAK,MAAM,OAAO,IAAI,QAAQ,KAAK,IAAI,KAAK,EAAE,IAAI,KAAK;UAEjE,UAAU,KAAK,MAAM,CAAC;KAGxB,MAAM,cAAc,gBAAgB,YAAY,KAAK,OAAO;KAC5D,eAAe;KACf,IAAI,YAAY,oBACd,mBAAmB,GAAG,IAAI,CAAC,CACxB,MAAM,eAAe;MACpB,IAAI,OAAO,eAAe,UACxB,YAAY,aACV,6BACA,UACF;KAEJ,CAAC,CAAC,CACD,OAAO,UAAU;MAChB,gBAAgB,MAAM,KAAK,sBAAsB,KAAK;KACxD,CAAC;KAGL,SAAS,eAAe,MAAY,OAAmB;MACrD,gBAAgB,2BAA2B,MAAM,SAAS,KAAK;MAC/D,gBAAgB,SAAS,MAAM;OAC7B,QAAQ,MAAM;OACd,OAAO,MAAM,QAAQ,MAAM;OAC3B,SAAS,MAAM,SAAS;MAC1B,CAAC;KACH;KAEA,SAAS,iBAAiB,MAAY,UAAoB;MACxD,gBAAgB,2BAA2B,MAAM,SAAS,QAAQ;MAClE,gBAAgB,SAAS,MAAM,QAAQ;KACzC;KAEA,SAAS,UAAU,MAAY,UAA8B;MAC3D,IAAI;OAWF,MAAM,OADW,SAAS,MACN,CAAC,CAAC;OACtB,IAAI,MAAM;QACR,MAAM,SAAS,KAAK,UAAU;QAE9B,MAAM,aAAmB;SACvB,OAAO,KAAK,CAAC,CAAC,MACX,EAAE,WAAW;UACZ,IAAI;WACF,IAAI,MACF,iBAAiB,MAAM,QAAQ;gBAE/B,KAAK;UAET,SAAS,GAAG;WACV,gBAAgB,MAAM,MACpB,2CACA,CACF;WACA,gBAAgB,SAAS,MAAM,EAC7B,QAAQ,SAAS,OACnB,CAAC;UACH;SACF,IACC,UAAU;UACT,IAAI;WACF,eAAe,MAAM;YACnB,MAAM,OAAO;YACb,SAAS,OAAO;YAChB,QAAQ,SAAS;WACnB,CAAC;UACH,SAAS,GAAG;WACV,gBAAgB,MAAM,MACpB,+CACA,CACF;WACA,gBAAgB,SAAS,MAAM,EAC7B,QAAQ,SAAS,OACnB,CAAC;UACH;SACF,CACF;QACF;QACA,KAAK;OACP,OAEE,iBAAiB,MAAM,QAAQ;MAEnC,SAAS,OAAO;OACd,gBAAgB,MAAM,MACpB,sCACA,KACF;OACA,gBAAgB,SAAS,MAAM,EAAE,QAAQ,SAAS,OAAO,CAAC;MAC5D;MACA,OAAO;KACT;KAEA,SAAS,QAAQ,MAAY,OAA0B;MACrD,IAAI;OACF,eAAe,MAAM,KAAK;MAC5B,SAAS,GAAY;OAGnB,gBAAgB,MAAM,MACpB,qCACA,CACF;OACA,gBAAgB,SAAS,MAAM,CAAC,CAAC;MACnC;MACA,MAAM;KACR;KAEA,MAAM,eAAe,MAAM,QAAQ,QAAQ,OAAO,GAAG,WAAW;KAChE,OAAO,QAAQ,KAAK,oBAAoB;MACtC,gBAAgB,eAAe,IAAI,aAAa;OAC9C,MAAM;OACN,YAAY,YAAY,IAAI;MAC9B,CAA8B;MAK9B,gBAAgB,iBAAiB,aAAa,OAAO;MACrD,gBAAgB,YAAY,SAAS,KAAK,YAAY;MAEtD,OAAO,SACJ,MACC,YACA,mBAAmB,UAAU,CAAC,OAAO,IAAI,CAAC,KAAK,OAAO,CACxD,CAAC,CACA,KACC,UAAU,KAAK,YAAY,WAAW,GACtC,QAAQ,KAAK,YAAY,WAAW,CACtC;KACJ,CAAC;IACH,SAAS,GAAY;KACnB,gBAAgB,MAAM,MAAM,sCAAsC,CAAC;IACrE;IAEA,IAAI,cACF,gBAAgB,eAAe,OAAO,YAAY;IAEpD,OAAO,SAAS,MAAM,MAAM,IAAI;GAClC;EACF;CACF;;;;CAKA,YACE,KACA,UAA0C,CAAC,GACrC;EACN,MAAM,aAAa,CAAC;EACpB,MAAM,YAAY,SAAS,GAAG;EAC9B,MAAM,aAAa,QAAQ;EAC3B,MAAM,aAAa,2BAA2B,QAAQ,UAAU,KAAK;EACrE,MAAM,aAAa,kBAAkB,SAAS;EAC9C,MAAM,OAAO;EACb,MAAM,EAAE,gBAAgB,KAAK,UAAU;EAEvC,WAAW,4BAA4B;EACvC,WAAW,iBAAiB,cAAc,YAAY,GAAG,IAAI;EAC7D,WAAW,uBAAuB,UAAU;EAC5C,IAAI,YACF,WAAW,oBAAoB;EAEjC,IAAI,cAAc,eAAe,YAC/B,WAAW,qCAAqC;EAGlD,OAAO,KAAK,OAAO,UAAU,MAAM;GACjC,MAAM,SAAS;GACf;EACF,CAAC;CACH;;;;CAKA,SAAiB,MAAY,UAAyB;EACpD,IAAI,SAAS,WAAW,KAAA,GACtB,KAAK,aAAa,gCAAgC,SAAS,MAAM;EAMnE,IAAI,CAAC,SAAS,SAEV;OAAA,SAAS,WAAW,KAAA,KACpB,SAAS,UAAU,OACnB,SAAS,UAAU,KAAA,GACF;IACjB,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;IAC7C,MAAM,YACJ,SAAS,UACR,SAAS,WAAW,KAAA,IAAY,OAAO,SAAS,MAAM,IAAI,KAAA;IAC7D,IAAI,OAAO,cAAc,UACvB,KAAK,aAAa,iBAAiB,SAAS;GAEhD;;EAGF,KAAK,IAAI;EAGT,MAAM,WAAW,KAAK,eAAe,IAAI,IAAI;EAC7C,IAAI,UAAU;GACZ,KAAK,eAAe,OAAO,IAAI;GAC/B,0BAA0B,CAAC,CAAC,SAAS,MAAM;IACzC,KAAK,SAAS;IACd,cAAc,SAAS;IACvB,YAAY,YAAY,IAAI;GAC9B,CAAC;EACH;CACF;CAEA,2BACE,MACA,SACA,QACA;EACA,MAAM,8BACJ,KAAK,UAAU,CAAC,CAAC;EACnB,IAAI,6BACF,6BACQ,4BAA4B,MAAM,SAAS,MAAM,IACtD,UAAU;GACT,IAAI,CAAC,OACH;GAEF,KAAK,MAAM,MAAM,+BAA+B,KAAK;EACvD,GACA,IACF;CAEJ;;;;CAKA,iBAAyB,MAAY,SAAgC;EACnE,MAAM,cAAc,KAAK,UAAU,CAAC,CAAC;EAErC,IAAI,aACF,6BACQ,YAAY,MAAM,OAAO,IAC9B,UAAU;GACT,IAAI,OACF,KAAK,MAAM,MAAM,eAAe,KAAK;EAEzC,GACA,IACF;CAEJ;;;;CAKA,YACE,SACA,KACA,KACM;EAEN,MAAM,kBAAkB,KAAK,UAAU,CAAC,CAAC;EACzC,MAAM,YAAY,SAAS,GAAG,CAAC,CAAC;EAIhC,IAHmB,SAAS,WAAW,aACD,WAAW,KAAK,eAAe,GAEhD;GACnB,IAAI,mBAAmB,SAIrB,YAAY,OAAO,KAAK,QAAQ,SAAS,EACvC,MAAM,GAAG,GAAG,MAAM,EAAE,IAAI,GAAG,OAAO,MAAM,WAAW,IAAI,OAAO,CAAC,CAAC,EAClE,CAAC;QACI;IAGL,MAAM,UAAU,IAAI,QAAQ,QAAQ,OAAO;IAC3C,YAAY,OAAO,KAAK,SAAS,EAC/B,MAAM,GAAG,GAAG,MAAM,EAAE,IAAI,GAAG,OAAO,MAAM,WAAW,IAAI,OAAO,CAAC,CAAC,EAClE,CAAC;IACD,QAAQ,UAAU;GACpB;EACF,OAAO;GACL,MAAM,UAA4C,CAAC;GACnD,YAAY,OAAO,KAAK,OAAO;GAC/B,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAChC,KAAK,MAAM,MAAM,2CAA2C;EAEhE;CACF;AACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/fetch/semconv.ts
|
|
2
|
+
/**
|
|
3
|
+
* The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size.
|
|
4
|
+
*
|
|
5
|
+
* @example 3495
|
|
6
|
+
*
|
|
7
|
+
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
|
|
8
|
+
*/
|
|
9
|
+
const ATTR_HTTP_REQUEST_BODY_SIZE = "http.request.body.size";
|
|
10
|
+
//#endregion
|
|
11
|
+
export { ATTR_HTTP_REQUEST_BODY_SIZE };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=semconv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semconv.js","names":[],"sources":["../../src/fetch/semconv.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/*\n * This file contains a copy of unstable semantic convention definitions\n * used by this package.\n * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv\n */\n\n/**\n * The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size.\n *\n * @example 3495\n *\n * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.\n */\nexport const ATTR_HTTP_REQUEST_BODY_SIZE = 'http.request.body.size' as const;\n"],"mappings":";;;;;;;;AAkBA,MAAa,8BAA8B"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Span } from "@opentelemetry/api";
|
|
2
|
+
import { InstrumentationConfig } from "@opentelemetry/instrumentation";
|
|
3
|
+
//#region src/fetch/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Interface used to provide information to finish span on fetch response.
|
|
6
|
+
* `status` is omitted when no HTTP response was ever received (e.g. a
|
|
7
|
+
* network error), per
|
|
8
|
+
* https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status
|
|
9
|
+
*/
|
|
10
|
+
interface FetchResponse {
|
|
11
|
+
status?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Error identifier used as `error.type` (e.g. the exception's `name`).
|
|
14
|
+
* Not the HTTP status reason phrase. Also forces the span into an error
|
|
15
|
+
* state even if `status` is a non-error code.
|
|
16
|
+
*/
|
|
17
|
+
error?: string;
|
|
18
|
+
/** Whether the request was intentionally cancelled by the caller (e.g. via AbortController). */
|
|
19
|
+
aborted?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Interface used to provide information to finish span on fetch error
|
|
23
|
+
*/
|
|
24
|
+
interface FetchError {
|
|
25
|
+
status?: number;
|
|
26
|
+
message: string;
|
|
27
|
+
/** The error's exception type (e.g. `TypeError`, `AbortError`), if available. */
|
|
28
|
+
name?: string;
|
|
29
|
+
}
|
|
30
|
+
type FetchCustomAttributeFunction = (span: Span, request: Request | RequestInit, result: Response | FetchError) => void;
|
|
31
|
+
type FetchRequestHookFunction = (span: Span, request: Request | RequestInit) => void;
|
|
32
|
+
interface FetchInstrumentationConfig extends InstrumentationConfig {
|
|
33
|
+
/** urls which should include trace headers when origin doesn't match */
|
|
34
|
+
propagateTraceHeaderCorsUrls?: Array<string | RegExp>;
|
|
35
|
+
/**
|
|
36
|
+
* URLs that partially match any regex in ignoreUrls will not be traced.
|
|
37
|
+
* In addition, URLs that are _exact matches_ of strings in ignoreUrls will
|
|
38
|
+
* also not be traced.
|
|
39
|
+
*/
|
|
40
|
+
ignoreUrls?: Array<string | RegExp>;
|
|
41
|
+
/** Function for adding custom attributes on the span */
|
|
42
|
+
applyCustomAttributesOnSpan?: FetchCustomAttributeFunction;
|
|
43
|
+
/** Function for adding custom attributes or headers before the request is handled */
|
|
44
|
+
requestHook?: FetchRequestHookFunction;
|
|
45
|
+
/** Measure outgoing request size */
|
|
46
|
+
measureRequestSize?: boolean;
|
|
47
|
+
/** Custom function to sanitize URLs before adding to log records. */
|
|
48
|
+
sanitizeUrl?: (url: string) => string;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { FetchCustomAttributeFunction, FetchError, FetchInstrumentationConfig, FetchRequestHookFunction, FetchResponse };
|
|
52
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/package.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { version } from "../package.js";
|
|
2
|
+
import { getNetworkContextRegistry } from "../utils/NetworkContextRegistry.js";
|
|
3
|
+
import { matchesUrl } from "../utils/url.js";
|
|
2
4
|
import { cancelIdleCallbackShim, requestIdleCallbackShim } from "./idle-callback-shim.js";
|
|
3
5
|
import { ATTR_RESOURCE_CONNECT_END, ATTR_RESOURCE_CONNECT_START, ATTR_RESOURCE_DECODED_BODY_SIZE, ATTR_RESOURCE_DOMAIN_LOOKUP_END, ATTR_RESOURCE_DOMAIN_LOOKUP_START, ATTR_RESOURCE_DURATION, ATTR_RESOURCE_ENCODED_BODY_SIZE, ATTR_RESOURCE_FETCH_START, ATTR_RESOURCE_INITIATOR_TYPE, ATTR_RESOURCE_NEXT_HOP_PROTOCOL, ATTR_RESOURCE_REDIRECT_END, ATTR_RESOURCE_REDIRECT_START, ATTR_RESOURCE_RENDER_BLOCKING_STATUS, ATTR_RESOURCE_REQUEST_START, ATTR_RESOURCE_RESPONSE_END, ATTR_RESOURCE_RESPONSE_START, ATTR_RESOURCE_SECURE_CONNECTION_START, ATTR_RESOURCE_TRANSFER_SIZE, ATTR_RESOURCE_URL, ATTR_RESOURCE_WORKER_START, RESOURCE_TIMING_EVENT_NAME } from "./semconv.js";
|
|
4
6
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
5
7
|
import { InstrumentationBase } from "@opentelemetry/instrumentation";
|
|
6
|
-
import { isUrlIgnored } from "@opentelemetry/core";
|
|
7
8
|
//#region src/resource-timing/instrumentation.ts
|
|
8
9
|
const DEFAULT_BATCH_SIZE = 50;
|
|
9
10
|
const DEFAULT_FORCE_PROCESSING_AFTER = 1e3;
|
|
@@ -71,14 +72,7 @@ var ResourceTimingInstrumentation = class extends InstrumentationBase {
|
|
|
71
72
|
const initiatorTypes = this._config.initiatorTypes;
|
|
72
73
|
for (const entry of entries) {
|
|
73
74
|
if (initiatorTypes !== void 0 && !initiatorTypes.includes(entry.initiatorType)) continue;
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
ignored = isUrlIgnored(entry.name, this._config.ignoreUrls);
|
|
77
|
-
} catch (e) {
|
|
78
|
-
this._diag.error("Failed to check ignoreUrls for resource entry", e);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (ignored) continue;
|
|
75
|
+
if (matchesUrl(entry.name, this._config.ignoreUrls)) continue;
|
|
82
76
|
if (this._pendingEntries.length >= maxQueueSize) this._flush();
|
|
83
77
|
this._pendingEntries.push(entry);
|
|
84
78
|
}
|
|
@@ -122,7 +116,7 @@ var ResourceTimingInstrumentation = class extends InstrumentationBase {
|
|
|
122
116
|
}
|
|
123
117
|
_emitResource(entry) {
|
|
124
118
|
try {
|
|
125
|
-
|
|
119
|
+
const record = {
|
|
126
120
|
eventName: RESOURCE_TIMING_EVENT_NAME,
|
|
127
121
|
severityNumber: SeverityNumber.INFO,
|
|
128
122
|
attributes: {
|
|
@@ -147,7 +141,14 @@ var ResourceTimingInstrumentation = class extends InstrumentationBase {
|
|
|
147
141
|
[ATTR_RESOURCE_NEXT_HOP_PROTOCOL]: entry.nextHopProtocol,
|
|
148
142
|
[ATTR_RESOURCE_RENDER_BLOCKING_STATUS]: entry.renderBlockingStatus
|
|
149
143
|
}
|
|
150
|
-
}
|
|
144
|
+
};
|
|
145
|
+
const registry = getNetworkContextRegistry();
|
|
146
|
+
const ctx = registry.getContext(entry);
|
|
147
|
+
if (ctx) {
|
|
148
|
+
record.context = ctx;
|
|
149
|
+
registry.unregister(entry);
|
|
150
|
+
}
|
|
151
|
+
this.logger.emit(record);
|
|
151
152
|
} catch (error) {
|
|
152
153
|
this._diag.error(`Failed to emit resource timing entry for "${entry.name}"`, error);
|
|
153
154
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/resource-timing/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { isUrlIgnored } from '@opentelemetry/core';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { version } from '../../package.json' with { type: 'json' };\nimport type { IdleCallbackHandle } from './idle-callback-shim.ts';\nimport {\n cancelIdleCallbackShim,\n requestIdleCallbackShim,\n} from './idle-callback-shim.ts';\nimport {\n ATTR_RESOURCE_CONNECT_END,\n ATTR_RESOURCE_CONNECT_START,\n ATTR_RESOURCE_DECODED_BODY_SIZE,\n ATTR_RESOURCE_DOMAIN_LOOKUP_END,\n ATTR_RESOURCE_DOMAIN_LOOKUP_START,\n ATTR_RESOURCE_DURATION,\n ATTR_RESOURCE_ENCODED_BODY_SIZE,\n ATTR_RESOURCE_FETCH_START,\n ATTR_RESOURCE_INITIATOR_TYPE,\n ATTR_RESOURCE_NEXT_HOP_PROTOCOL,\n ATTR_RESOURCE_REDIRECT_END,\n ATTR_RESOURCE_REDIRECT_START,\n ATTR_RESOURCE_RENDER_BLOCKING_STATUS,\n ATTR_RESOURCE_REQUEST_START,\n ATTR_RESOURCE_RESPONSE_END,\n ATTR_RESOURCE_RESPONSE_START,\n ATTR_RESOURCE_SECURE_CONNECTION_START,\n ATTR_RESOURCE_TRANSFER_SIZE,\n ATTR_RESOURCE_URL,\n ATTR_RESOURCE_WORKER_START,\n RESOURCE_TIMING_EVENT_NAME,\n} from './semconv.ts';\nimport type { ResourceTimingInstrumentationConfig } from './types.ts';\n\nconst DEFAULT_BATCH_SIZE = 50;\nconst DEFAULT_FORCE_PROCESSING_AFTER = 1000;\nconst DEFAULT_MAX_PROCESSING_TIME = 50;\nconst DEFAULT_MAX_QUEUE_SIZE = 1000;\n\nconst MIN_BATCH_SIZE = 1;\nconst MIN_FORCE_PROCESSING_AFTER = 0;\nconst MIN_PROCESSING_TIME = 0;\nconst MIN_QUEUE_SIZE = 1;\n\n/**\n * OpenTelemetry instrumentation for resource timing for browser applications.\n *\n * This instrumentation captures resource timing data using PerformanceObserver\n * and batches emissions to avoid overwhelming the main thread. It uses\n * requestIdleCallback when available (with fallback for Safari) to ensure\n * processing happens during idle periods.\n */\nexport class ResourceTimingInstrumentation extends InstrumentationBase<ResourceTimingInstrumentationConfig> {\n private _observer?: PerformanceObserver;\n private _pendingEntries: PerformanceResourceTiming[] = [];\n private _idleHandle?: IdleCallbackHandle;\n\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n private declare _isEnabled: boolean;\n private declare _loadHandler: (() => void) | undefined;\n private declare _visibilityChangeHandler: (() => void) | undefined;\n\n constructor(config: ResourceTimingInstrumentationConfig = {}) {\n super(\n '@opentelemetry/browser-instrumentation/resource-timing',\n version,\n config,\n );\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n\n if (!('PerformanceObserver' in window)) {\n this._diag.debug(\n 'PerformanceObserver is not supported, resource timings will not be collected',\n );\n return;\n }\n\n this._isEnabled = true;\n\n if (document.readyState === 'complete') {\n this._setupObserver();\n } else {\n this._loadHandler = () => this._setupObserver();\n window.addEventListener('load', this._loadHandler, { once: true });\n }\n\n this._visibilityChangeHandler = () => {\n if (document.hidden) {\n this._flush();\n }\n };\n document.addEventListener(\n 'visibilitychange',\n this._visibilityChangeHandler,\n );\n }\n\n override disable(): void {\n this._isEnabled = false;\n this._flush();\n this._observer?.disconnect();\n this._observer = undefined;\n if (this._loadHandler) {\n window.removeEventListener('load', this._loadHandler);\n this._loadHandler = undefined;\n }\n if (this._visibilityChangeHandler) {\n document.removeEventListener(\n 'visibilitychange',\n this._visibilityChangeHandler,\n );\n this._visibilityChangeHandler = undefined;\n }\n }\n\n private _setupObserver(): void {\n if (!this._isEnabled) {\n return;\n }\n\n const observer = new PerformanceObserver((list) => {\n if (!this._isEnabled) {\n return;\n }\n\n const maxQueueSize = Math.max(\n this._config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,\n MIN_QUEUE_SIZE,\n );\n const entries = list.getEntries() as PerformanceResourceTiming[];\n const initiatorTypes = this._config.initiatorTypes;\n\n for (const entry of entries) {\n if (\n initiatorTypes !== undefined &&\n !initiatorTypes.includes(entry.initiatorType)\n ) {\n continue;\n }\n\n let ignored: boolean;\n try {\n ignored = isUrlIgnored(entry.name, this._config.ignoreUrls);\n } catch (e) {\n this._diag.error('Failed to check ignoreUrls for resource entry', e);\n continue;\n }\n if (ignored) {\n continue;\n }\n\n if (this._pendingEntries.length >= maxQueueSize) {\n this._flush();\n }\n this._pendingEntries.push(entry);\n }\n\n if (this._pendingEntries.length > 0) {\n this._scheduleProcessing();\n }\n });\n\n this._observer = observer;\n\n try {\n observer.observe({ type: 'resource', buffered: true });\n } catch (e) {\n this._diag.error('Failed to start resource PerformanceObserver', e);\n this._observer = undefined;\n }\n }\n\n private _scheduleProcessing(): void {\n if (this._idleHandle !== undefined) {\n return;\n }\n\n const timeout = Math.max(\n this._config.forceProcessingAfter ?? DEFAULT_FORCE_PROCESSING_AFTER,\n MIN_FORCE_PROCESSING_AFTER,\n );\n this._idleHandle = requestIdleCallbackShim(\n (deadline) => this._processChunk(deadline),\n { timeout },\n );\n }\n\n private _processChunk(deadline: IdleDeadline): void {\n this._idleHandle = undefined;\n if (!this._isEnabled || this._pendingEntries.length === 0) {\n return;\n }\n\n const maxTime = Math.max(\n this._config.maxProcessingTime ?? DEFAULT_MAX_PROCESSING_TIME,\n MIN_PROCESSING_TIME,\n );\n const batchSize = Math.max(\n this._config.batchSize ?? DEFAULT_BATCH_SIZE,\n MIN_BATCH_SIZE,\n );\n const startTime = performance.now();\n\n let cursor = 0;\n try {\n for (let i = 0; i < batchSize; i++) {\n const entry = this._pendingEntries[cursor];\n if (entry === undefined) {\n break;\n }\n\n const elapsed = performance.now() - startTime;\n // timeRemaining() is not Baseline widely available (no Safari support),\n // compatibility is handled by idle-callback-shim.ts.\n // eslint-disable-next-line baseline-js/use-baseline\n if (elapsed >= maxTime || deadline.timeRemaining() < 1) {\n break;\n }\n\n cursor++;\n this._emitResource(entry);\n }\n } finally {\n this._pendingEntries.splice(0, cursor);\n if (this._pendingEntries.length > 0) {\n this._scheduleProcessing();\n }\n }\n }\n\n private _emitResource(entry: PerformanceResourceTiming): void {\n try {\n this.logger.emit({\n eventName: RESOURCE_TIMING_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_RESOURCE_URL]: entry.name,\n [ATTR_RESOURCE_INITIATOR_TYPE]: entry.initiatorType,\n [ATTR_RESOURCE_DURATION]: entry.duration,\n [ATTR_RESOURCE_FETCH_START]: entry.fetchStart,\n [ATTR_RESOURCE_DOMAIN_LOOKUP_START]: entry.domainLookupStart,\n [ATTR_RESOURCE_DOMAIN_LOOKUP_END]: entry.domainLookupEnd,\n [ATTR_RESOURCE_CONNECT_START]: entry.connectStart,\n [ATTR_RESOURCE_CONNECT_END]: entry.connectEnd,\n [ATTR_RESOURCE_SECURE_CONNECTION_START]: entry.secureConnectionStart,\n [ATTR_RESOURCE_REQUEST_START]: entry.requestStart,\n [ATTR_RESOURCE_RESPONSE_START]: entry.responseStart,\n [ATTR_RESOURCE_RESPONSE_END]: entry.responseEnd,\n [ATTR_RESOURCE_TRANSFER_SIZE]: entry.transferSize,\n [ATTR_RESOURCE_ENCODED_BODY_SIZE]: entry.encodedBodySize,\n [ATTR_RESOURCE_DECODED_BODY_SIZE]: entry.decodedBodySize,\n [ATTR_RESOURCE_REDIRECT_START]: entry.redirectStart,\n [ATTR_RESOURCE_REDIRECT_END]: entry.redirectEnd,\n [ATTR_RESOURCE_WORKER_START]: entry.workerStart,\n [ATTR_RESOURCE_NEXT_HOP_PROTOCOL]: entry.nextHopProtocol,\n // @ts-expect-error renderBlockingStatus is only available in Chromium as of March 2026\n [ATTR_RESOURCE_RENDER_BLOCKING_STATUS]: entry.renderBlockingStatus,\n },\n });\n } catch (error) {\n this._diag.error(\n `Failed to emit resource timing entry for \"${entry.name}\"`,\n error,\n );\n }\n }\n\n private _flush(): void {\n this._cancelScheduledProcessing();\n\n for (const entry of this._pendingEntries) {\n this._emitResource(entry);\n }\n this._pendingEntries = [];\n }\n\n private _cancelScheduledProcessing(): void {\n if (this._idleHandle !== undefined) {\n cancelIdleCallbackShim(this._idleHandle);\n this._idleHandle = undefined;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/resource-timing/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { LogRecord } from '@opentelemetry/api-logs';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { InstrumentationBase } from '@opentelemetry/instrumentation';\nimport { getNetworkContextRegistry } from '#utils';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { matchesUrl } from '../utils/url.ts';\nimport type { IdleCallbackHandle } from './idle-callback-shim.ts';\nimport {\n cancelIdleCallbackShim,\n requestIdleCallbackShim,\n} from './idle-callback-shim.ts';\nimport {\n ATTR_RESOURCE_CONNECT_END,\n ATTR_RESOURCE_CONNECT_START,\n ATTR_RESOURCE_DECODED_BODY_SIZE,\n ATTR_RESOURCE_DOMAIN_LOOKUP_END,\n ATTR_RESOURCE_DOMAIN_LOOKUP_START,\n ATTR_RESOURCE_DURATION,\n ATTR_RESOURCE_ENCODED_BODY_SIZE,\n ATTR_RESOURCE_FETCH_START,\n ATTR_RESOURCE_INITIATOR_TYPE,\n ATTR_RESOURCE_NEXT_HOP_PROTOCOL,\n ATTR_RESOURCE_REDIRECT_END,\n ATTR_RESOURCE_REDIRECT_START,\n ATTR_RESOURCE_RENDER_BLOCKING_STATUS,\n ATTR_RESOURCE_REQUEST_START,\n ATTR_RESOURCE_RESPONSE_END,\n ATTR_RESOURCE_RESPONSE_START,\n ATTR_RESOURCE_SECURE_CONNECTION_START,\n ATTR_RESOURCE_TRANSFER_SIZE,\n ATTR_RESOURCE_URL,\n ATTR_RESOURCE_WORKER_START,\n RESOURCE_TIMING_EVENT_NAME,\n} from './semconv.ts';\nimport type { ResourceTimingInstrumentationConfig } from './types.ts';\n\nconst DEFAULT_BATCH_SIZE = 50;\nconst DEFAULT_FORCE_PROCESSING_AFTER = 1000;\nconst DEFAULT_MAX_PROCESSING_TIME = 50;\nconst DEFAULT_MAX_QUEUE_SIZE = 1000;\n\nconst MIN_BATCH_SIZE = 1;\nconst MIN_FORCE_PROCESSING_AFTER = 0;\nconst MIN_PROCESSING_TIME = 0;\nconst MIN_QUEUE_SIZE = 1;\n\n/**\n * OpenTelemetry instrumentation for resource timing for browser applications.\n *\n * This instrumentation captures resource timing data using PerformanceObserver\n * and batches emissions to avoid overwhelming the main thread. It uses\n * requestIdleCallback when available (with fallback for Safari) to ensure\n * processing happens during idle periods.\n */\nexport class ResourceTimingInstrumentation extends InstrumentationBase<ResourceTimingInstrumentationConfig> {\n private _observer?: PerformanceObserver;\n private _pendingEntries: PerformanceResourceTiming[] = [];\n private _idleHandle?: IdleCallbackHandle;\n\n // Use `declare` to prevent JS class field initializers from running after\n // super(), which would reset values set by the enable() call that\n // InstrumentationBase makes during its constructor.\n private declare _isEnabled: boolean;\n private declare _loadHandler: (() => void) | undefined;\n private declare _visibilityChangeHandler: (() => void) | undefined;\n\n constructor(config: ResourceTimingInstrumentationConfig = {}) {\n super(\n '@opentelemetry/browser-instrumentation/resource-timing',\n version,\n config,\n );\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n\n if (!('PerformanceObserver' in window)) {\n this._diag.debug(\n 'PerformanceObserver is not supported, resource timings will not be collected',\n );\n return;\n }\n\n this._isEnabled = true;\n\n if (document.readyState === 'complete') {\n this._setupObserver();\n } else {\n this._loadHandler = () => this._setupObserver();\n window.addEventListener('load', this._loadHandler, { once: true });\n }\n\n this._visibilityChangeHandler = () => {\n if (document.hidden) {\n this._flush();\n }\n };\n document.addEventListener(\n 'visibilitychange',\n this._visibilityChangeHandler,\n );\n }\n\n override disable(): void {\n this._isEnabled = false;\n this._flush();\n this._observer?.disconnect();\n this._observer = undefined;\n if (this._loadHandler) {\n window.removeEventListener('load', this._loadHandler);\n this._loadHandler = undefined;\n }\n if (this._visibilityChangeHandler) {\n document.removeEventListener(\n 'visibilitychange',\n this._visibilityChangeHandler,\n );\n this._visibilityChangeHandler = undefined;\n }\n }\n\n private _setupObserver(): void {\n if (!this._isEnabled) {\n return;\n }\n\n const observer = new PerformanceObserver((list) => {\n if (!this._isEnabled) {\n return;\n }\n\n const maxQueueSize = Math.max(\n this._config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,\n MIN_QUEUE_SIZE,\n );\n const entries = list.getEntries() as PerformanceResourceTiming[];\n const initiatorTypes = this._config.initiatorTypes;\n\n for (const entry of entries) {\n if (\n initiatorTypes !== undefined &&\n !initiatorTypes.includes(entry.initiatorType)\n ) {\n continue;\n }\n\n const shouldIgnoreUrl = matchesUrl(entry.name, this._config.ignoreUrls);\n if (shouldIgnoreUrl) {\n continue;\n }\n\n if (this._pendingEntries.length >= maxQueueSize) {\n this._flush();\n }\n this._pendingEntries.push(entry);\n }\n\n if (this._pendingEntries.length > 0) {\n this._scheduleProcessing();\n }\n });\n\n this._observer = observer;\n\n try {\n observer.observe({ type: 'resource', buffered: true });\n } catch (e) {\n this._diag.error('Failed to start resource PerformanceObserver', e);\n this._observer = undefined;\n }\n }\n\n private _scheduleProcessing(): void {\n if (this._idleHandle !== undefined) {\n return;\n }\n\n const timeout = Math.max(\n this._config.forceProcessingAfter ?? DEFAULT_FORCE_PROCESSING_AFTER,\n MIN_FORCE_PROCESSING_AFTER,\n );\n this._idleHandle = requestIdleCallbackShim(\n (deadline) => this._processChunk(deadline),\n { timeout },\n );\n }\n\n private _processChunk(deadline: IdleDeadline): void {\n this._idleHandle = undefined;\n if (!this._isEnabled || this._pendingEntries.length === 0) {\n return;\n }\n\n const maxTime = Math.max(\n this._config.maxProcessingTime ?? DEFAULT_MAX_PROCESSING_TIME,\n MIN_PROCESSING_TIME,\n );\n const batchSize = Math.max(\n this._config.batchSize ?? DEFAULT_BATCH_SIZE,\n MIN_BATCH_SIZE,\n );\n const startTime = performance.now();\n\n let cursor = 0;\n try {\n for (let i = 0; i < batchSize; i++) {\n const entry = this._pendingEntries[cursor];\n if (entry === undefined) {\n break;\n }\n\n const elapsed = performance.now() - startTime;\n // timeRemaining() is not Baseline widely available (no Safari support),\n // compatibility is handled by idle-callback-shim.ts.\n // eslint-disable-next-line baseline-js/use-baseline\n if (elapsed >= maxTime || deadline.timeRemaining() < 1) {\n break;\n }\n\n cursor++;\n this._emitResource(entry);\n }\n } finally {\n this._pendingEntries.splice(0, cursor);\n if (this._pendingEntries.length > 0) {\n this._scheduleProcessing();\n }\n }\n }\n\n private _emitResource(entry: PerformanceResourceTiming): void {\n try {\n const record: LogRecord = {\n eventName: RESOURCE_TIMING_EVENT_NAME,\n severityNumber: SeverityNumber.INFO,\n attributes: {\n [ATTR_RESOURCE_URL]: entry.name,\n [ATTR_RESOURCE_INITIATOR_TYPE]: entry.initiatorType,\n [ATTR_RESOURCE_DURATION]: entry.duration,\n [ATTR_RESOURCE_FETCH_START]: entry.fetchStart,\n [ATTR_RESOURCE_DOMAIN_LOOKUP_START]: entry.domainLookupStart,\n [ATTR_RESOURCE_DOMAIN_LOOKUP_END]: entry.domainLookupEnd,\n [ATTR_RESOURCE_CONNECT_START]: entry.connectStart,\n [ATTR_RESOURCE_CONNECT_END]: entry.connectEnd,\n [ATTR_RESOURCE_SECURE_CONNECTION_START]: entry.secureConnectionStart,\n [ATTR_RESOURCE_REQUEST_START]: entry.requestStart,\n [ATTR_RESOURCE_RESPONSE_START]: entry.responseStart,\n [ATTR_RESOURCE_RESPONSE_END]: entry.responseEnd,\n [ATTR_RESOURCE_TRANSFER_SIZE]: entry.transferSize,\n [ATTR_RESOURCE_ENCODED_BODY_SIZE]: entry.encodedBodySize,\n [ATTR_RESOURCE_DECODED_BODY_SIZE]: entry.decodedBodySize,\n [ATTR_RESOURCE_REDIRECT_START]: entry.redirectStart,\n [ATTR_RESOURCE_REDIRECT_END]: entry.redirectEnd,\n [ATTR_RESOURCE_WORKER_START]: entry.workerStart,\n [ATTR_RESOURCE_NEXT_HOP_PROTOCOL]: entry.nextHopProtocol,\n // @ts-expect-error renderBlockingStatus is only available in Chromium as of March 2026\n [ATTR_RESOURCE_RENDER_BLOCKING_STATUS]: entry.renderBlockingStatus,\n },\n };\n const registry = getNetworkContextRegistry();\n const ctx = registry.getContext(entry);\n // Attach the context to the LogRecord and remove it from the registry\n if (ctx) {\n record.context = ctx;\n registry.unregister(entry);\n }\n this.logger.emit(record);\n } catch (error) {\n this._diag.error(\n `Failed to emit resource timing entry for \"${entry.name}\"`,\n error,\n );\n }\n }\n\n private _flush(): void {\n this._cancelScheduledProcessing();\n\n for (const entry of this._pendingEntries) {\n this._emitResource(entry);\n }\n this._pendingEntries = [];\n }\n\n private _cancelScheduledProcessing(): void {\n if (this._idleHandle !== undefined) {\n cancelIdleCallbackShim(this._idleHandle);\n this._idleHandle = undefined;\n }\n }\n}\n"],"mappings":"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"}
|