@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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ROOT_CONTEXT, trace } from "@opentelemetry/api";
|
|
2
|
+
//#region src/utils/ContextRegistry.ts
|
|
3
|
+
const MAX_CAPACITY = 1e3;
|
|
4
|
+
/**
|
|
5
|
+
* Base class for sharing OTel span context between two instrumentations that
|
|
6
|
+
* observe the same events from different angles.
|
|
7
|
+
*
|
|
8
|
+
* The producing instrumentation calls `register()` when a span ends; the
|
|
9
|
+
* consuming instrumentation calls `getContext()` to retrieve the span context
|
|
10
|
+
* associated with the event it is processing.
|
|
11
|
+
*
|
|
12
|
+
* @typeParam TData - Registration payload. Must include enough info to derive a `key` string
|
|
13
|
+
* which will be used as the primary index, plus any implementation-specific fields needed to
|
|
14
|
+
* disambiguate concurrent operations (e.g. timing windows for network spans).
|
|
15
|
+
* @typeParam TLookup - The object the consuming instrumentation passes to
|
|
16
|
+
* `getContext` and `unregister` (e.g. a `PerformanceResourceTiming` entry).
|
|
17
|
+
*/
|
|
18
|
+
var ContextRegistry = class {
|
|
19
|
+
_recordsByKey = /* @__PURE__ */ new Map();
|
|
20
|
+
_recordsList = [];
|
|
21
|
+
/** Store the span context for the given data, key is resolved by getDataKey(). */
|
|
22
|
+
register(span, data) {
|
|
23
|
+
const ctx = trace.setSpan(ROOT_CONTEXT, span);
|
|
24
|
+
const key = this.getDataKey(data);
|
|
25
|
+
const list = this._recordsByKey.get(key) ?? [];
|
|
26
|
+
const record = {
|
|
27
|
+
...data,
|
|
28
|
+
ctx
|
|
29
|
+
};
|
|
30
|
+
list.push(record);
|
|
31
|
+
this._recordsByKey.set(key, list);
|
|
32
|
+
this._recordsList.push(record);
|
|
33
|
+
if (this._recordsList.length > MAX_CAPACITY) {
|
|
34
|
+
const oldestRecord = this._recordsList.shift();
|
|
35
|
+
if (oldestRecord) {
|
|
36
|
+
const oldestKey = this.getDataKey(oldestRecord);
|
|
37
|
+
const oldestCtx = oldestRecord.ctx;
|
|
38
|
+
this.removeByKey(oldestKey, oldestCtx);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Remove the record that matches `lookup`. If no matching record exists this
|
|
44
|
+
* is a no-op. Deletes the key entirely once all records under it are removed.
|
|
45
|
+
*/
|
|
46
|
+
unregister(lookup) {
|
|
47
|
+
const key = this.getLookupKey(lookup);
|
|
48
|
+
const ctx = this.getContext(lookup);
|
|
49
|
+
if (ctx === void 0) return;
|
|
50
|
+
this.removeByKey(key, ctx);
|
|
51
|
+
const index = this._recordsList.findIndex((r) => {
|
|
52
|
+
const k = this.getDataKey(r);
|
|
53
|
+
return key === k && ctx === r.ctx;
|
|
54
|
+
});
|
|
55
|
+
if (index > -1) this._recordsList.splice(index, 1);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Remove a record from the records sub-list of the given key
|
|
59
|
+
*/
|
|
60
|
+
removeByKey(key, ctx) {
|
|
61
|
+
const list = this._recordsByKey.get(key);
|
|
62
|
+
if (!list) return;
|
|
63
|
+
const filtered = list.filter((r) => r.ctx !== ctx);
|
|
64
|
+
if (filtered.length === 0) this._recordsByKey.delete(key);
|
|
65
|
+
else this._recordsByKey.set(key, filtered);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Return the size number of records stored in the registry
|
|
69
|
+
*/
|
|
70
|
+
size() {
|
|
71
|
+
return this._recordsList.length;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { ContextRegistry };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=ContextRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextRegistry.js","names":[],"sources":["../../src/utils/ContextRegistry.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Context, Span } from '@opentelemetry/api';\nimport { ROOT_CONTEXT, trace } from '@opentelemetry/api';\n\ntype StoredRecord<TData> = TData & { ctx: Context };\n\nconst MAX_CAPACITY = 1000;\n\n/**\n * Base class for sharing OTel span context between two instrumentations that\n * observe the same events from different angles.\n *\n * The producing instrumentation calls `register()` when a span ends; the\n * consuming instrumentation calls `getContext()` to retrieve the span context\n * associated with the event it is processing.\n *\n * @typeParam TData - Registration payload. Must include enough info to derive a `key` string\n * which will be used as the primary index, plus any implementation-specific fields needed to\n * disambiguate concurrent operations (e.g. timing windows for network spans).\n * @typeParam TLookup - The object the consuming instrumentation passes to\n * `getContext` and `unregister` (e.g. a `PerformanceResourceTiming` entry).\n */\nexport abstract class ContextRegistry<TData, TLookup> {\n protected _recordsByKey = new Map<string, StoredRecord<TData>[]>();\n private _recordsList: StoredRecord<TData>[] = [];\n\n /** Store the span context for the given data, key is resolved by getDataKey(). */\n register(span: Span, data: TData): void {\n const ctx = trace.setSpan(ROOT_CONTEXT, span);\n const key = this.getDataKey(data);\n const list = this._recordsByKey.get(key) ?? [];\n const record = { ...data, ctx };\n\n // Set record into the map for faster lookup and in the\n // list to track the size\n list.push(record);\n this._recordsByKey.set(key, list);\n this._recordsList.push(record);\n\n // If capacity is exceeded remove the oldest (1st in the array)\n if (this._recordsList.length > MAX_CAPACITY) {\n const oldestRecord = this._recordsList.shift();\n if (oldestRecord) {\n const oldestKey = this.getDataKey(oldestRecord);\n const oldestCtx = oldestRecord.ctx;\n this.removeByKey(oldestKey, oldestCtx);\n }\n }\n }\n\n /**\n * Remove the record that matches `lookup`. If no matching record exists this\n * is a no-op. Deletes the key entirely once all records under it are removed.\n */\n unregister(lookup: TLookup): void {\n const key = this.getLookupKey(lookup);\n const ctx = this.getContext(lookup);\n if (ctx === undefined) {\n return;\n }\n\n // Remove from the lookup map and from the global list\n // to keep size up to date\n this.removeByKey(key, ctx);\n const index = this._recordsList.findIndex((r) => {\n const k = this.getDataKey(r);\n return key === k && ctx === r.ctx;\n });\n if (index > -1) {\n this._recordsList.splice(index, 1);\n }\n }\n\n /**\n * Remove a record from the records sub-list of the given key\n */\n private removeByKey(key: string, ctx: Context): void {\n const list = this._recordsByKey.get(key);\n if (!list) {\n return;\n }\n\n const filtered = list.filter((r) => r.ctx !== ctx);\n if (filtered.length === 0) {\n this._recordsByKey.delete(key);\n } else {\n this._recordsByKey.set(key, filtered);\n }\n }\n\n /**\n * Return the size number of records stored in the registry\n */\n size(): number {\n return this._recordsList.length;\n }\n\n /** Return the index key for the given data object. */\n abstract getDataKey(data: TData): string;\n\n /** Return the index key for the given lookup object. */\n abstract getLookupKey(lookup: TLookup): string;\n\n /**\n * Return the OTel context for the record that matches `lookup`, or\n * `undefined` if no match is found.\n */\n abstract getContext(lookup: TLookup): Context | undefined;\n}\n"],"mappings":";;AAUA,MAAM,eAAe;;;;;;;;;;;;;;;AAgBrB,IAAsB,kBAAtB,MAAsD;CACpD,gCAA0B,IAAI,IAAmC;CACjE,eAA8C,CAAC;;CAG/C,SAAS,MAAY,MAAmB;EACtC,MAAM,MAAM,MAAM,QAAQ,cAAc,IAAI;EAC5C,MAAM,MAAM,KAAK,WAAW,IAAI;EAChC,MAAM,OAAO,KAAK,cAAc,IAAI,GAAG,KAAK,CAAC;EAC7C,MAAM,SAAS;GAAE,GAAG;GAAM;EAAI;EAI9B,KAAK,KAAK,MAAM;EAChB,KAAK,cAAc,IAAI,KAAK,IAAI;EAChC,KAAK,aAAa,KAAK,MAAM;EAG7B,IAAI,KAAK,aAAa,SAAS,cAAc;GAC3C,MAAM,eAAe,KAAK,aAAa,MAAM;GAC7C,IAAI,cAAc;IAChB,MAAM,YAAY,KAAK,WAAW,YAAY;IAC9C,MAAM,YAAY,aAAa;IAC/B,KAAK,YAAY,WAAW,SAAS;GACvC;EACF;CACF;;;;;CAMA,WAAW,QAAuB;EAChC,MAAM,MAAM,KAAK,aAAa,MAAM;EACpC,MAAM,MAAM,KAAK,WAAW,MAAM;EAClC,IAAI,QAAQ,KAAA,GACV;EAKF,KAAK,YAAY,KAAK,GAAG;EACzB,MAAM,QAAQ,KAAK,aAAa,WAAW,MAAM;GAC/C,MAAM,IAAI,KAAK,WAAW,CAAC;GAC3B,OAAO,QAAQ,KAAK,QAAQ,EAAE;EAChC,CAAC;EACD,IAAI,QAAQ,IACV,KAAK,aAAa,OAAO,OAAO,CAAC;CAErC;;;;CAKA,YAAoB,KAAa,KAAoB;EACnD,MAAM,OAAO,KAAK,cAAc,IAAI,GAAG;EACvC,IAAI,CAAC,MACH;EAGF,MAAM,WAAW,KAAK,QAAQ,MAAM,EAAE,QAAQ,GAAG;EACjD,IAAI,SAAS,WAAW,GACtB,KAAK,cAAc,OAAO,GAAG;OAE7B,KAAK,cAAc,IAAI,KAAK,QAAQ;CAExC;;;;CAKA,OAAe;EACb,OAAO,KAAK,aAAa;CAC3B;AAaF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ContextRegistry } from "./ContextRegistry.js";
|
|
2
|
+
//#region src/utils/NetworkContextRegistry.ts
|
|
3
|
+
var NetworkContextRegistry = class extends ContextRegistry {
|
|
4
|
+
getDataKey(data) {
|
|
5
|
+
return data.key;
|
|
6
|
+
}
|
|
7
|
+
getLookupKey(entry) {
|
|
8
|
+
return entry.name;
|
|
9
|
+
}
|
|
10
|
+
getContext(entry) {
|
|
11
|
+
const key = this.getLookupKey(entry);
|
|
12
|
+
return this._recordsByKey.get(key)?.find((r) => entry.fetchStart >= r.startPerfNow && entry.responseEnd <= r.endPerfNow)?.ctx;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
let _instance;
|
|
16
|
+
function getNetworkContextRegistry() {
|
|
17
|
+
if (!_instance) _instance = new NetworkContextRegistry();
|
|
18
|
+
return _instance;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { NetworkContextRegistry, getNetworkContextRegistry };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=NetworkContextRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NetworkContextRegistry.js","names":[],"sources":["../../src/utils/NetworkContextRegistry.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Context } from '@opentelemetry/api';\nimport { ContextRegistry } from './ContextRegistry.ts';\n\nexport interface NetworkSpanData {\n key: string;\n startPerfNow: number;\n endPerfNow: number;\n}\n\nexport class NetworkContextRegistry extends ContextRegistry<\n NetworkSpanData,\n PerformanceResourceTiming\n> {\n getDataKey(data: NetworkSpanData): string {\n return data.key;\n }\n\n getLookupKey(entry: PerformanceResourceTiming): string {\n return entry.name;\n }\n\n getContext(entry: PerformanceResourceTiming): Context | undefined {\n const key = this.getLookupKey(entry);\n const list = this._recordsByKey.get(key);\n return list?.find(\n (r) =>\n entry.fetchStart >= r.startPerfNow && entry.responseEnd <= r.endPerfNow,\n )?.ctx;\n }\n}\n\nlet _instance: NetworkContextRegistry | undefined;\n\nexport function getNetworkContextRegistry(): NetworkContextRegistry {\n if (!_instance) {\n _instance = new NetworkContextRegistry();\n }\n\n return _instance;\n}\n"],"mappings":";;AAcA,IAAa,yBAAb,cAA4C,gBAG1C;CACA,WAAW,MAA+B;EACxC,OAAO,KAAK;CACd;CAEA,aAAa,OAA0C;EACrD,OAAO,MAAM;CACf;CAEA,WAAW,OAAuD;EAChE,MAAM,MAAM,KAAK,aAAa,KAAK;EAEnC,OADa,KAAK,cAAc,IAAI,GAC1B,CAAC,EAAE,MACV,MACC,MAAM,cAAc,EAAE,gBAAgB,MAAM,eAAe,EAAE,UACjE,CAAC,EAAE;CACL;AACF;AAEA,IAAI;AAEJ,SAAgB,4BAAoD;CAClE,IAAI,CAAC,WACH,YAAY,IAAI,uBAAuB;CAGzC,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getElementCSSSelector.js","names":[],"sources":["../../src/utils/getElementCSSSelector.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\ntype GetElementCSSSelectorOptions = {\n /**\n * If true, the function will attempt to use element ID to create condensed CSS selector.\n */\n useIdForTargetElement?: boolean;\n /**\n * If true, the function will attempt to use element ID for all ancestor elements to create condensed CSS selector.\n */\n useIdForAncestors?: boolean;\n};\n\n/**\n * Generates the CSS selector of a given element in the DOM tree.\n *\n * @example #main > div:nth-child(2) > button.submit\n * @example #unique-id\n */\nexport const getElementCSSSelector = (\n element: Node,\n {\n useIdForTargetElement = false,\n useIdForAncestors = false,\n }: GetElementCSSSelectorOptions = {},\n): string => {\n // Handle document node\n if (element.nodeType === Node.DOCUMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const nodeValue = getNodeSelector(\n htmlElement,\n useIdForTargetElement || useIdForAncestors,\n );\n\n // If optimized and found an ID selector, stop recursion early\n if (nodeValue.startsWith('#')) {\n return nodeValue;\n }\n\n const parent = htmlElement.parentElement;\n const parentSelector = parent\n ? getElementCSSSelector(parent, {\n useIdForAncestors,\n useIdForTargetElement: false,\n })\n : '';\n\n return parentSelector ? `${parentSelector} > ${nodeValue}` : nodeValue;\n};\n\nconst getNodeSelector = (element: Node, useElementId = false): string => {\n if (element.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const id = htmlElement.getAttribute('id');\n\n // Use ID if requested and it's unique\n if (useElementId && id) {\n // Check if ID is unique in the document\n const elementsWithSameId = htmlElement.ownerDocument.querySelectorAll(\n `#${CSS.escape(id)}`,\n );\n\n if (elementsWithSameId.length === 1) {\n return `#${CSS.escape(id)}`;\n }\n }\n\n let selector = getFullClassSelector(htmlElement);\n // Add nth-child if there are siblings with the same tag and classes\n const index = getNthChild(htmlElement);\n\n if (index > 0) {\n selector += `:nth-child(${index})`;\n }\n\n return selector;\n};\n\nconst getNthChild = (element: HTMLElement): number => {\n // parentElement is needed to access children\n if (!element.parentElement) {\n return 0;\n }\n\n const selector = getFullClassSelector(element);\n\n // Get all siblings that match the same selector\n const siblings = Array.from(element.parentElement.children).filter(\n (sibling) => getFullClassSelector(sibling) === selector,\n );\n\n // Only add nth-child if there are multiple matching siblings\n if (siblings.length > 1) {\n return siblings.indexOf(element) + 1;\n }\n\n return 0;\n};\n\nconst getFullClassSelector = (element: Element): string =>\n element.localName +\n (element.classList.length > 0\n ? Array.from(element.classList)\n .map((cls) => `.${CSS.escape(cls)}`)\n .join('')\n : '');\n"],"mappings":";;;;;;;AAsBA,MAAa,yBACX,SACA,EACE,wBAAwB,OACxB,oBAAoB,UACY,CAAC,MACxB;CAEX,IAAI,QAAQ,aAAa,KAAK,eAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,YAAY,gBAChB,aACA,yBAAyB,iBAC3B;CAGA,IAAI,UAAU,WAAW,GAAG,GAC1B,OAAO;CAGT,MAAM,SAAS,YAAY;CAC3B,MAAM,iBAAiB,SACnB,sBAAsB,QAAQ;EAC5B;EACA,uBAAuB;CACzB,CAAC,IACD;CAEJ,OAAO,iBAAiB,GAAG,eAAe,KAAK,cAAc;AAC/D;AAEA,MAAM,mBAAmB,SAAe,eAAe,UAAkB;CACvE,IAAI,QAAQ,aAAa,KAAK,cAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,KAAK,YAAY,aAAa,IAAI;CAGxC,IAAI,gBAAgB;
|
|
1
|
+
{"version":3,"file":"getElementCSSSelector.js","names":[],"sources":["../../src/utils/getElementCSSSelector.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\ntype GetElementCSSSelectorOptions = {\n /**\n * If true, the function will attempt to use element ID to create condensed CSS selector.\n */\n useIdForTargetElement?: boolean;\n /**\n * If true, the function will attempt to use element ID for all ancestor elements to create condensed CSS selector.\n */\n useIdForAncestors?: boolean;\n};\n\n/**\n * Generates the CSS selector of a given element in the DOM tree.\n *\n * @example #main > div:nth-child(2) > button.submit\n * @example #unique-id\n */\nexport const getElementCSSSelector = (\n element: Node,\n {\n useIdForTargetElement = false,\n useIdForAncestors = false,\n }: GetElementCSSSelectorOptions = {},\n): string => {\n // Handle document node\n if (element.nodeType === Node.DOCUMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const nodeValue = getNodeSelector(\n htmlElement,\n useIdForTargetElement || useIdForAncestors,\n );\n\n // If optimized and found an ID selector, stop recursion early\n if (nodeValue.startsWith('#')) {\n return nodeValue;\n }\n\n const parent = htmlElement.parentElement;\n const parentSelector = parent\n ? getElementCSSSelector(parent, {\n useIdForAncestors,\n useIdForTargetElement: false,\n })\n : '';\n\n return parentSelector ? `${parentSelector} > ${nodeValue}` : nodeValue;\n};\n\nconst getNodeSelector = (element: Node, useElementId = false): string => {\n if (element.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const id = htmlElement.getAttribute('id');\n\n // Use ID if requested and it's unique\n if (useElementId && id) {\n // Check if ID is unique in the document\n const elementsWithSameId = htmlElement.ownerDocument.querySelectorAll(\n `#${CSS.escape(id)}`,\n );\n\n if (elementsWithSameId.length === 1) {\n return `#${CSS.escape(id)}`;\n }\n }\n\n let selector = getFullClassSelector(htmlElement);\n // Add nth-child if there are siblings with the same tag and classes\n const index = getNthChild(htmlElement);\n\n if (index > 0) {\n selector += `:nth-child(${index})`;\n }\n\n return selector;\n};\n\nconst getNthChild = (element: HTMLElement): number => {\n // parentElement is needed to access children\n if (!element.parentElement) {\n return 0;\n }\n\n const selector = getFullClassSelector(element);\n\n // Get all siblings that match the same selector\n const siblings = Array.from(element.parentElement.children).filter(\n (sibling) => getFullClassSelector(sibling) === selector,\n );\n\n // Only add nth-child if there are multiple matching siblings\n if (siblings.length > 1) {\n return siblings.indexOf(element) + 1;\n }\n\n return 0;\n};\n\nconst getFullClassSelector = (element: Element): string =>\n element.localName +\n (element.classList.length > 0\n ? Array.from(element.classList)\n .map((cls) => `.${CSS.escape(cls)}`)\n .join('')\n : '');\n"],"mappings":";;;;;;;AAsBA,MAAa,yBACX,SACA,EACE,wBAAwB,OACxB,oBAAoB,UACY,CAAC,MACxB;CAEX,IAAI,QAAQ,aAAa,KAAK,eAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,YAAY,gBAChB,aACA,yBAAyB,iBAC3B;CAGA,IAAI,UAAU,WAAW,GAAG,GAC1B,OAAO;CAGT,MAAM,SAAS,YAAY;CAC3B,MAAM,iBAAiB,SACnB,sBAAsB,QAAQ;EAC5B;EACA,uBAAuB;CACzB,CAAC,IACD;CAEJ,OAAO,iBAAiB,GAAG,eAAe,KAAK,cAAc;AAC/D;AAEA,MAAM,mBAAmB,SAAe,eAAe,UAAkB;CACvE,IAAI,QAAQ,aAAa,KAAK,cAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,KAAK,YAAY,aAAa,IAAI;CAGxC,IAAI,gBAAgB,IAES;MAAA,YAAY,cAAc,iBACnD,IAAI,IAAI,OAAO,EAAE,GAGE,CAAC,CAAC,WAAW,GAChC,OAAO,IAAI,IAAI,OAAO,EAAE;CAAA;CAI5B,IAAI,WAAW,qBAAqB,WAAW;CAE/C,MAAM,QAAQ,YAAY,WAAW;CAErC,IAAI,QAAQ,GACV,YAAY,cAAc,MAAM;CAGlC,OAAO;AACT;AAEA,MAAM,eAAe,YAAiC;CAEpD,IAAI,CAAC,QAAQ,eACX,OAAO;CAGT,MAAM,WAAW,qBAAqB,OAAO;CAG7C,MAAM,WAAW,MAAM,KAAK,QAAQ,cAAc,QAAQ,CAAC,CAAC,QACzD,YAAY,qBAAqB,OAAO,MAAM,QACjD;CAGA,IAAI,SAAS,SAAS,GACpB,OAAO,SAAS,QAAQ,OAAO,IAAI;CAGrC,OAAO;AACT;AAEA,MAAM,wBAAwB,YAC5B,QAAQ,aACP,QAAQ,UAAU,SAAS,IACxB,MAAM,KAAK,QAAQ,SAAS,CAAC,CAC1B,KAAK,QAAQ,IAAI,IAAI,OAAO,GAAG,GAAG,CAAC,CACnC,KAAK,EAAE,IACV"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { diag } from "@opentelemetry/api";
|
|
2
|
+
//#region src/utils/request.ts
|
|
3
|
+
const DIAG_LOGGER = diag.createComponentLogger({ namespace: "@opentelemetry/browser-instrumentation/utils/request" });
|
|
4
|
+
function isDocument(value) {
|
|
5
|
+
return typeof Document !== "undefined" && value instanceof Document;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Helper function to determine payload content length for fetch requests
|
|
9
|
+
*
|
|
10
|
+
* The fetch API is kinda messy: there are a couple of ways the body can be passed in.
|
|
11
|
+
*
|
|
12
|
+
* In all cases, the body param can be some variation of ReadableStream,
|
|
13
|
+
* and ReadableStreams can only be read once! We want to avoid consuming the body here,
|
|
14
|
+
* because that would mean that the body never gets sent with the actual fetch request.
|
|
15
|
+
*
|
|
16
|
+
* Either the first arg is a Request object, which can be cloned
|
|
17
|
+
* so we can clone that object and read the body of the clone
|
|
18
|
+
* without disturbing the original argument
|
|
19
|
+
* However, reading the body here can only be done async; the body() method returns a promise
|
|
20
|
+
* this means this entire function has to return a promise
|
|
21
|
+
*
|
|
22
|
+
* OR the first arg is a url/string
|
|
23
|
+
* in which case the second arg has type RequestInit
|
|
24
|
+
* RequestInit is NOT cloneable, but RequestInit.body is writable
|
|
25
|
+
* so we can chain it into ReadableStream.pipeThrough()
|
|
26
|
+
*
|
|
27
|
+
* ReadableStream.pipeThrough() lets us process a stream and returns a new stream
|
|
28
|
+
* So we can measure the body length as it passes through the pipe, but need to attach
|
|
29
|
+
* the new stream to the original request
|
|
30
|
+
* so that the browser still has access to the body.
|
|
31
|
+
*/
|
|
32
|
+
async function getFetchBodyLength(...args) {
|
|
33
|
+
if (args[0] instanceof URL || typeof args[0] === "string") {
|
|
34
|
+
const requestInit = args[1];
|
|
35
|
+
if (!requestInit?.body) return;
|
|
36
|
+
if (requestInit.body instanceof ReadableStream) {
|
|
37
|
+
const { body, length } = _getBodyNonDestructively(requestInit.body);
|
|
38
|
+
requestInit.body = body;
|
|
39
|
+
return await length;
|
|
40
|
+
} else return getXHRBodyLength(requestInit.body);
|
|
41
|
+
} else {
|
|
42
|
+
const info = args[0];
|
|
43
|
+
if (!info?.body) return;
|
|
44
|
+
return getByteLength(await info.clone().text());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function _getBodyNonDestructively(body) {
|
|
48
|
+
if (!body.pipeThrough) {
|
|
49
|
+
DIAG_LOGGER.warn("Platform has ReadableStream but not pipeThrough!");
|
|
50
|
+
return {
|
|
51
|
+
body,
|
|
52
|
+
length: Promise.resolve(void 0)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
let length = 0;
|
|
56
|
+
let resolveLength;
|
|
57
|
+
const lengthPromise = new Promise((resolve) => {
|
|
58
|
+
resolveLength = resolve;
|
|
59
|
+
});
|
|
60
|
+
const bodyReader = body.getReader();
|
|
61
|
+
return {
|
|
62
|
+
body: new ReadableStream({
|
|
63
|
+
async pull(controller) {
|
|
64
|
+
const { value, done } = await bodyReader.read();
|
|
65
|
+
length += value?.byteLength || 0;
|
|
66
|
+
if (done) {
|
|
67
|
+
controller.close();
|
|
68
|
+
bodyReader.releaseLock();
|
|
69
|
+
resolveLength(length);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
controller.enqueue(value);
|
|
73
|
+
},
|
|
74
|
+
cancel(reason) {
|
|
75
|
+
resolveLength(void 0);
|
|
76
|
+
bodyReader.cancel(reason);
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
length: lengthPromise
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Helper function to determine payload content length for XHR requests
|
|
84
|
+
*/
|
|
85
|
+
function getXHRBodyLength(body) {
|
|
86
|
+
if (isDocument(body)) return new XMLSerializer().serializeToString(body).length;
|
|
87
|
+
if (typeof body === "string") return getByteLength(body);
|
|
88
|
+
if (body instanceof Blob) return body.size;
|
|
89
|
+
if (body instanceof FormData) return getFormDataSize(body);
|
|
90
|
+
if (body instanceof URLSearchParams) return getByteLength(body.toString());
|
|
91
|
+
if (body.byteLength !== void 0) return body.byteLength;
|
|
92
|
+
DIAG_LOGGER.warn("unknown body type");
|
|
93
|
+
}
|
|
94
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
95
|
+
function getByteLength(s) {
|
|
96
|
+
return TEXT_ENCODER.encode(s).byteLength;
|
|
97
|
+
}
|
|
98
|
+
function getFormDataSize(formData) {
|
|
99
|
+
let size = 0;
|
|
100
|
+
for (const [key, value] of formData.entries()) {
|
|
101
|
+
size += key.length;
|
|
102
|
+
if (value instanceof Blob) size += value.size;
|
|
103
|
+
else size += value.length;
|
|
104
|
+
}
|
|
105
|
+
return size;
|
|
106
|
+
}
|
|
107
|
+
const DEFAULT_KNOWN_METHODS = {
|
|
108
|
+
CONNECT: true,
|
|
109
|
+
DELETE: true,
|
|
110
|
+
GET: true,
|
|
111
|
+
HEAD: true,
|
|
112
|
+
OPTIONS: true,
|
|
113
|
+
PATCH: true,
|
|
114
|
+
POST: true,
|
|
115
|
+
PUT: true,
|
|
116
|
+
TRACE: true,
|
|
117
|
+
QUERY: true
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Normalize an HTTP request method string per `http.request.method` spec
|
|
121
|
+
* https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-client-span
|
|
122
|
+
*/
|
|
123
|
+
function normalizeHttpRequestMethod(method) {
|
|
124
|
+
const methUpper = method.toUpperCase();
|
|
125
|
+
if (Object.hasOwn(DEFAULT_KNOWN_METHODS, methUpper)) return methUpper;
|
|
126
|
+
return "_OTHER";
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { getFetchBodyLength, getXHRBodyLength, normalizeHttpRequestMethod };
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.js","names":[],"sources":["../../src/utils/request.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport { diag } from '@opentelemetry/api';\n\nconst DIAG_LOGGER = diag.createComponentLogger({\n namespace: '@opentelemetry/browser-instrumentation/utils/request',\n});\n\nfunction isDocument(value: unknown): value is Document {\n return typeof Document !== 'undefined' && value instanceof Document;\n}\n\n/**\n * Helper function to determine payload content length for fetch requests\n *\n * The fetch API is kinda messy: there are a couple of ways the body can be passed in.\n *\n * In all cases, the body param can be some variation of ReadableStream,\n * and ReadableStreams can only be read once! We want to avoid consuming the body here,\n * because that would mean that the body never gets sent with the actual fetch request.\n *\n * Either the first arg is a Request object, which can be cloned\n * so we can clone that object and read the body of the clone\n * without disturbing the original argument\n * However, reading the body here can only be done async; the body() method returns a promise\n * this means this entire function has to return a promise\n *\n * OR the first arg is a url/string\n * in which case the second arg has type RequestInit\n * RequestInit is NOT cloneable, but RequestInit.body is writable\n * so we can chain it into ReadableStream.pipeThrough()\n *\n * ReadableStream.pipeThrough() lets us process a stream and returns a new stream\n * So we can measure the body length as it passes through the pipe, but need to attach\n * the new stream to the original request\n * so that the browser still has access to the body.\n */\nexport async function getFetchBodyLength(\n ...args: Parameters<typeof fetch>\n): Promise<number | undefined> {\n if (args[0] instanceof URL || typeof args[0] === 'string') {\n const requestInit = args[1];\n if (!requestInit?.body) {\n return undefined;\n }\n if (requestInit.body instanceof ReadableStream) {\n const { body, length } = _getBodyNonDestructively(requestInit.body);\n requestInit.body = body;\n\n return await length;\n } else {\n return getXHRBodyLength(requestInit.body);\n }\n } else {\n const info = args[0];\n // Request.body has limited availability. ref: https://developer.mozilla.org/en-US/docs/Web/API/Request/body\n // Accessing it here triggers a eslint error from the baseline plugin\n /* eslint-disable baseline-js/use-baseline */\n if (!info?.body) {\n return undefined;\n }\n /* eslint-enable baseline-js/use-baseline */\n\n const text = await info.clone().text();\n return getByteLength(text);\n }\n}\n\n// This function returns the body right away piped to a transform stream\n// if available. The length is returned as a promise because the original\n// fetch logic has to consume the body to get the final value.\nfunction _getBodyNonDestructively(body: ReadableStream): {\n body: ReadableStream;\n length: Promise<number | undefined>;\n} {\n // can't read a ReadableStream without destroying it\n // but we CAN pipe it through and return a new ReadableStream\n\n // some (older) platforms don't expose the pipeThrough method and in that scenario, we're out of luck;\n // there's no way to read the stream without consuming it.\n if (!body.pipeThrough) {\n DIAG_LOGGER.warn('Platform has ReadableStream but not pipeThrough!');\n return {\n body,\n length: Promise.resolve(undefined),\n };\n }\n\n let length = 0;\n let resolveLength: (l: number | undefined) => void;\n const lengthPromise = new Promise<number | undefined>((resolve) => {\n resolveLength = resolve;\n });\n\n const bodyReader = (body as ReadableStream<Uint8Array>).getReader();\n const bodyWrapper = new ReadableStream<Uint8Array>({\n async pull(controller) {\n const { value, done } = await bodyReader.read();\n\n length += value?.byteLength || 0;\n if (done) {\n controller.close();\n bodyReader.releaseLock();\n resolveLength(length);\n return;\n }\n controller.enqueue(value);\n },\n cancel(reason) {\n resolveLength(undefined);\n bodyReader.cancel(reason);\n },\n });\n\n return {\n body: bodyWrapper,\n length: lengthPromise,\n };\n}\n\n/**\n * Helper function to determine payload content length for XHR requests\n */\nexport function getXHRBodyLength(\n body: Document | XMLHttpRequestBodyInit,\n): number | undefined {\n if (isDocument(body)) {\n return new XMLSerializer().serializeToString(body).length;\n }\n\n // XMLHttpRequestBodyInit expands to the following:\n if (typeof body === 'string') {\n return getByteLength(body);\n }\n\n if (body instanceof Blob) {\n return body.size;\n }\n\n if (body instanceof FormData) {\n return getFormDataSize(body);\n }\n\n if (body instanceof URLSearchParams) {\n return getByteLength(body.toString());\n }\n\n // ArrayBuffer | ArrayBufferView\n if (body.byteLength !== undefined) {\n return body.byteLength;\n }\n\n DIAG_LOGGER.warn('unknown body type');\n return undefined;\n}\n\nconst TEXT_ENCODER = new TextEncoder();\nfunction getByteLength(s: string): number {\n return TEXT_ENCODER.encode(s).byteLength;\n}\n\nfunction getFormDataSize(formData: FormData): number {\n let size = 0;\n for (const [key, value] of formData.entries()) {\n size += key.length;\n if (value instanceof Blob) {\n size += value.size;\n } else {\n size += value.length;\n }\n }\n return size;\n}\n\nconst DEFAULT_KNOWN_METHODS = {\n CONNECT: true,\n DELETE: true,\n GET: true,\n HEAD: true,\n OPTIONS: true,\n PATCH: true,\n POST: true,\n PUT: true,\n TRACE: true,\n // QUERY from https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/\n QUERY: true,\n};\n/**\n * Normalize an HTTP request method string per `http.request.method` spec\n * https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-client-span\n */\nexport function normalizeHttpRequestMethod(method: string): string {\n const methUpper = method.toUpperCase();\n if (Object.hasOwn(DEFAULT_KNOWN_METHODS, methUpper)) {\n return methUpper;\n }\n return '_OTHER';\n}\n"],"mappings":";;AAOA,MAAM,cAAc,KAAK,sBAAsB,EAC7C,WAAW,uDACb,CAAC;AAED,SAAS,WAAW,OAAmC;CACrD,OAAO,OAAO,aAAa,eAAe,iBAAiB;AAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,eAAsB,mBACpB,GAAG,MAC0B;CAC7B,IAAI,KAAK,cAAc,OAAO,OAAO,KAAK,OAAO,UAAU;EACzD,MAAM,cAAc,KAAK;EACzB,IAAI,CAAC,aAAa,MAChB;EAEF,IAAI,YAAY,gBAAgB,gBAAgB;GAC9C,MAAM,EAAE,MAAM,WAAW,yBAAyB,YAAY,IAAI;GAClE,YAAY,OAAO;GAEnB,OAAO,MAAM;EACf,OACE,OAAO,iBAAiB,YAAY,IAAI;CAE5C,OAAO;EACL,MAAM,OAAO,KAAK;EAIlB,IAAI,CAAC,MAAM,MACT;EAKF,OAAO,cAAc,MADF,KAAK,MAAM,CAAC,CAAC,KAAK,CACZ;CAC3B;AACF;AAKA,SAAS,yBAAyB,MAGhC;CAMA,IAAI,CAAC,KAAK,aAAa;EACrB,YAAY,KAAK,kDAAkD;EACnE,OAAO;GACL;GACA,QAAQ,QAAQ,QAAQ,KAAA,CAAS;EACnC;CACF;CAEA,IAAI,SAAS;CACb,IAAI;CACJ,MAAM,gBAAgB,IAAI,SAA6B,YAAY;EACjE,gBAAgB;CAClB,CAAC;CAED,MAAM,aAAc,KAAoC,UAAU;CAoBlE,OAAO;EACL,MAAM,IApBgB,eAA2B;GACjD,MAAM,KAAK,YAAY;IACrB,MAAM,EAAE,OAAO,SAAS,MAAM,WAAW,KAAK;IAE9C,UAAU,OAAO,cAAc;IAC/B,IAAI,MAAM;KACR,WAAW,MAAM;KACjB,WAAW,YAAY;KACvB,cAAc,MAAM;KACpB;IACF;IACA,WAAW,QAAQ,KAAK;GAC1B;GACA,OAAO,QAAQ;IACb,cAAc,KAAA,CAAS;IACvB,WAAW,OAAO,MAAM;GAC1B;EACF,CAGkB;EAChB,QAAQ;CACV;AACF;;;;AAKA,SAAgB,iBACd,MACoB;CACpB,IAAI,WAAW,IAAI,GACjB,OAAO,IAAI,cAAc,CAAC,CAAC,kBAAkB,IAAI,CAAC,CAAC;CAIrD,IAAI,OAAO,SAAS,UAClB,OAAO,cAAc,IAAI;CAG3B,IAAI,gBAAgB,MAClB,OAAO,KAAK;CAGd,IAAI,gBAAgB,UAClB,OAAO,gBAAgB,IAAI;CAG7B,IAAI,gBAAgB,iBAClB,OAAO,cAAc,KAAK,SAAS,CAAC;CAItC,IAAI,KAAK,eAAe,KAAA,GACtB,OAAO,KAAK;CAGd,YAAY,KAAK,mBAAmB;AAEtC;AAEA,MAAM,eAAe,IAAI,YAAY;AACrC,SAAS,cAAc,GAAmB;CACxC,OAAO,aAAa,OAAO,CAAC,CAAC,CAAC;AAChC;AAEA,SAAS,gBAAgB,UAA4B;CACnD,IAAI,OAAO;CACX,KAAK,MAAM,CAAC,KAAK,UAAU,SAAS,QAAQ,GAAG;EAC7C,QAAQ,IAAI;EACZ,IAAI,iBAAiB,MACnB,QAAQ,MAAM;OAEd,QAAQ,MAAM;CAElB;CACA,OAAO;AACT;AAEA,MAAM,wBAAwB;CAC5B,SAAS;CACT,QAAQ;CACR,KAAK;CACL,MAAM;CACN,SAAS;CACT,OAAO;CACP,MAAM;CACN,KAAK;CACL,OAAO;CAEP,OAAO;AACT;;;;;AAKA,SAAgB,2BAA2B,QAAwB;CACjE,MAAM,YAAY,OAAO,YAAY;CACrC,IAAI,OAAO,OAAO,uBAAuB,SAAS,GAChD,OAAO;CAET,OAAO;AACT"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/utils/url.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parses a URL, resolving relative URLs against document.baseURI (or location.href)
|
|
4
|
+
* @param url
|
|
5
|
+
*/
|
|
6
|
+
function parseUrl(url) {
|
|
7
|
+
return new URL(url, document?.baseURI || location?.href);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Tells if the given URL matches any of the string|RegExp provided in the list
|
|
11
|
+
* @param url
|
|
12
|
+
* @param urlsToMatch
|
|
13
|
+
*/
|
|
14
|
+
function matchesUrl(url, urlsToMatch) {
|
|
15
|
+
if (!urlsToMatch) return false;
|
|
16
|
+
for (const urlToMatch of urlsToMatch) if (typeof urlToMatch === "string") {
|
|
17
|
+
if (url === urlToMatch) return true;
|
|
18
|
+
} else if (url.match(urlToMatch)) return true;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const HTTP_PORT_FROM_PROTOCOL = {
|
|
22
|
+
"https:": "443",
|
|
23
|
+
"http:": "80"
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Extracts the server port from the given URL object
|
|
27
|
+
*/
|
|
28
|
+
function serverPortFromUrl(url) {
|
|
29
|
+
const serverPort = Number(url.port || HTTP_PORT_FROM_PROTOCOL[url.protocol]);
|
|
30
|
+
if (serverPort && !Number.isNaN(serverPort)) return serverPort;
|
|
31
|
+
else return;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { matchesUrl, parseUrl, serverPortFromUrl };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.js","names":[],"sources":["../../src/utils/url.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nconst SENSITIVE_PARAMS = [\n 'password',\n 'passwd',\n 'secret',\n 'api_key',\n 'apikey',\n 'auth',\n 'authorization',\n 'token',\n 'access_token',\n 'refresh_token',\n 'jwt',\n 'session',\n 'sessionid',\n 'key',\n 'private_key',\n 'client_secret',\n 'client_id',\n 'signature',\n 'hash',\n];\n\n/**\n * Default URL sanitization function that redacts credentials and sensitive query parameters.\n * This is the default implementation used when no custom sanitizeUrl callback is provided.\n *\n * @param url - The URL to sanitize\n * @returns The sanitized URL with credentials and sensitive parameters redacted\n */\nexport function defaultSanitizeUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n if (urlObj.username || urlObj.password) {\n urlObj.username = 'REDACTED';\n urlObj.password = 'REDACTED';\n }\n for (const param of SENSITIVE_PARAMS) {\n if (urlObj.searchParams.has(param)) {\n urlObj.searchParams.set(param, 'REDACTED');\n }\n }\n return urlObj.toString();\n } catch {\n let sanitized = url.replace(/\\/\\/[^:/@]+:[^/@]+@/, '//REDACTED:REDACTED@');\n for (const param of SENSITIVE_PARAMS) {\n const regex = new RegExp(`([?&]${param}(?:%3D|=))[^&]*`, 'gi');\n sanitized = sanitized.replace(regex, '$1REDACTED');\n }\n return sanitized;\n }\n}\n\n/**\n * Parses a URL, resolving relative URLs against document.baseURI (or location.href)\n * @param url\n */\nexport function parseUrl(url: string): URL {\n return new URL(url, document?.baseURI || location?.href);\n}\n\n/**\n * Tells if the given URL matches any of the string|RegExp provided in the list\n * @param url\n * @param urlsToMatch\n */\nexport function matchesUrl(\n url: string,\n urlsToMatch?: Array<string | RegExp>,\n): boolean {\n if (!urlsToMatch) {\n return false;\n }\n\n for (const urlToMatch of urlsToMatch) {\n if (typeof urlToMatch === 'string') {\n if (url === urlToMatch) {\n return true;\n }\n } else if (url.match(urlToMatch)) {\n return true;\n }\n }\n return false;\n}\n\nconst HTTP_PORT_FROM_PROTOCOL: { [key: string]: string } = {\n 'https:': '443',\n 'http:': '80',\n};\n/**\n * Extracts the server port from the given URL object\n */\nexport function serverPortFromUrl(url: URL): number | undefined {\n const serverPort = Number(url.port || HTTP_PORT_FROM_PROTOCOL[url.protocol]);\n // Guard with `if (serverPort)` because `Number('') === 0`.\n if (serverPort && !Number.isNaN(serverPort)) {\n return serverPort;\n } else {\n return undefined;\n }\n}\n"],"mappings":";;;;;AA6DA,SAAgB,SAAS,KAAkB;CACzC,OAAO,IAAI,IAAI,KAAK,UAAU,WAAW,UAAU,IAAI;AACzD;;;;;;AAOA,SAAgB,WACd,KACA,aACS;CACT,IAAI,CAAC,aACH,OAAO;CAGT,KAAK,MAAM,cAAc,aACvB,IAAI,OAAO,eAAe,UACpB;MAAA,QAAQ,YACV,OAAO;CAAA,OAEJ,IAAI,IAAI,MAAM,UAAU,GAC7B,OAAO;CAGX,OAAO;AACT;AAEA,MAAM,0BAAqD;CACzD,UAAU;CACV,SAAS;AACX;;;;AAIA,SAAgB,kBAAkB,KAA8B;CAC9D,MAAM,aAAa,OAAO,IAAI,QAAQ,wBAAwB,IAAI,SAAS;CAE3E,IAAI,cAAc,CAAC,OAAO,MAAM,UAAU,GACxC,OAAO;MAEP;AAEJ"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { XhrInstrumentationConfig } from "./types.js";
|
|
2
|
+
import { InstrumentationBase } from "@opentelemetry/instrumentation";
|
|
3
|
+
//#region src/xhr/instrumentation.d.ts
|
|
4
|
+
declare class XhrInstrumentation extends InstrumentationBase<XhrInstrumentationConfig> {
|
|
5
|
+
private _isEnabled;
|
|
6
|
+
private _isXhrPatched;
|
|
7
|
+
private _xhrSpanMap;
|
|
8
|
+
constructor(config?: XhrInstrumentationConfig);
|
|
9
|
+
protected init(): never[];
|
|
10
|
+
enable(): void;
|
|
11
|
+
disable(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Patches the "open" method of XmlHttpRequest
|
|
14
|
+
*/
|
|
15
|
+
private _patchOpen;
|
|
16
|
+
/**
|
|
17
|
+
* Patches the "open" method of XmlHttpRequest
|
|
18
|
+
*/
|
|
19
|
+
private _patchSend;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new span when method "send" is called
|
|
22
|
+
*/
|
|
23
|
+
private _createSpan;
|
|
24
|
+
/**
|
|
25
|
+
* Finish span, add attributes, network events etc.
|
|
26
|
+
*/
|
|
27
|
+
private _endSpan;
|
|
28
|
+
/**
|
|
29
|
+
* Runs the user provided `applyCustomAttributesOnSpan` function
|
|
30
|
+
*/
|
|
31
|
+
private _applyAttributesAfterSend;
|
|
32
|
+
/**
|
|
33
|
+
* Adds custom headers to XMLHttpRequest
|
|
34
|
+
*/
|
|
35
|
+
private _addHeaders;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { XhrInstrumentation };
|
|
39
|
+
//# sourceMappingURL=instrumentation.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { version } from "../package.js";
|
|
2
|
+
import { getNetworkContextRegistry } from "../utils/NetworkContextRegistry.js";
|
|
3
|
+
import { getXHRBodyLength, 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/xhr/instrumentation.ts
|
|
10
|
+
var XhrInstrumentation = class extends InstrumentationBase {
|
|
11
|
+
_xhrSpanMap = /* @__PURE__ */ new WeakMap();
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
super("@opentelemetry/browser-instrumentation/xhr", version, config);
|
|
14
|
+
}
|
|
15
|
+
init() {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
enable() {
|
|
19
|
+
if (this._isEnabled) return;
|
|
20
|
+
if (this._isXhrPatched) {
|
|
21
|
+
this._diag.debug("XMLHttpRequest prototype already patched");
|
|
22
|
+
this._isEnabled = true;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
this._wrap(XMLHttpRequest.prototype, "open", this._patchOpen());
|
|
27
|
+
} catch (err) {
|
|
28
|
+
this._diag.warn("Failed to patch XMLHttpRequest.prototype.open; instrumentation will not be enabled. Another script may have locked XMLHttpRequest.prototype.open via Object.defineProperty.", err);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
this._wrap(XMLHttpRequest.prototype, "send", this._patchSend());
|
|
33
|
+
this._isXhrPatched = true;
|
|
34
|
+
this._isEnabled = true;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
this._unwrap(XMLHttpRequest.prototype, "open");
|
|
37
|
+
this._diag.warn("Failed to patch XMLHttpRequest.prototype.send; instrumentation will not be enabled. Another script may have locked XMLHttpRequest.prototype.send via Object.defineProperty.", err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
disable() {
|
|
41
|
+
if (!this._isEnabled) return;
|
|
42
|
+
this._isEnabled = false;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Patches the "open" method of XmlHttpRequest
|
|
46
|
+
*/
|
|
47
|
+
_patchOpen() {
|
|
48
|
+
return (original) => {
|
|
49
|
+
const instrumentation = this;
|
|
50
|
+
return function patchedOpen(...args) {
|
|
51
|
+
if (!instrumentation._isEnabled) return original.apply(this, args);
|
|
52
|
+
try {
|
|
53
|
+
const method = args[0];
|
|
54
|
+
const urlArgument = typeof args[1] === "string" ? args[1] : args[1].toString();
|
|
55
|
+
const url = parseUrl(urlArgument);
|
|
56
|
+
if (matchesUrl(url.href, instrumentation.getConfig().ignoreUrls)) return original.apply(this, args);
|
|
57
|
+
instrumentation._xhrSpanMap.set(this, {
|
|
58
|
+
method,
|
|
59
|
+
url
|
|
60
|
+
});
|
|
61
|
+
} catch (e) {
|
|
62
|
+
instrumentation._diag.error("Failed to instrument XmlHttpRequest.open", e);
|
|
63
|
+
}
|
|
64
|
+
return original.apply(this, args);
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Patches the "open" method of XmlHttpRequest
|
|
70
|
+
*/
|
|
71
|
+
_patchSend() {
|
|
72
|
+
return (original) => {
|
|
73
|
+
const instrumentation = this;
|
|
74
|
+
return function patchedSend(...args) {
|
|
75
|
+
if (!instrumentation._isEnabled) return original.apply(this, args);
|
|
76
|
+
const xhrRecord = instrumentation._xhrSpanMap.get(this);
|
|
77
|
+
if (xhrRecord) try {
|
|
78
|
+
const { method, url } = xhrRecord;
|
|
79
|
+
const span = instrumentation._createSpan(url, method);
|
|
80
|
+
xhrRecord.span = span;
|
|
81
|
+
xhrRecord.start = performance.now();
|
|
82
|
+
if (instrumentation.getConfig().measureRequestSize && args?.[0]) {
|
|
83
|
+
const bodyLength = getXHRBodyLength(args[0]);
|
|
84
|
+
if (bodyLength) span.setAttribute(ATTR_HTTP_REQUEST_BODY_SIZE, bodyLength);
|
|
85
|
+
}
|
|
86
|
+
const onAbort = () => instrumentation._endSpan(this, "abort");
|
|
87
|
+
const onError = () => instrumentation._endSpan(this, "error");
|
|
88
|
+
const onTimeout = () => instrumentation._endSpan(this, "timeout");
|
|
89
|
+
const onLoad = () => instrumentation._endSpan(this, "load");
|
|
90
|
+
xhrRecord.listeners = {
|
|
91
|
+
abort: onAbort,
|
|
92
|
+
error: onError,
|
|
93
|
+
timeout: onTimeout,
|
|
94
|
+
load: onLoad
|
|
95
|
+
};
|
|
96
|
+
const xhrContext = trace.setSpan(context.active(), span);
|
|
97
|
+
context.with(xhrContext, () => {
|
|
98
|
+
this.addEventListener("abort", onAbort);
|
|
99
|
+
this.addEventListener("error", onError);
|
|
100
|
+
this.addEventListener("timeout", onTimeout);
|
|
101
|
+
this.addEventListener("load", onLoad);
|
|
102
|
+
instrumentation._addHeaders(this, url, xhrContext);
|
|
103
|
+
});
|
|
104
|
+
} catch (e) {
|
|
105
|
+
instrumentation._diag.error("Failed to instrument fetch request", e);
|
|
106
|
+
instrumentation._xhrSpanMap.delete(this);
|
|
107
|
+
}
|
|
108
|
+
return original.apply(this, args);
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Creates a new span when method "send" is called
|
|
114
|
+
*/
|
|
115
|
+
_createSpan(url, method) {
|
|
116
|
+
const origMethod = method;
|
|
117
|
+
const normMethod = normalizeHttpRequestMethod(method);
|
|
118
|
+
const attributes = {};
|
|
119
|
+
const { sanitizeUrl } = this.getConfig();
|
|
120
|
+
attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;
|
|
121
|
+
if (normMethod !== origMethod) attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;
|
|
122
|
+
attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url.href) : url.href;
|
|
123
|
+
attributes[ATTR_SERVER_ADDRESS] = url.hostname;
|
|
124
|
+
const serverPort = serverPortFromUrl(url);
|
|
125
|
+
if (serverPort) attributes[ATTR_SERVER_PORT] = serverPort;
|
|
126
|
+
return this.tracer.startSpan(normMethod, {
|
|
127
|
+
kind: SpanKind.CLIENT,
|
|
128
|
+
attributes
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Finish span, add attributes, network events etc.
|
|
133
|
+
*/
|
|
134
|
+
_endSpan(xhr, eventName) {
|
|
135
|
+
const xhrRecord = this._xhrSpanMap.get(xhr);
|
|
136
|
+
if (xhrRecord?.listeners) for (const k of Object.keys(xhrRecord.listeners)) xhr.removeEventListener(k, xhrRecord.listeners[k]);
|
|
137
|
+
if (xhrRecord?.span) {
|
|
138
|
+
const { span, url, start } = xhrRecord;
|
|
139
|
+
if (eventName === "load") span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, xhr.status);
|
|
140
|
+
if (eventName !== "abort") {
|
|
141
|
+
if (xhr.status >= 400 || eventName === "error" || eventName === "timeout") {
|
|
142
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
143
|
+
const errorType = xhr.status >= 400 ? String(xhr.status) : eventName === "error" ? "Error" : "TimeoutError";
|
|
144
|
+
span.setAttribute(ATTR_ERROR_TYPE, errorType);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this._xhrSpanMap.delete(xhr);
|
|
148
|
+
this._applyAttributesAfterSend(span, xhr);
|
|
149
|
+
span.end();
|
|
150
|
+
getNetworkContextRegistry().register(span, {
|
|
151
|
+
key: url.href,
|
|
152
|
+
startPerfNow: Number(start),
|
|
153
|
+
endPerfNow: performance.now()
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Runs the user provided `applyCustomAttributesOnSpan` function
|
|
159
|
+
*/
|
|
160
|
+
_applyAttributesAfterSend(span, xhr) {
|
|
161
|
+
const applyCustomAttributesOnSpan = this.getConfig().applyCustomAttributesOnSpan;
|
|
162
|
+
if (applyCustomAttributesOnSpan) safeExecuteInTheMiddle(() => applyCustomAttributesOnSpan(span, xhr), (error) => {
|
|
163
|
+
if (!error) return;
|
|
164
|
+
this._diag.error("applyCustomAttributesOnSpan", error);
|
|
165
|
+
}, true);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Adds custom headers to XMLHttpRequest
|
|
169
|
+
*/
|
|
170
|
+
_addHeaders(xhr, url, ctx) {
|
|
171
|
+
const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;
|
|
172
|
+
if (location.origin === url.origin || matchesUrl(url.href, urlsToPropagate)) propagation.inject(ctx, xhr, { set: (x, k, v) => {
|
|
173
|
+
x.setRequestHeader(k, typeof v === "string" ? v : String(v));
|
|
174
|
+
} });
|
|
175
|
+
else {
|
|
176
|
+
const headers = {};
|
|
177
|
+
propagation.inject(ctx, headers, { set: (h, k, v) => {
|
|
178
|
+
h[k] = v;
|
|
179
|
+
} });
|
|
180
|
+
if (Object.keys(headers).length > 0) this._diag.debug("headers inject skipped due to CORS policy");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
//#endregion
|
|
185
|
+
export { XhrInstrumentation };
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=instrumentation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/xhr/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 getXHRBodyLength,\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 { XhrInstrumentationConfig } from './types.ts';\n\ntype XhrOpenFunction = typeof XMLHttpRequest.prototype.open;\ntype XhrSendFunction = typeof XMLHttpRequest.prototype.send;\ntype XhrEventName = 'abort' | 'timeout' | 'error' | 'load';\ntype XhrRecord = {\n method: string;\n url: URL;\n start?: number;\n span?: Span;\n listeners?: Record<XhrEventName, (...args: unknown[]) => unknown>;\n};\n\nexport class XhrInstrumentation extends InstrumentationBase<XhrInstrumentationConfig> {\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 instrumentaitons in a base state (enabled, patched but with flags set to false)\n private declare _isEnabled: boolean;\n private declare _isXhrPatched: boolean;\n\n // To keep references to span/xhr tuples across XHR events and stores\n // init data like URL and method\n private _xhrSpanMap: WeakMap<XMLHttpRequest, XhrRecord> = new WeakMap();\n\n constructor(config: XhrInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/xhr', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (this._isEnabled) {\n return;\n }\n\n if (this._isXhrPatched) {\n this._diag.debug('XMLHttpRequest prototype already patched');\n this._isEnabled = true;\n return;\n }\n\n // `_wrap` throws if a third-party script has locked the target methods via\n // Object.defineProperty(XMLHttpRequest.prototype, 'open', { writable: false, ... }).\n try {\n this._wrap(XMLHttpRequest.prototype, 'open', this._patchOpen());\n } catch (err) {\n this._diag.warn(\n 'Failed to patch XMLHttpRequest.prototype.open; instrumentation will not be enabled. ' +\n 'Another script may have locked XMLHttpRequest.prototype.open via Object.defineProperty.',\n err,\n );\n return;\n }\n\n // If 1st patch has succeded try the second. Unpatch `open` if error\n // here to avoid having multiple patches of `open`\n try {\n this._wrap(XMLHttpRequest.prototype, 'send', this._patchSend());\n this._isXhrPatched = true;\n this._isEnabled = true;\n } catch (err) {\n this._unwrap(XMLHttpRequest.prototype, 'open');\n this._diag.warn(\n 'Failed to patch XMLHttpRequest.prototype.send; instrumentation will not be enabled. ' +\n 'Another script may have locked XMLHttpRequest.prototype.send via Object.defineProperty.',\n err,\n );\n }\n }\n\n override disable(): void {\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n }\n\n /**\n * Patches the \"open\" method of XmlHttpRequest\n */\n private _patchOpen(): (original: XhrOpenFunction) => XhrOpenFunction {\n return (original) => {\n const instrumentation = this;\n\n return function patchedOpen(\n this: XMLHttpRequest,\n ...args: Parameters<XhrOpenFunction>\n ): ReturnType<XhrOpenFunction> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n try {\n const method = args[0];\n const urlArgument =\n typeof args[1] === 'string' ? args[1] : args[1].toString();\n const url = parseUrl(urlArgument);\n const shouldIgnoreUrl = matchesUrl(\n url.href,\n instrumentation.getConfig().ignoreUrls,\n );\n if (shouldIgnoreUrl) {\n return original.apply(this, args);\n }\n instrumentation._xhrSpanMap.set(this, { method, url });\n } catch (e: unknown) {\n instrumentation._diag.error(\n 'Failed to instrument XmlHttpRequest.open',\n e,\n );\n }\n return original.apply(this, args);\n } as XhrOpenFunction;\n };\n }\n\n /**\n * Patches the \"open\" method of XmlHttpRequest\n */\n private _patchSend(): (original: XhrSendFunction) => XhrSendFunction {\n return (original) => {\n const instrumentation = this;\n\n return function patchedSend(\n this: XMLHttpRequest,\n ...args: Parameters<XhrSendFunction>\n ): ReturnType<XhrSendFunction> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n\n // Instrument the method if the XHR was previously picked in `open`\n const xhrRecord = instrumentation._xhrSpanMap.get(this);\n if (xhrRecord) {\n try {\n const { method, url } = xhrRecord;\n const span = instrumentation._createSpan(url, method);\n xhrRecord.span = span;\n xhrRecord.start = performance.now();\n\n if (instrumentation.getConfig().measureRequestSize && args?.[0]) {\n const bodyLength = getXHRBodyLength(args[0]);\n if (bodyLength) {\n span.setAttribute(ATTR_HTTP_REQUEST_BODY_SIZE, bodyLength);\n }\n }\n\n const onAbort = () => instrumentation._endSpan(this, 'abort');\n const onError = () => instrumentation._endSpan(this, 'error');\n const onTimeout = () => instrumentation._endSpan(this, 'timeout');\n const onLoad = () => instrumentation._endSpan(this, 'load');\n xhrRecord.listeners = {\n abort: onAbort,\n error: onError,\n timeout: onTimeout,\n load: onLoad,\n };\n\n const xhrContext = trace.setSpan(context.active(), span);\n context.with(xhrContext, () => {\n this.addEventListener('abort', onAbort);\n this.addEventListener('error', onError);\n this.addEventListener('timeout', onTimeout);\n this.addEventListener('load', onLoad);\n instrumentation._addHeaders(this, url, xhrContext);\n });\n } catch (e: unknown) {\n // failed to instrument request, remove span\n instrumentation._diag.error(\n 'Failed to instrument fetch request',\n e,\n );\n instrumentation._xhrSpanMap.delete(this);\n }\n }\n return original.apply(this, args);\n } as XhrSendFunction;\n };\n }\n\n /**\n * Creates a new span when method \"send\" is called\n */\n private _createSpan(url: URL, method: string): Span {\n const origMethod = method;\n const normMethod = normalizeHttpRequestMethod(method);\n const attributes = {} as Attributes;\n const { sanitizeUrl } = this.getConfig();\n\n attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;\n if (normMethod !== origMethod) {\n attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;\n }\n attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url.href) : url.href;\n attributes[ATTR_SERVER_ADDRESS] = url.hostname;\n const serverPort = serverPortFromUrl(url);\n if (serverPort) {\n attributes[ATTR_SERVER_PORT] = serverPort;\n }\n\n return this.tracer.startSpan(normMethod, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n }\n\n /**\n * Finish span, add attributes, network events etc.\n */\n private _endSpan(xhr: XMLHttpRequest, eventName: XhrEventName) {\n const xhrRecord = this._xhrSpanMap.get(xhr);\n\n if (xhrRecord?.listeners) {\n for (const k of Object.keys(xhrRecord.listeners)) {\n xhr.removeEventListener(\n k as XhrEventName,\n xhrRecord.listeners[k as XhrEventName],\n );\n }\n }\n\n if (xhrRecord?.span) {\n const { span, url, start } = xhrRecord;\n\n // Status code only has a meaningful value if request got a response\n if (eventName === 'load') {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, xhr.status);\n }\n\n if (eventName !== 'abort') {\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 const isErrorStatus =\n xhr.status >= 400 || eventName === 'error' || eventName === 'timeout';\n\n if (isErrorStatus) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n const errorType =\n xhr.status >= 400\n ? String(xhr.status)\n : eventName === 'error'\n ? 'Error'\n : 'TimeoutError';\n span.setAttribute(ATTR_ERROR_TYPE, errorType);\n }\n }\n\n this._xhrSpanMap.delete(xhr);\n this._applyAttributesAfterSend(span, xhr);\n span.end();\n\n getNetworkContextRegistry().register(span, {\n key: url.href,\n startPerfNow: Number(start),\n endPerfNow: performance.now(),\n });\n }\n }\n\n /**\n * Runs the user provided `applyCustomAttributesOnSpan` function\n */\n private _applyAttributesAfterSend(span: Span, xhr: XMLHttpRequest) {\n const applyCustomAttributesOnSpan =\n this.getConfig().applyCustomAttributesOnSpan;\n if (applyCustomAttributesOnSpan) {\n safeExecuteInTheMiddle(\n () => applyCustomAttributesOnSpan(span, xhr),\n (error) => {\n if (!error) {\n return;\n }\n this._diag.error('applyCustomAttributesOnSpan', error);\n },\n true,\n );\n }\n }\n\n /**\n * Adds custom headers to XMLHttpRequest\n */\n private _addHeaders(xhr: XMLHttpRequest, url: URL, ctx: Context) {\n // Propagate only if in request goes to same origin or is in the allow list\n const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;\n const sameOrigin = location.origin === url.origin;\n const shouldPropagate = sameOrigin || matchesUrl(url.href, urlsToPropagate);\n\n if (shouldPropagate) {\n propagation.inject(ctx, xhr, {\n set: (x, k, v) => {\n x.setRequestHeader(k, typeof v === 'string' ? v : String(v));\n },\n });\n } else {\n const headers: Partial<Record<string, unknown>> = {};\n propagation.inject(ctx, headers, {\n set: (h, k, v) => {\n h[k] = v;\n },\n });\n if (Object.keys(headers).length > 0) {\n this._diag.debug('headers inject skipped due to CORS policy');\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;AA+CA,IAAa,qBAAb,cAAwC,oBAA8C;CAYpF,8BAA0D,IAAI,QAAQ;CAEtE,YAAY,SAAmC,CAAC,GAAG;EACjD,MAAM,8CAA8C,SAAS,MAAM;CACrE;CAEA,OAA0B;EACxB,OAAO,CAAC;CACV;CAEA,SAAwB;EACtB,IAAI,KAAK,YACP;EAGF,IAAI,KAAK,eAAe;GACtB,KAAK,MAAM,MAAM,0CAA0C;GAC3D,KAAK,aAAa;GAClB;EACF;EAIA,IAAI;GACF,KAAK,MAAM,eAAe,WAAW,QAAQ,KAAK,WAAW,CAAC;EAChE,SAAS,KAAK;GACZ,KAAK,MAAM,KACT,+KAEA,GACF;GACA;EACF;EAIA,IAAI;GACF,KAAK,MAAM,eAAe,WAAW,QAAQ,KAAK,WAAW,CAAC;GAC9D,KAAK,gBAAgB;GACrB,KAAK,aAAa;EACpB,SAAS,KAAK;GACZ,KAAK,QAAQ,eAAe,WAAW,MAAM;GAC7C,KAAK,MAAM,KACT,+KAEA,GACF;EACF;CACF;CAEA,UAAyB;EACvB,IAAI,CAAC,KAAK,YACR;EAEF,KAAK,aAAa;CACpB;;;;CAKA,aAAqE;EACnE,QAAQ,aAAa;GACnB,MAAM,kBAAkB;GAExB,OAAO,SAAS,YAEd,GAAG,MAC0B;IAC7B,IAAI,CAAC,gBAAgB,YACnB,OAAO,SAAS,MAAM,MAAM,IAAI;IAElC,IAAI;KACF,MAAM,SAAS,KAAK;KACpB,MAAM,cACJ,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK,KAAK,EAAE,CAAC,SAAS;KAC3D,MAAM,MAAM,SAAS,WAAW;KAKhC,IAJwB,WACtB,IAAI,MACJ,gBAAgB,UAAU,CAAC,CAAC,UAEZ,GAChB,OAAO,SAAS,MAAM,MAAM,IAAI;KAElC,gBAAgB,YAAY,IAAI,MAAM;MAAE;MAAQ;KAAI,CAAC;IACvD,SAAS,GAAY;KACnB,gBAAgB,MAAM,MACpB,4CACA,CACF;IACF;IACA,OAAO,SAAS,MAAM,MAAM,IAAI;GAClC;EACF;CACF;;;;CAKA,aAAqE;EACnE,QAAQ,aAAa;GACnB,MAAM,kBAAkB;GAExB,OAAO,SAAS,YAEd,GAAG,MAC0B;IAC7B,IAAI,CAAC,gBAAgB,YACnB,OAAO,SAAS,MAAM,MAAM,IAAI;IAIlC,MAAM,YAAY,gBAAgB,YAAY,IAAI,IAAI;IACtD,IAAI,WACF,IAAI;KACF,MAAM,EAAE,QAAQ,QAAQ;KACxB,MAAM,OAAO,gBAAgB,YAAY,KAAK,MAAM;KACpD,UAAU,OAAO;KACjB,UAAU,QAAQ,YAAY,IAAI;KAElC,IAAI,gBAAgB,UAAU,CAAC,CAAC,sBAAsB,OAAO,IAAI;MAC/D,MAAM,aAAa,iBAAiB,KAAK,EAAE;MAC3C,IAAI,YACF,KAAK,aAAa,6BAA6B,UAAU;KAE7D;KAEA,MAAM,gBAAgB,gBAAgB,SAAS,MAAM,OAAO;KAC5D,MAAM,gBAAgB,gBAAgB,SAAS,MAAM,OAAO;KAC5D,MAAM,kBAAkB,gBAAgB,SAAS,MAAM,SAAS;KAChE,MAAM,eAAe,gBAAgB,SAAS,MAAM,MAAM;KAC1D,UAAU,YAAY;MACpB,OAAO;MACP,OAAO;MACP,SAAS;MACT,MAAM;KACR;KAEA,MAAM,aAAa,MAAM,QAAQ,QAAQ,OAAO,GAAG,IAAI;KACvD,QAAQ,KAAK,kBAAkB;MAC7B,KAAK,iBAAiB,SAAS,OAAO;MACtC,KAAK,iBAAiB,SAAS,OAAO;MACtC,KAAK,iBAAiB,WAAW,SAAS;MAC1C,KAAK,iBAAiB,QAAQ,MAAM;MACpC,gBAAgB,YAAY,MAAM,KAAK,UAAU;KACnD,CAAC;IACH,SAAS,GAAY;KAEnB,gBAAgB,MAAM,MACpB,sCACA,CACF;KACA,gBAAgB,YAAY,OAAO,IAAI;IACzC;IAEF,OAAO,SAAS,MAAM,MAAM,IAAI;GAClC;EACF;CACF;;;;CAKA,YAAoB,KAAU,QAAsB;EAClD,MAAM,aAAa;EACnB,MAAM,aAAa,2BAA2B,MAAM;EACpD,MAAM,aAAa,CAAC;EACpB,MAAM,EAAE,gBAAgB,KAAK,UAAU;EAEvC,WAAW,4BAA4B;EACvC,IAAI,eAAe,YACjB,WAAW,qCAAqC;EAElD,WAAW,iBAAiB,cAAc,YAAY,IAAI,IAAI,IAAI,IAAI;EACtE,WAAW,uBAAuB,IAAI;EACtC,MAAM,aAAa,kBAAkB,GAAG;EACxC,IAAI,YACF,WAAW,oBAAoB;EAGjC,OAAO,KAAK,OAAO,UAAU,YAAY;GACvC,MAAM,SAAS;GACf;EACF,CAAC;CACH;;;;CAKA,SAAiB,KAAqB,WAAyB;EAC7D,MAAM,YAAY,KAAK,YAAY,IAAI,GAAG;EAE1C,IAAI,WAAW,WACb,KAAK,MAAM,KAAK,OAAO,KAAK,UAAU,SAAS,GAC7C,IAAI,oBACF,GACA,UAAU,UAAU,EACtB;EAIJ,IAAI,WAAW,MAAM;GACnB,MAAM,EAAE,MAAM,KAAK,UAAU;GAG7B,IAAI,cAAc,QAChB,KAAK,aAAa,gCAAgC,IAAI,MAAM;GAG9D,IAAI,cAAc,SAKd;QAAA,IAAI,UAAU,OAAO,cAAc,WAAW,cAAc,WAE3C;KACjB,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;KAC7C,MAAM,YACJ,IAAI,UAAU,MACV,OAAO,IAAI,MAAM,IACjB,cAAc,UACZ,UACA;KACR,KAAK,aAAa,iBAAiB,SAAS;IAC9C;;GAGF,KAAK,YAAY,OAAO,GAAG;GAC3B,KAAK,0BAA0B,MAAM,GAAG;GACxC,KAAK,IAAI;GAET,0BAA0B,CAAC,CAAC,SAAS,MAAM;IACzC,KAAK,IAAI;IACT,cAAc,OAAO,KAAK;IAC1B,YAAY,YAAY,IAAI;GAC9B,CAAC;EACH;CACF;;;;CAKA,0BAAkC,MAAY,KAAqB;EACjE,MAAM,8BACJ,KAAK,UAAU,CAAC,CAAC;EACnB,IAAI,6BACF,6BACQ,4BAA4B,MAAM,GAAG,IAC1C,UAAU;GACT,IAAI,CAAC,OACH;GAEF,KAAK,MAAM,MAAM,+BAA+B,KAAK;EACvD,GACA,IACF;CAEJ;;;;CAKA,YAAoB,KAAqB,KAAU,KAAc;EAE/D,MAAM,kBAAkB,KAAK,UAAU,CAAC,CAAC;EAIzC,IAHmB,SAAS,WAAW,IAAI,UACL,WAAW,IAAI,MAAM,eAAe,GAGxE,YAAY,OAAO,KAAK,KAAK,EAC3B,MAAM,GAAG,GAAG,MAAM;GAChB,EAAE,iBAAiB,GAAG,OAAO,MAAM,WAAW,IAAI,OAAO,CAAC,CAAC;EAC7D,EACF,CAAC;OACI;GACL,MAAM,UAA4C,CAAC;GACnD,YAAY,OAAO,KAAK,SAAS,EAC/B,MAAM,GAAG,GAAG,MAAM;IAChB,EAAE,KAAK;GACT,EACF,CAAC;GACD,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAChC,KAAK,MAAM,MAAM,2CAA2C;EAEhE;CACF;AACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/xhr/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/xhr/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"}
|