@dash0/sdk-web 0.6.2
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/LICENSE +202 -0
- package/README.md +496 -0
- package/dist/dash0.iife.js +2 -0
- package/dist/dash0.iife.js.map +1 -0
- package/dist/dash0.js +2 -0
- package/dist/dash0.js.map +1 -0
- package/dist/dash0.umd.cjs +2 -0
- package/dist/dash0.umd.cjs.map +1 -0
- package/dist/modules/api/attributes.js +16 -0
- package/dist/modules/api/debug.js +5 -0
- package/dist/modules/api/events.js +30 -0
- package/dist/modules/api/identify.js +29 -0
- package/dist/modules/api/init.js +130 -0
- package/dist/modules/api/report-error.js +4 -0
- package/dist/modules/api/session.js +88 -0
- package/dist/modules/attributes/common.js +26 -0
- package/dist/modules/attributes/index.js +2 -0
- package/dist/modules/attributes/url.js +25 -0
- package/dist/modules/entrypoint/npm-package.js +12 -0
- package/dist/modules/entrypoint/script.js +60 -0
- package/dist/modules/instrumentations/errors/async-function-wrapping.js +105 -0
- package/dist/modules/instrumentations/errors/event-handlers.js +52 -0
- package/dist/modules/instrumentations/errors/index.js +10 -0
- package/dist/modules/instrumentations/errors/timers.js +46 -0
- package/dist/modules/instrumentations/errors/unhandled-error.js +137 -0
- package/dist/modules/instrumentations/errors/unhandled-promise-rejection.js +29 -0
- package/dist/modules/instrumentations/http/fetch.js +164 -0
- package/dist/modules/instrumentations/http/utils.js +35 -0
- package/dist/modules/instrumentations/navigation/event.js +44 -0
- package/dist/modules/instrumentations/navigation/index.js +6 -0
- package/dist/modules/instrumentations/navigation/page-load.js +83 -0
- package/dist/modules/instrumentations/navigation/page-transition.js +72 -0
- package/dist/modules/instrumentations/web-vitals.js +32 -0
- package/dist/modules/semantic-conventions.js +79 -0
- package/dist/modules/transport/batcher.js +48 -0
- package/dist/modules/transport/fetch.js +58 -0
- package/dist/modules/transport/index.js +66 -0
- package/dist/modules/utils/constants.js +2 -0
- package/dist/modules/utils/debug.js +19 -0
- package/dist/modules/utils/fn.js +3 -0
- package/dist/modules/utils/globals.js +36 -0
- package/dist/modules/utils/id.js +16 -0
- package/dist/modules/utils/id_test.js +33 -0
- package/dist/modules/utils/ignore-rules.js +48 -0
- package/dist/modules/utils/index.js +16 -0
- package/dist/modules/utils/listeners.js +18 -0
- package/dist/modules/utils/local-storage.js +19 -0
- package/dist/modules/utils/math.js +3 -0
- package/dist/modules/utils/obj.js +10 -0
- package/dist/modules/utils/on-last-chance.js +31 -0
- package/dist/modules/utils/origin.js +14 -0
- package/dist/modules/utils/otel/attributes.js +55 -0
- package/dist/modules/utils/otel/attributes_test.js +158 -0
- package/dist/modules/utils/otel/error.js +30 -0
- package/dist/modules/utils/otel/http.js +7 -0
- package/dist/modules/utils/otel/index.js +4 -0
- package/dist/modules/utils/otel/span.js +56 -0
- package/dist/modules/utils/otel/trace-context.js +60 -0
- package/dist/modules/utils/performance.js +162 -0
- package/dist/modules/utils/pick.js +17 -0
- package/dist/modules/utils/pick_test.js +104 -0
- package/dist/modules/utils/rate-limit.js +20 -0
- package/dist/modules/utils/session-storage.js +21 -0
- package/dist/modules/utils/tab-id.js +29 -0
- package/dist/modules/utils/time.js +28 -0
- package/dist/modules/utils/timers.js +62 -0
- package/dist/modules/utils/url.js +11 -0
- package/dist/modules/utils/wrap.js +22 -0
- package/dist/modules/vars.js +25 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/api/attributes.d.ts +12 -0
- package/dist/types/api/debug.d.ts +1 -0
- package/dist/types/api/events.d.ts +36 -0
- package/dist/types/api/identify.d.ts +9 -0
- package/dist/types/api/init.d.ts +31 -0
- package/dist/types/api/report-error.d.ts +2 -0
- package/dist/types/api/session.d.ts +3 -0
- package/dist/types/attributes/common.d.ts +6 -0
- package/dist/types/attributes/index.d.ts +2 -0
- package/dist/types/attributes/url.d.ts +2 -0
- package/dist/types/entrypoint/npm-package.d.ts +8 -0
- package/dist/types/entrypoint/script.d.ts +1 -0
- package/dist/types/instrumentations/errors/async-function-wrapping.d.ts +8 -0
- package/dist/types/instrumentations/errors/event-handlers.d.ts +1 -0
- package/dist/types/instrumentations/errors/index.d.ts +1 -0
- package/dist/types/instrumentations/errors/timers.d.ts +1 -0
- package/dist/types/instrumentations/errors/unhandled-error.d.ts +5 -0
- package/dist/types/instrumentations/errors/unhandled-promise-rejection.d.ts +2 -0
- package/dist/types/instrumentations/http/fetch.d.ts +1 -0
- package/dist/types/instrumentations/http/utils.d.ts +5 -0
- package/dist/types/instrumentations/navigation/event.d.ts +1 -0
- package/dist/types/instrumentations/navigation/index.d.ts +1 -0
- package/dist/types/instrumentations/navigation/page-load.d.ts +7 -0
- package/dist/types/instrumentations/navigation/page-transition.d.ts +7 -0
- package/dist/types/instrumentations/web-vitals.d.ts +1 -0
- package/dist/types/semantic-conventions.d.ts +68 -0
- package/dist/types/transport/batcher.d.ts +4 -0
- package/dist/types/transport/fetch.d.ts +1 -0
- package/dist/types/transport/index.d.ts +3 -0
- package/dist/types/utils/constants.d.ts +2 -0
- package/dist/types/utils/debug.d.ts +7 -0
- package/dist/types/utils/fn.d.ts +1 -0
- package/dist/types/utils/globals.d.ts +14 -0
- package/dist/types/utils/id.d.ts +6 -0
- package/dist/types/utils/id_test.d.ts +1 -0
- package/dist/types/utils/ignore-rules.d.ts +3 -0
- package/dist/types/utils/index.d.ts +16 -0
- package/dist/types/utils/listeners.d.ts +2 -0
- package/dist/types/utils/local-storage.d.ts +4 -0
- package/dist/types/utils/math.d.ts +1 -0
- package/dist/types/utils/obj.d.ts +5 -0
- package/dist/types/utils/on-last-chance.d.ts +5 -0
- package/dist/types/utils/origin.d.ts +1 -0
- package/dist/types/utils/otel/attributes.d.ts +9 -0
- package/dist/types/utils/otel/attributes_test.d.ts +1 -0
- package/dist/types/utils/otel/error.d.ts +24 -0
- package/dist/types/utils/otel/http.d.ts +2 -0
- package/dist/types/utils/otel/index.d.ts +4 -0
- package/dist/types/utils/otel/span.d.ts +10 -0
- package/dist/types/utils/otel/trace-context.d.ts +8 -0
- package/dist/types/utils/performance.d.ts +45 -0
- package/dist/types/utils/pick.d.ts +9 -0
- package/dist/types/utils/pick_test.d.ts +1 -0
- package/dist/types/utils/rate-limit.d.ts +5 -0
- package/dist/types/utils/session-storage.d.ts +4 -0
- package/dist/types/utils/tab-id.d.ts +8 -0
- package/dist/types/utils/time.d.ts +6 -0
- package/dist/types/utils/timers.d.ts +6 -0
- package/dist/types/utils/url.d.ts +6 -0
- package/dist/types/utils/wrap.d.ts +1 -0
- package/dist/types/vars.d.ts +123 -0
- package/package.json +124 -0
- package/src/api/attributes.ts +19 -0
- package/src/api/debug.ts +6 -0
- package/src/api/events.ts +67 -0
- package/src/api/identify.ts +43 -0
- package/src/api/init.ts +221 -0
- package/src/api/report-error.ts +6 -0
- package/src/api/session.ts +113 -0
- package/src/attributes/common.ts +42 -0
- package/src/attributes/index.ts +2 -0
- package/src/attributes/url.ts +25 -0
- package/src/entrypoint/npm-package.ts +14 -0
- package/src/entrypoint/script-tag.html +23 -0
- package/src/entrypoint/script.ts +82 -0
- package/src/instrumentations/errors/async-function-wrapping.ts +157 -0
- package/src/instrumentations/errors/event-handlers.ts +77 -0
- package/src/instrumentations/errors/index.ts +11 -0
- package/src/instrumentations/errors/timers.ts +52 -0
- package/src/instrumentations/errors/unhandled-error.ts +187 -0
- package/src/instrumentations/errors/unhandled-promise-rejection.ts +30 -0
- package/src/instrumentations/http/fetch.ts +194 -0
- package/src/instrumentations/http/utils.ts +49 -0
- package/src/instrumentations/navigation/event.ts +66 -0
- package/src/instrumentations/navigation/index.ts +7 -0
- package/src/instrumentations/navigation/page-load.ts +96 -0
- package/src/instrumentations/navigation/page-transition.ts +100 -0
- package/src/instrumentations/web-vitals.ts +37 -0
- package/src/semantic-conventions.ts +91 -0
- package/src/transport/batcher.ts +59 -0
- package/src/transport/fetch.ts +72 -0
- package/src/transport/index.ts +77 -0
- package/src/utils/constants.ts +3 -0
- package/src/utils/debug.ts +25 -0
- package/src/utils/fn.ts +3 -0
- package/src/utils/globals.ts +42 -0
- package/src/utils/id.ts +17 -0
- package/src/utils/id_test.ts +41 -0
- package/src/utils/ignore-rules.ts +59 -0
- package/src/utils/index.ts +16 -0
- package/src/utils/listeners.ts +18 -0
- package/src/utils/local-storage.ts +25 -0
- package/src/utils/math.ts +3 -0
- package/src/utils/obj.ts +12 -0
- package/src/utils/on-last-chance.ts +36 -0
- package/src/utils/origin.ts +15 -0
- package/src/utils/otel/attributes.ts +67 -0
- package/src/utils/otel/attributes_test.ts +181 -0
- package/src/utils/otel/error.ts +63 -0
- package/src/utils/otel/http.ts +9 -0
- package/src/utils/otel/index.ts +4 -0
- package/src/utils/otel/span.ts +70 -0
- package/src/utils/otel/trace-context.ts +84 -0
- package/src/utils/performance.ts +203 -0
- package/src/utils/pick.ts +19 -0
- package/src/utils/pick_test.ts +134 -0
- package/src/utils/rate-limit.ts +34 -0
- package/src/utils/session-storage.ts +27 -0
- package/src/utils/tab-id.ts +33 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/timers.ts +79 -0
- package/src/utils/url.ts +12 -0
- package/src/utils/wrap.ts +33 -0
- package/src/vars.ts +169 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { debug, observeResourcePerformance, win } from "../../utils";
|
|
2
|
+
import { isUrlIgnored, matchesAny } from "../../utils/ignore-rules";
|
|
3
|
+
import { addAttribute, setSpanStatus, addTraceContextHttpHeaders, endSpan, errorToSpanStatus, recordException, startSpan, } from "../../utils/otel";
|
|
4
|
+
import { HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, SPAN_STATUS_ERROR, SPAN_STATUS_UNSET, } from "../../semantic-conventions";
|
|
5
|
+
import { isSameOrigin, wrap, parseUrl } from "../../utils";
|
|
6
|
+
import { vars } from "../../vars";
|
|
7
|
+
import { httpRequestHeaderKey, httpResponseHeaderKey } from "../../utils/otel/http";
|
|
8
|
+
import { sendSpan } from "../../transport";
|
|
9
|
+
import { addResourceNetworkEvents, addResourceSize, HTTP_METHOD_OTHER, isWellKnownHttpMethod } from "./utils";
|
|
10
|
+
import { addCommonAttributes } from "../../attributes";
|
|
11
|
+
export function instrumentFetch() {
|
|
12
|
+
if (!win || !win.fetch || !win.Request) {
|
|
13
|
+
debug("Browser does not support the Fetch API, skipping instrumentation");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
wrap(win, "fetch", wrapFetch);
|
|
17
|
+
}
|
|
18
|
+
// eslint-disable-next-line no-restricted-globals -- only used as type here
|
|
19
|
+
function wrapFetch(original) {
|
|
20
|
+
return async function fetchWithInstrumentation(input, init) {
|
|
21
|
+
let copyOfInit = init ? Object.assign({}, init) : init;
|
|
22
|
+
let body = null;
|
|
23
|
+
if (copyOfInit?.body) {
|
|
24
|
+
body = copyOfInit.body;
|
|
25
|
+
copyOfInit.body = undefined;
|
|
26
|
+
}
|
|
27
|
+
const request = new Request(input, copyOfInit);
|
|
28
|
+
if (body && copyOfInit) {
|
|
29
|
+
copyOfInit.body = body;
|
|
30
|
+
}
|
|
31
|
+
const url = request.url;
|
|
32
|
+
if (isUrlIgnored(url)) {
|
|
33
|
+
debug(`Not creating span for fetch call because the url is ignored, URL: ${url}`);
|
|
34
|
+
return original(input instanceof Request ? request : input, init);
|
|
35
|
+
}
|
|
36
|
+
// https://fetch.spec.whatwg.org/#concept-request-method
|
|
37
|
+
// We'll match methods case insensitive here to make the user experience a bit less painful
|
|
38
|
+
const originalMethod = request.method ?? "GET";
|
|
39
|
+
const isWellKnownMethod = isWellKnownHttpMethod(originalMethod);
|
|
40
|
+
const isWellKnownMethodMatchingLeniently = isWellKnownHttpMethod(originalMethod.toUpperCase());
|
|
41
|
+
const method = isWellKnownMethodMatchingLeniently ? originalMethod.toUpperCase() : HTTP_METHOD_OTHER;
|
|
42
|
+
const span = startSpan(`HTTP ${method}`);
|
|
43
|
+
// The url namespace of a http span has a different meaning than for common rum signals.
|
|
44
|
+
addCommonAttributes(span.attributes, { url });
|
|
45
|
+
addGraphQlProperties(input, init, span);
|
|
46
|
+
addAttribute(span.attributes, HTTP_REQUEST_METHOD, method);
|
|
47
|
+
if (!isWellKnownMethod) {
|
|
48
|
+
addAttribute(span.attributes, HTTP_REQUEST_METHOD_ORIGINAL, originalMethod);
|
|
49
|
+
}
|
|
50
|
+
const shouldSetCorrelationHeaders = isSameOrigin(url) || matchesAny(vars.propagateTraceHeadersCorsURLs, url);
|
|
51
|
+
if (shouldSetCorrelationHeaders) {
|
|
52
|
+
if (copyOfInit?.headers) {
|
|
53
|
+
// ensure we have a unified container for the headers
|
|
54
|
+
copyOfInit.headers = new Headers(copyOfInit.headers);
|
|
55
|
+
addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
|
|
56
|
+
}
|
|
57
|
+
else if (input instanceof Request) {
|
|
58
|
+
addTraceContextHttpHeaders(request.headers.append, request.headers, span);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (!copyOfInit) {
|
|
62
|
+
copyOfInit = {};
|
|
63
|
+
}
|
|
64
|
+
copyOfInit.headers = new Headers();
|
|
65
|
+
addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
tryCaptureHttpHeaders(request.headers, span, (k) => httpRequestHeaderKey(k));
|
|
69
|
+
const performanceObserver = observeResourcePerformance({
|
|
70
|
+
// We match on both fetch and XHR here to support polyfills
|
|
71
|
+
resourceMatcher: ({ initiatorType, name }) => (initiatorType === "fetch" || initiatorType === "xmlhttprequest") && name === parseUrl(url).href,
|
|
72
|
+
maxWaitForResourceMillis: vars.maxWaitForResourceTimingsMillis,
|
|
73
|
+
maxToleranceForResourceTimingsMillis: vars.maxToleranceForResourceTimingsMillis,
|
|
74
|
+
onEnd: ({ duration, resource }) => {
|
|
75
|
+
if (resource) {
|
|
76
|
+
addResourceNetworkEvents(span, resource);
|
|
77
|
+
addResourceSize(span, resource);
|
|
78
|
+
}
|
|
79
|
+
// duration is millis we need to convert to nanos
|
|
80
|
+
sendSpan(endSpan(span, undefined, duration * 1000000));
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
performanceObserver.start();
|
|
84
|
+
try {
|
|
85
|
+
const response = await original(input instanceof Request ? request : input, copyOfInit);
|
|
86
|
+
addResponseData(span, response);
|
|
87
|
+
// We use a separate promise here because this needs to happen in parallel to application code consuming the response
|
|
88
|
+
waitForFullResponse(response)
|
|
89
|
+
.then(() => performanceObserver.end())
|
|
90
|
+
.catch((e) => {
|
|
91
|
+
performanceObserver.cancel();
|
|
92
|
+
endSpanOnError(span, e);
|
|
93
|
+
});
|
|
94
|
+
return response;
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
performanceObserver.cancel();
|
|
98
|
+
endSpanOnError(span, e);
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// @ts-expect-error -- WIP
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- WIP
|
|
105
|
+
function addGraphQlProperties(input, init, span) {
|
|
106
|
+
try {
|
|
107
|
+
if (!isGraphQLQuery(input, init))
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
debug("failed to analyze request for GraphQL insights", e, input, init);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- WIP
|
|
115
|
+
function isGraphQLQuery(input, init) {
|
|
116
|
+
/**
|
|
117
|
+
* TODO: Add GraphQL support.
|
|
118
|
+
* GraphQL queries are either POST or GET requests.
|
|
119
|
+
* Identified by either setting Accept=application/graphql-response+json or the url matching a config field for graphql urls.
|
|
120
|
+
* See: https://graphql.github.io/graphql-over-http/draft/
|
|
121
|
+
* GET requests are queries carrying a query search param
|
|
122
|
+
* POST requests are either queries or mutations, See: https://graphql.org/learn/serving-over-http/#post-request-and-body
|
|
123
|
+
* and https://github.com/instana/weasel/blob/main/lib/hooks/Fetch.ts#L201
|
|
124
|
+
*/
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
function tryCaptureHttpHeaders(headers, span, getAttributeKey) {
|
|
128
|
+
try {
|
|
129
|
+
headers.forEach((value, key) => {
|
|
130
|
+
if (vars.headersToCapture.some((rxp) => rxp.test(key))) {
|
|
131
|
+
addAttribute(span.attributes, getAttributeKey(key), value);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (_e) {
|
|
136
|
+
debug("unable to capture http headers due to CORS policy");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function addResponseData(span, response) {
|
|
140
|
+
const status = response.status;
|
|
141
|
+
setSpanStatus(span, status >= 200 && status < 400 ? SPAN_STATUS_UNSET : SPAN_STATUS_ERROR, response.statusText);
|
|
142
|
+
addAttribute(span.attributes, HTTP_RESPONSE_STATUS_CODE, String(status));
|
|
143
|
+
tryCaptureHttpHeaders(response.headers, span, (k) => httpResponseHeaderKey(k));
|
|
144
|
+
}
|
|
145
|
+
function waitForFullResponse(response) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const clonedResponse = response.clone();
|
|
148
|
+
const body = clonedResponse.body;
|
|
149
|
+
if (!body)
|
|
150
|
+
return resolve();
|
|
151
|
+
const reader = body.getReader();
|
|
152
|
+
const read = async () => {
|
|
153
|
+
const { done } = await reader.read();
|
|
154
|
+
if (done)
|
|
155
|
+
return resolve();
|
|
156
|
+
return read();
|
|
157
|
+
};
|
|
158
|
+
return read();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function endSpanOnError(span, error) {
|
|
162
|
+
recordException(span, error);
|
|
163
|
+
sendSpan(endSpan(span, errorToSpanStatus(error), undefined));
|
|
164
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { addAttribute, addSpanEvent } from "../../utils/otel";
|
|
2
|
+
import { domHRTimestampToNanos, hasKey, PerformanceTimingNames } from "../../utils";
|
|
3
|
+
import { HTTP_RESPONSE_BODY_SIZE } from "../../semantic-conventions";
|
|
4
|
+
// SEE: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/http.md?plain=1#L67
|
|
5
|
+
const KNOWN_HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
|
|
6
|
+
export const HTTP_METHOD_OTHER = "_OTHER";
|
|
7
|
+
export function isWellKnownHttpMethod(method) {
|
|
8
|
+
return KNOWN_HTTP_METHODS.includes(method);
|
|
9
|
+
}
|
|
10
|
+
export function addResourceNetworkEvents(span, resource) {
|
|
11
|
+
const ignoreZeros = resource.startTime !== 0;
|
|
12
|
+
addSpanNetworkEvent(span, PerformanceTimingNames.FETCH_START, resource, ignoreZeros);
|
|
13
|
+
addSpanNetworkEvent(span, PerformanceTimingNames.DOMAIN_LOOKUP_START, resource, ignoreZeros);
|
|
14
|
+
addSpanNetworkEvent(span, PerformanceTimingNames.DOMAIN_LOOKUP_END, resource, ignoreZeros);
|
|
15
|
+
addSpanNetworkEvent(span, PerformanceTimingNames.CONNECT_START, resource, ignoreZeros);
|
|
16
|
+
addSpanNetworkEvent(span, PerformanceTimingNames.SECURE_CONNECTION_START, resource, ignoreZeros);
|
|
17
|
+
addSpanNetworkEvent(span, PerformanceTimingNames.CONNECT_END, resource, ignoreZeros);
|
|
18
|
+
addSpanNetworkEvent(span, PerformanceTimingNames.REQUEST_START, resource, ignoreZeros);
|
|
19
|
+
addSpanNetworkEvent(span, PerformanceTimingNames.RESPONSE_START, resource, ignoreZeros);
|
|
20
|
+
addSpanNetworkEvent(span, PerformanceTimingNames.RESPONSE_END, resource, ignoreZeros);
|
|
21
|
+
}
|
|
22
|
+
function addSpanNetworkEvent(span, propertyName, resource, ignoreZeros = true) {
|
|
23
|
+
if (!hasKey(resource, propertyName) ||
|
|
24
|
+
typeof resource[propertyName] !== "number" ||
|
|
25
|
+
(ignoreZeros && resource[propertyName] === 0)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
addSpanEvent(span, propertyName, domHRTimestampToNanos(resource[propertyName]));
|
|
29
|
+
}
|
|
30
|
+
export function addResourceSize(span, resource) {
|
|
31
|
+
const encodedLength = resource.encodedBodySize;
|
|
32
|
+
if (encodedLength != undefined) {
|
|
33
|
+
addAttribute(span.attributes, HTTP_RESPONSE_BODY_SIZE, encodedLength);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { addAttribute, getTraceContextForPageLoad } from "../../utils/otel";
|
|
2
|
+
import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES, PAGE_VIEW_CHANGE_STATE, PAGE_VIEW_CHANGE_STATE_VALUES, PAGE_VIEW_TYPE, PAGE_VIEW_TYPE_VALUES, } from "../../semantic-conventions";
|
|
3
|
+
import { doc, NO_VALUE_FALLBACK } from "../../utils";
|
|
4
|
+
import { addCommonAttributes } from "../../attributes";
|
|
5
|
+
import { sendLog } from "../../transport";
|
|
6
|
+
import { vars } from "../../vars";
|
|
7
|
+
function getPageViewMeta(url) {
|
|
8
|
+
if (!url)
|
|
9
|
+
return {};
|
|
10
|
+
return vars.pageViewInstrumentation.generateMetadata?.(url) ?? {};
|
|
11
|
+
}
|
|
12
|
+
export function transmitPageViewEvent(timeUnixNano, url, virtual, replaced) {
|
|
13
|
+
const meta = getPageViewMeta(url);
|
|
14
|
+
const attributes = [];
|
|
15
|
+
addAttribute(attributes, EVENT_NAME, EVENT_NAMES.PAGE_VIEW);
|
|
16
|
+
if (meta.attributes) {
|
|
17
|
+
Object.entries(meta.attributes).forEach(([key, value]) => addAttribute(attributes, key, value));
|
|
18
|
+
}
|
|
19
|
+
addCommonAttributes(attributes, { url });
|
|
20
|
+
const bodyAttributes = [];
|
|
21
|
+
addAttribute(bodyAttributes, "title", meta.title ?? doc?.title ?? NO_VALUE_FALLBACK);
|
|
22
|
+
if (doc?.referrer) {
|
|
23
|
+
addAttribute(bodyAttributes, "referrer", doc.referrer);
|
|
24
|
+
}
|
|
25
|
+
addAttribute(bodyAttributes, PAGE_VIEW_TYPE, virtual ? PAGE_VIEW_TYPE_VALUES.VIRTUAL : PAGE_VIEW_TYPE_VALUES.INITIAL);
|
|
26
|
+
addAttribute(bodyAttributes, PAGE_VIEW_CHANGE_STATE, replaced ? PAGE_VIEW_CHANGE_STATE_VALUES.REPLACE : PAGE_VIEW_CHANGE_STATE_VALUES.PUSH);
|
|
27
|
+
const log = {
|
|
28
|
+
timeUnixNano: timeUnixNano,
|
|
29
|
+
attributes: attributes,
|
|
30
|
+
severityNumber: LOG_SEVERITIES.INFO,
|
|
31
|
+
severityText: "INFO",
|
|
32
|
+
body: {
|
|
33
|
+
kvlistValue: {
|
|
34
|
+
values: bodyAttributes,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const traceContext = getTraceContextForPageLoad();
|
|
39
|
+
if (traceContext) {
|
|
40
|
+
log.traceId = traceContext.traceId;
|
|
41
|
+
log.spanId = traceContext.spanId;
|
|
42
|
+
}
|
|
43
|
+
sendLog(log);
|
|
44
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { addEventListener, debug, doc, win, roundToTwoDecimals, error, toNanosTs, getTimeOrigin } from "../../utils";
|
|
2
|
+
import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES } from "../../semantic-conventions";
|
|
3
|
+
import { sendLog } from "../../transport";
|
|
4
|
+
import { getTraceContextForPageLoad, addAttribute } from "../../utils/otel";
|
|
5
|
+
import { addCommonAttributes } from "../../attributes";
|
|
6
|
+
import { transmitPageViewEvent } from "./event";
|
|
7
|
+
/**
|
|
8
|
+
* Tracks page loads as per this OTel spec:
|
|
9
|
+
* https://github.com/open-telemetry/semantic-conventions/pull/1910/files
|
|
10
|
+
*
|
|
11
|
+
* Notable difference: The full URL is transmitted as a signal attribute.
|
|
12
|
+
*/
|
|
13
|
+
export function startPageLoadInstrumentation() {
|
|
14
|
+
try {
|
|
15
|
+
transmitPageViewEvent(getStartTimeUnixNanos(), win?.location.href ? new URL(win?.location.href) : undefined);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
error("Failed to transmit initial page view event", e);
|
|
19
|
+
}
|
|
20
|
+
if (doc?.readyState === "complete") {
|
|
21
|
+
return onLoaded();
|
|
22
|
+
}
|
|
23
|
+
if (win) {
|
|
24
|
+
addEventListener(win, "load", function () {
|
|
25
|
+
// we want to get timing data for loadEventEnd,
|
|
26
|
+
// so asynchronously process this
|
|
27
|
+
setTimeout(onLoaded, 0);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* See https://github.com/open-telemetry/semantic-conventions/pull/1919
|
|
33
|
+
*/
|
|
34
|
+
function onLoaded() {
|
|
35
|
+
const nt = win?.performance.getEntriesByType("navigation")[0];
|
|
36
|
+
if (!nt) {
|
|
37
|
+
debug("Navigation timings not available. Cannot emit navigation timing log");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const attributes = [];
|
|
41
|
+
addAttribute(attributes, EVENT_NAME, EVENT_NAMES.NAVIGATION_TIMING);
|
|
42
|
+
const bodyAttributes = [];
|
|
43
|
+
addAttribute(bodyAttributes, "name", nt.name);
|
|
44
|
+
addNavigationTiming(bodyAttributes, nt, "responseStatus");
|
|
45
|
+
addNavigationTiming(bodyAttributes, nt, "fetchStart");
|
|
46
|
+
addNavigationTiming(bodyAttributes, nt, "requestStart");
|
|
47
|
+
addNavigationTiming(bodyAttributes, nt, "responseStart");
|
|
48
|
+
addNavigationTiming(bodyAttributes, nt, "domInteractive");
|
|
49
|
+
addNavigationTiming(bodyAttributes, nt, "domContentLoadedEventEnd");
|
|
50
|
+
addNavigationTiming(bodyAttributes, nt, "domComplete");
|
|
51
|
+
addNavigationTiming(bodyAttributes, nt, "loadEventEnd");
|
|
52
|
+
addNavigationTiming(bodyAttributes, nt, "transferSize");
|
|
53
|
+
addNavigationTiming(bodyAttributes, nt, "encodedBodySize");
|
|
54
|
+
addNavigationTiming(bodyAttributes, nt, "decodedBodySize");
|
|
55
|
+
const log = {
|
|
56
|
+
timeUnixNano: getStartTimeUnixNanos(),
|
|
57
|
+
attributes: attributes,
|
|
58
|
+
severityNumber: LOG_SEVERITIES.INFO,
|
|
59
|
+
severityText: "INFO",
|
|
60
|
+
body: {
|
|
61
|
+
kvlistValue: {
|
|
62
|
+
values: bodyAttributes,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
addCommonAttributes(log.attributes);
|
|
67
|
+
const traceContext = getTraceContextForPageLoad();
|
|
68
|
+
if (traceContext) {
|
|
69
|
+
log.traceId = traceContext.traceId;
|
|
70
|
+
log.spanId = traceContext.spanId;
|
|
71
|
+
}
|
|
72
|
+
sendLog(log);
|
|
73
|
+
}
|
|
74
|
+
function addNavigationTiming(attributes, nt, field) {
|
|
75
|
+
// @ts-expect-error index access not recognized by TS, but this makes the code more reusable
|
|
76
|
+
const value = nt[field];
|
|
77
|
+
if (typeof value === "number" && !isNaN(value)) {
|
|
78
|
+
addAttribute(attributes, field, Number.isInteger(value) ? value : roundToTwoDecimals(value));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function getStartTimeUnixNanos() {
|
|
82
|
+
return toNanosTs(Math.round(getTimeOrigin()));
|
|
83
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { debug, nowNanos, win, wrap } from "../../utils";
|
|
2
|
+
import { vars } from "../../vars";
|
|
3
|
+
import { transmitPageViewEvent } from "./event";
|
|
4
|
+
let currentLocation = {};
|
|
5
|
+
let shouldIncludeHashChanges = false;
|
|
6
|
+
let shouldIncludeSearchChanges = false;
|
|
7
|
+
/**
|
|
8
|
+
* Tracks page transitions (virtual page views) as per this OTel spec:
|
|
9
|
+
* https://github.com/open-telemetry/semantic-conventions/pull/1910/files
|
|
10
|
+
*
|
|
11
|
+
* Notable difference: The full URL is transmitted as a signal attribute.
|
|
12
|
+
*/
|
|
13
|
+
export function startPageTransitionInstrumentation() {
|
|
14
|
+
if (!win || !win.history) {
|
|
15
|
+
debug("Browser does not support history API, skipping instrumentation");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!vars.pageViewInstrumentation.trackVirtualPageViews) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
shouldIncludeSearchChanges = vars.pageViewInstrumentation.includeParts?.includes("SEARCH") ?? false;
|
|
22
|
+
shouldIncludeHashChanges = vars.pageViewInstrumentation.includeParts?.includes("HASH") ?? false;
|
|
23
|
+
/**
|
|
24
|
+
* Not wrapping history.go, history.backward and history.forward here, because their call signatures don't receive
|
|
25
|
+
* url information. For these cases we can use the popstate and hashchange events.
|
|
26
|
+
*/
|
|
27
|
+
wrap(win.history, "replaceState", (original) => function (state, unused, url) {
|
|
28
|
+
onUrlChange(url ? String(url) : undefined, true);
|
|
29
|
+
return original.apply(this, [state, unused, url]);
|
|
30
|
+
});
|
|
31
|
+
wrap(win.history, "pushState", (original) => function (state, unused, url) {
|
|
32
|
+
onUrlChange(url ? String(url) : undefined);
|
|
33
|
+
return original.apply(this, [state, unused, url]);
|
|
34
|
+
});
|
|
35
|
+
win.addEventListener("hashchange", onHashChange);
|
|
36
|
+
win.addEventListener("popstate", onPopState);
|
|
37
|
+
try {
|
|
38
|
+
updateCurrentLocation(new URL(win.location.href));
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
function onPopState() {
|
|
43
|
+
// popState event is fired after the location entry has been updated, so the location should already contain the new url.
|
|
44
|
+
onUrlChange(win?.location.href);
|
|
45
|
+
}
|
|
46
|
+
function onHashChange(event) {
|
|
47
|
+
onUrlChange(event.newURL);
|
|
48
|
+
}
|
|
49
|
+
function onUrlChange(url, replaced) {
|
|
50
|
+
if (!url)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
const parsedUrl = new URL(url, win?.location.href);
|
|
54
|
+
if (isLocationChange(parsedUrl)) {
|
|
55
|
+
updateCurrentLocation(parsedUrl);
|
|
56
|
+
transmitPageViewEvent(nowNanos(), parsedUrl, true, Boolean(replaced));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
debug("Failed to handle url change", e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function updateCurrentLocation(url) {
|
|
64
|
+
currentLocation.path = url.pathname;
|
|
65
|
+
currentLocation.search = url.search;
|
|
66
|
+
currentLocation.hash = url.hash;
|
|
67
|
+
}
|
|
68
|
+
function isLocationChange(newUrl) {
|
|
69
|
+
return (newUrl.pathname !== currentLocation.path ||
|
|
70
|
+
(shouldIncludeSearchChanges && newUrl.search !== currentLocation.search) ||
|
|
71
|
+
(shouldIncludeHashChanges && newUrl.hash !== currentLocation.hash));
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { onLCP, onINP, onCLS } from "web-vitals";
|
|
2
|
+
import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES } from "../semantic-conventions";
|
|
3
|
+
import { nowNanos, roundToTwoDecimals } from "../utils";
|
|
4
|
+
import { sendLog } from "../transport";
|
|
5
|
+
import { addAttribute } from "../utils/otel";
|
|
6
|
+
import { addCommonAttributes } from "../attributes";
|
|
7
|
+
export function startWebVitalsInstrumentation() {
|
|
8
|
+
onLCP(onWebVital);
|
|
9
|
+
onINP(onWebVital);
|
|
10
|
+
onCLS(onWebVital);
|
|
11
|
+
}
|
|
12
|
+
function onWebVital(metric) {
|
|
13
|
+
const attributes = [];
|
|
14
|
+
addAttribute(attributes, EVENT_NAME, EVENT_NAMES.WEB_VITAL);
|
|
15
|
+
const bodyAttributes = [];
|
|
16
|
+
addAttribute(bodyAttributes, "name", metric.name);
|
|
17
|
+
addAttribute(bodyAttributes, "value", roundToTwoDecimals(metric.value));
|
|
18
|
+
addAttribute(bodyAttributes, "delta", roundToTwoDecimals(metric.delta));
|
|
19
|
+
const log = {
|
|
20
|
+
timeUnixNano: nowNanos(),
|
|
21
|
+
attributes: attributes,
|
|
22
|
+
severityNumber: LOG_SEVERITIES.INFO,
|
|
23
|
+
severityText: "INFO",
|
|
24
|
+
body: {
|
|
25
|
+
kvlistValue: {
|
|
26
|
+
values: bodyAttributes,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
addCommonAttributes(log.attributes);
|
|
31
|
+
sendLog(log);
|
|
32
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Resource Attribute Keys
|
|
2
|
+
export const SERVICE_NAME = "service.name";
|
|
3
|
+
export const SERVICE_VERSION = "service.version";
|
|
4
|
+
export const DEPLOYMENT_ENVIRONMENT_NAME = "deployment.environment.name";
|
|
5
|
+
export const DEPLOYMENT_NAME = "deployment.name";
|
|
6
|
+
export const DEPLOYMENT_ID = "deployment.id";
|
|
7
|
+
// Misc Signal Attribute Keys
|
|
8
|
+
export const EVENT_NAME = "event.name";
|
|
9
|
+
export const EVENT_TITLE = "dash0.event.title";
|
|
10
|
+
export const PAGE_LOAD_ID = "page.load.id";
|
|
11
|
+
export const SESSION_ID = "session.id";
|
|
12
|
+
export const USER_AGENT = "user_agent.original";
|
|
13
|
+
export const BROWSER_TAB_ID = "browser.tab.id";
|
|
14
|
+
export const WINDOW_WIDTH = "browser.window.width";
|
|
15
|
+
export const WINDOW_HEIGHT = "browser.window.height";
|
|
16
|
+
export const NETWORK_CONNECTION_TYPE = "network.connection.subtype";
|
|
17
|
+
export const EXCEPTION_COMPONENT_STACK = "exception.component_stack";
|
|
18
|
+
// User Attribute Keys
|
|
19
|
+
export const USER_ID = "user.id";
|
|
20
|
+
export const USER_NAME = "user.name";
|
|
21
|
+
export const USER_FULL_NAME = "user.full_name";
|
|
22
|
+
export const USER_EMAIL = "user.email";
|
|
23
|
+
export const USER_HASH = "user.hash";
|
|
24
|
+
export const USER_ROLES = "user.roles";
|
|
25
|
+
// Exception Attribute Keys
|
|
26
|
+
export const EXCEPTION_MESSAGE = "exception.message";
|
|
27
|
+
export const EXCEPTION_TYPE = "exception.type";
|
|
28
|
+
export const EXCEPTION_STACKTRACE = "exception.stacktrace";
|
|
29
|
+
// URL Attribute Keys
|
|
30
|
+
export const URL_DOMAIN = "url.domain";
|
|
31
|
+
export const URL_FRAGMENT = "url.fragment";
|
|
32
|
+
export const URL_FULL = "url.full";
|
|
33
|
+
export const URL_PATH = "url.path";
|
|
34
|
+
export const URL_QUERY = "url.query";
|
|
35
|
+
export const URL_SCHEME = "url.scheme";
|
|
36
|
+
// Http Attribute Keys
|
|
37
|
+
export const HTTP_REQUEST_METHOD = "http.request.method";
|
|
38
|
+
export const HTTP_REQUEST_METHOD_ORIGINAL = "http.request.method_original";
|
|
39
|
+
export const HTTP_REQUEST_HEADER = "http.request.header";
|
|
40
|
+
export const HTTP_RESPONSE_STATUS_CODE = "http.response.status_code";
|
|
41
|
+
export const HTTP_RESPONSE_HEADER = "http.response.header";
|
|
42
|
+
export const HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
|
|
43
|
+
// Event Names
|
|
44
|
+
export const EVENT_NAMES = {
|
|
45
|
+
PAGE_VIEW: "browser.page_view",
|
|
46
|
+
NAVIGATION_TIMING: "browser.navigation_timing",
|
|
47
|
+
WEB_VITAL: "browser.web_vital",
|
|
48
|
+
ERROR: "browser.error",
|
|
49
|
+
};
|
|
50
|
+
export const SPAN_EVENT_NAME_EXCEPTION = "exception";
|
|
51
|
+
// Log Severities
|
|
52
|
+
export const LOG_SEVERITIES = {
|
|
53
|
+
UNSPECIFIED: 0,
|
|
54
|
+
TRACE: 1,
|
|
55
|
+
DEBUG: 5,
|
|
56
|
+
INFO: 9,
|
|
57
|
+
WARN: 13,
|
|
58
|
+
ERROR: 17,
|
|
59
|
+
FATAL: 21,
|
|
60
|
+
};
|
|
61
|
+
// Page View Event Attributes
|
|
62
|
+
// SEE: https://github.com/open-telemetry/semantic-conventions/pull/1910/files
|
|
63
|
+
export const PAGE_VIEW_TYPE = "type";
|
|
64
|
+
export const PAGE_VIEW_TYPE_VALUES = {
|
|
65
|
+
INITIAL: 0,
|
|
66
|
+
VIRTUAL: 1,
|
|
67
|
+
};
|
|
68
|
+
export const PAGE_VIEW_CHANGE_STATE = "change_state";
|
|
69
|
+
export const PAGE_VIEW_CHANGE_STATE_VALUES = {
|
|
70
|
+
PUSH: "pushState",
|
|
71
|
+
REPLACE: "replaceState",
|
|
72
|
+
};
|
|
73
|
+
// Span Status
|
|
74
|
+
export const SPAN_STATUS_UNSET = 0;
|
|
75
|
+
export const SPAN_STATUS_OK = 1; // This is here for completion, status ok is reserved for use by application developers
|
|
76
|
+
export const SPAN_STATUS_ERROR = 2;
|
|
77
|
+
// Span Kind
|
|
78
|
+
// See: https://github.com/open-telemetry/opentelemetry-proto/blob/ac3242b03157295e4ee9e616af53b81517b06559/opentelemetry/proto/trace/v1/trace.proto#L143-L169
|
|
79
|
+
export const SPAN_KIND_CLIENT = 3;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { onLastChance } from "../utils/on-last-chance";
|
|
2
|
+
import { doc } from "../utils";
|
|
3
|
+
import { setTimeout } from "../utils/timers";
|
|
4
|
+
const SCHEDULE_DELAY_MILLIS = 1000;
|
|
5
|
+
const MAX_QUEUE_SIZE = 15;
|
|
6
|
+
export function newBatcher(sendInternal) {
|
|
7
|
+
const queuedItems = [];
|
|
8
|
+
let pendingFlushTimeout;
|
|
9
|
+
// We attempt batching of messages to be more efficient on the client, network and
|
|
10
|
+
// server-side. While the connection is either a persistent HTTP 2 connection or
|
|
11
|
+
// an HTTP 1.1 connection with keep-alive, there is still some overhead involved
|
|
12
|
+
// in having many small messages.
|
|
13
|
+
//
|
|
14
|
+
// For this reason we attempt batching. When batching we must be careful to
|
|
15
|
+
// force a transmission when the document is unloaded.
|
|
16
|
+
onLastChance(flush);
|
|
17
|
+
return {
|
|
18
|
+
send(item) {
|
|
19
|
+
if (isWindowHidden()) {
|
|
20
|
+
// We cannot guarantee that we will ever get time to transmit data in a batched
|
|
21
|
+
// format when the window is hidden, as this might occur while the document is
|
|
22
|
+
// being unloaded. Immediately force a transmission in these cases.
|
|
23
|
+
sendInternal([item]);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
queuedItems.push(item);
|
|
27
|
+
if (queuedItems.length >= MAX_QUEUE_SIZE) {
|
|
28
|
+
flush();
|
|
29
|
+
}
|
|
30
|
+
else if (pendingFlushTimeout == null) {
|
|
31
|
+
pendingFlushTimeout = setTimeout(flush, SCHEDULE_DELAY_MILLIS);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
function flush() {
|
|
36
|
+
if (pendingFlushTimeout != null) {
|
|
37
|
+
clearTimeout(pendingFlushTimeout);
|
|
38
|
+
pendingFlushTimeout = null;
|
|
39
|
+
}
|
|
40
|
+
if (queuedItems.length > 0) {
|
|
41
|
+
sendInternal(queuedItems.slice());
|
|
42
|
+
queuedItems.length = 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isWindowHidden() {
|
|
47
|
+
return doc?.visibilityState !== "visible";
|
|
48
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { vars } from "../vars";
|
|
2
|
+
import { noop, warn, fetch } from "../utils";
|
|
3
|
+
const BEACON_BODY_SIZE_LIMIT = 60000;
|
|
4
|
+
export async function send(path, body) {
|
|
5
|
+
const jsonString = JSON.stringify(body);
|
|
6
|
+
let requestBody = jsonString;
|
|
7
|
+
let byteLength = jsonString.length;
|
|
8
|
+
let isCompressed = false;
|
|
9
|
+
// Try to compress if supported
|
|
10
|
+
if (typeof CompressionStream !== "undefined") {
|
|
11
|
+
requestBody = await compressWithGzip(jsonString);
|
|
12
|
+
byteLength = requestBody.byteLength;
|
|
13
|
+
isCompressed = true;
|
|
14
|
+
}
|
|
15
|
+
// Send to all endpoints in parallel
|
|
16
|
+
await Promise.all(vars.endpoints.map(async (endpoint) => {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(endpoint["url"]);
|
|
19
|
+
url.pathname = url.pathname + (url.pathname.endsWith("/") ? path.substring(1) : path);
|
|
20
|
+
const headers = {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
Authorization: `Bearer ${endpoint["authToken"]}`,
|
|
23
|
+
};
|
|
24
|
+
if (endpoint.dataset) {
|
|
25
|
+
headers["Dash0-Dataset"] = endpoint["dataset"];
|
|
26
|
+
}
|
|
27
|
+
// Try to compress if supported
|
|
28
|
+
if (isCompressed) {
|
|
29
|
+
headers["Content-Encoding"] = "gzip";
|
|
30
|
+
}
|
|
31
|
+
if (!fetch) {
|
|
32
|
+
warn("Unable to send telemetry, fetch is not defined");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers,
|
|
38
|
+
body: requestBody,
|
|
39
|
+
// The keepalive flag is related to the window.sendBeacon API. This in turn has size limitations.
|
|
40
|
+
keepalive: byteLength <= BEACON_BODY_SIZE_LIMIT,
|
|
41
|
+
});
|
|
42
|
+
// read the body so the connection can be closed
|
|
43
|
+
response.text().catch(noop);
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
warn(`Failed to send telemetry to ${url}: ${response.status} ${response.statusText}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
warn(`Error sending telemetry to ${endpoint.url}${path}:`, error);
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
async function compressWithGzip(data) {
|
|
54
|
+
const blob = new Blob([data]);
|
|
55
|
+
const stream = blob.stream();
|
|
56
|
+
const compressedStream = stream.pipeThrough(new CompressionStream("gzip"));
|
|
57
|
+
return new Response(compressedStream).arrayBuffer();
|
|
58
|
+
}
|