@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,96 @@
|
|
|
1
|
+
import { addEventListener, debug, doc, win, roundToTwoDecimals, error, toNanosTs, getTimeOrigin } from "../../utils";
|
|
2
|
+
import { KeyValue, LogRecord } from "../../../types/otlp";
|
|
3
|
+
import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES } from "../../semantic-conventions";
|
|
4
|
+
import { sendLog } from "../../transport";
|
|
5
|
+
import { getTraceContextForPageLoad, addAttribute } from "../../utils/otel";
|
|
6
|
+
import { addCommonAttributes } from "../../attributes";
|
|
7
|
+
import { transmitPageViewEvent } from "./event";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tracks page loads as per this OTel spec:
|
|
11
|
+
* https://github.com/open-telemetry/semantic-conventions/pull/1910/files
|
|
12
|
+
*
|
|
13
|
+
* Notable difference: The full URL is transmitted as a signal attribute.
|
|
14
|
+
*/
|
|
15
|
+
export function startPageLoadInstrumentation() {
|
|
16
|
+
try {
|
|
17
|
+
transmitPageViewEvent(getStartTimeUnixNanos(), win?.location.href ? new URL(win?.location.href) : undefined);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
error("Failed to transmit initial page view event", e);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (doc?.readyState === "complete") {
|
|
23
|
+
return onLoaded();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (win) {
|
|
27
|
+
addEventListener(win, "load", function () {
|
|
28
|
+
// we want to get timing data for loadEventEnd,
|
|
29
|
+
// so asynchronously process this
|
|
30
|
+
setTimeout(onLoaded, 0);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* See https://github.com/open-telemetry/semantic-conventions/pull/1919
|
|
37
|
+
*/
|
|
38
|
+
function onLoaded() {
|
|
39
|
+
const nt = win?.performance.getEntriesByType("navigation")[0];
|
|
40
|
+
if (!nt) {
|
|
41
|
+
debug("Navigation timings not available. Cannot emit navigation timing log");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const attributes: KeyValue[] = [];
|
|
46
|
+
addAttribute(attributes, EVENT_NAME, EVENT_NAMES.NAVIGATION_TIMING);
|
|
47
|
+
|
|
48
|
+
const bodyAttributes: KeyValue[] = [];
|
|
49
|
+
addAttribute(bodyAttributes, "name", nt.name);
|
|
50
|
+
addNavigationTiming(bodyAttributes, nt, "responseStatus");
|
|
51
|
+
|
|
52
|
+
addNavigationTiming(bodyAttributes, nt, "fetchStart");
|
|
53
|
+
addNavigationTiming(bodyAttributes, nt, "requestStart");
|
|
54
|
+
addNavigationTiming(bodyAttributes, nt, "responseStart");
|
|
55
|
+
addNavigationTiming(bodyAttributes, nt, "domInteractive");
|
|
56
|
+
addNavigationTiming(bodyAttributes, nt, "domContentLoadedEventEnd");
|
|
57
|
+
addNavigationTiming(bodyAttributes, nt, "domComplete");
|
|
58
|
+
addNavigationTiming(bodyAttributes, nt, "loadEventEnd");
|
|
59
|
+
|
|
60
|
+
addNavigationTiming(bodyAttributes, nt, "transferSize");
|
|
61
|
+
addNavigationTiming(bodyAttributes, nt, "encodedBodySize");
|
|
62
|
+
addNavigationTiming(bodyAttributes, nt, "decodedBodySize");
|
|
63
|
+
|
|
64
|
+
const log: LogRecord = {
|
|
65
|
+
timeUnixNano: getStartTimeUnixNanos(),
|
|
66
|
+
attributes: attributes,
|
|
67
|
+
severityNumber: LOG_SEVERITIES.INFO,
|
|
68
|
+
severityText: "INFO",
|
|
69
|
+
body: {
|
|
70
|
+
kvlistValue: {
|
|
71
|
+
values: bodyAttributes,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
addCommonAttributes(log.attributes);
|
|
76
|
+
|
|
77
|
+
const traceContext = getTraceContextForPageLoad();
|
|
78
|
+
if (traceContext) {
|
|
79
|
+
log.traceId = traceContext.traceId;
|
|
80
|
+
log.spanId = traceContext.spanId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
sendLog(log);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function addNavigationTiming(attributes: KeyValue[], nt: PerformanceNavigationTiming, field: string) {
|
|
87
|
+
// @ts-expect-error index access not recognized by TS, but this makes the code more reusable
|
|
88
|
+
const value = nt[field];
|
|
89
|
+
if (typeof value === "number" && !isNaN(value)) {
|
|
90
|
+
addAttribute(attributes, field, Number.isInteger(value) ? value : roundToTwoDecimals(value));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getStartTimeUnixNanos(): string {
|
|
95
|
+
return toNanosTs(Math.round(getTimeOrigin()));
|
|
96
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { debug, nowNanos, win, wrap } from "../../utils";
|
|
2
|
+
import { vars } from "../../vars";
|
|
3
|
+
import { transmitPageViewEvent } from "./event";
|
|
4
|
+
|
|
5
|
+
type LocationState = Partial<{
|
|
6
|
+
path: string;
|
|
7
|
+
search: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
let currentLocation: LocationState = {};
|
|
12
|
+
let shouldIncludeHashChanges = false;
|
|
13
|
+
let shouldIncludeSearchChanges = false;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tracks page transitions (virtual page views) as per this OTel spec:
|
|
17
|
+
* https://github.com/open-telemetry/semantic-conventions/pull/1910/files
|
|
18
|
+
*
|
|
19
|
+
* Notable difference: The full URL is transmitted as a signal attribute.
|
|
20
|
+
*/
|
|
21
|
+
export function startPageTransitionInstrumentation() {
|
|
22
|
+
if (!win || !win.history) {
|
|
23
|
+
debug("Browser does not support history API, skipping instrumentation");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!vars.pageViewInstrumentation.trackVirtualPageViews) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
shouldIncludeSearchChanges = vars.pageViewInstrumentation.includeParts?.includes("SEARCH") ?? false;
|
|
32
|
+
shouldIncludeHashChanges = vars.pageViewInstrumentation.includeParts?.includes("HASH") ?? false;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Not wrapping history.go, history.backward and history.forward here, because their call signatures don't receive
|
|
36
|
+
* url information. For these cases we can use the popstate and hashchange events.
|
|
37
|
+
*/
|
|
38
|
+
wrap(
|
|
39
|
+
win.history,
|
|
40
|
+
"replaceState",
|
|
41
|
+
(original) =>
|
|
42
|
+
function (this: History, state, unused, url) {
|
|
43
|
+
onUrlChange(url ? String(url) : undefined, true);
|
|
44
|
+
return original.apply(this, [state, unused, url]);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
wrap(
|
|
48
|
+
win.history,
|
|
49
|
+
"pushState",
|
|
50
|
+
(original) =>
|
|
51
|
+
function (this: History, state, unused, url) {
|
|
52
|
+
onUrlChange(url ? String(url) : undefined);
|
|
53
|
+
return original.apply(this, [state, unused, url]);
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
win.addEventListener("hashchange", onHashChange);
|
|
58
|
+
win.addEventListener("popstate", onPopState);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
updateCurrentLocation(new URL(win.location.href));
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function onPopState() {
|
|
66
|
+
// popState event is fired after the location entry has been updated, so the location should already contain the new url.
|
|
67
|
+
onUrlChange(win?.location.href);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onHashChange(event: HashChangeEvent) {
|
|
71
|
+
onUrlChange(event.newURL);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function onUrlChange(url?: string, replaced?: boolean) {
|
|
75
|
+
if (!url) return;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const parsedUrl = new URL(url, win?.location.href);
|
|
79
|
+
if (isLocationChange(parsedUrl)) {
|
|
80
|
+
updateCurrentLocation(parsedUrl);
|
|
81
|
+
transmitPageViewEvent(nowNanos(), parsedUrl, true, Boolean(replaced));
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
debug("Failed to handle url change", e);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function updateCurrentLocation(url: URL) {
|
|
89
|
+
currentLocation.path = url.pathname;
|
|
90
|
+
currentLocation.search = url.search;
|
|
91
|
+
currentLocation.hash = url.hash;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isLocationChange(newUrl: URL) {
|
|
95
|
+
return (
|
|
96
|
+
newUrl.pathname !== currentLocation.path ||
|
|
97
|
+
(shouldIncludeSearchChanges && newUrl.search !== currentLocation.search) ||
|
|
98
|
+
(shouldIncludeHashChanges && newUrl.hash !== currentLocation.hash)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { onLCP, onINP, onCLS, Metric } from "web-vitals";
|
|
2
|
+
import { KeyValue, LogRecord } from "../../types/otlp";
|
|
3
|
+
import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES } from "../semantic-conventions";
|
|
4
|
+
import { nowNanos, roundToTwoDecimals } from "../utils";
|
|
5
|
+
import { sendLog } from "../transport";
|
|
6
|
+
import { addAttribute } from "../utils/otel";
|
|
7
|
+
import { addCommonAttributes } from "../attributes";
|
|
8
|
+
|
|
9
|
+
export function startWebVitalsInstrumentation() {
|
|
10
|
+
onLCP(onWebVital);
|
|
11
|
+
onINP(onWebVital);
|
|
12
|
+
onCLS(onWebVital);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function onWebVital(metric: Metric) {
|
|
16
|
+
const attributes: KeyValue[] = [];
|
|
17
|
+
addAttribute(attributes, EVENT_NAME, EVENT_NAMES.WEB_VITAL);
|
|
18
|
+
|
|
19
|
+
const bodyAttributes: KeyValue[] = [];
|
|
20
|
+
addAttribute(bodyAttributes, "name", metric.name);
|
|
21
|
+
addAttribute(bodyAttributes, "value", roundToTwoDecimals(metric.value));
|
|
22
|
+
addAttribute(bodyAttributes, "delta", roundToTwoDecimals(metric.delta));
|
|
23
|
+
|
|
24
|
+
const log: LogRecord = {
|
|
25
|
+
timeUnixNano: nowNanos(),
|
|
26
|
+
attributes: attributes,
|
|
27
|
+
severityNumber: LOG_SEVERITIES.INFO,
|
|
28
|
+
severityText: "INFO",
|
|
29
|
+
body: {
|
|
30
|
+
kvlistValue: {
|
|
31
|
+
values: bodyAttributes,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
addCommonAttributes(log.attributes);
|
|
36
|
+
sendLog(log);
|
|
37
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
|
|
8
|
+
// Misc Signal Attribute Keys
|
|
9
|
+
export const EVENT_NAME = "event.name";
|
|
10
|
+
export const EVENT_TITLE = "dash0.event.title";
|
|
11
|
+
export const PAGE_LOAD_ID = "page.load.id";
|
|
12
|
+
export const SESSION_ID = "session.id";
|
|
13
|
+
export const USER_AGENT = "user_agent.original";
|
|
14
|
+
export const BROWSER_TAB_ID = "browser.tab.id";
|
|
15
|
+
export const WINDOW_WIDTH = "browser.window.width";
|
|
16
|
+
export const WINDOW_HEIGHT = "browser.window.height";
|
|
17
|
+
export const NETWORK_CONNECTION_TYPE = "network.connection.subtype";
|
|
18
|
+
export const EXCEPTION_COMPONENT_STACK = "exception.component_stack";
|
|
19
|
+
|
|
20
|
+
// User Attribute Keys
|
|
21
|
+
export const USER_ID = "user.id";
|
|
22
|
+
export const USER_NAME = "user.name";
|
|
23
|
+
export const USER_FULL_NAME = "user.full_name";
|
|
24
|
+
export const USER_EMAIL = "user.email";
|
|
25
|
+
export const USER_HASH = "user.hash";
|
|
26
|
+
export const USER_ROLES = "user.roles";
|
|
27
|
+
|
|
28
|
+
// Exception Attribute Keys
|
|
29
|
+
export const EXCEPTION_MESSAGE = "exception.message";
|
|
30
|
+
export const EXCEPTION_TYPE = "exception.type";
|
|
31
|
+
export const EXCEPTION_STACKTRACE = "exception.stacktrace";
|
|
32
|
+
|
|
33
|
+
// URL Attribute Keys
|
|
34
|
+
export const URL_DOMAIN = "url.domain";
|
|
35
|
+
export const URL_FRAGMENT = "url.fragment";
|
|
36
|
+
export const URL_FULL = "url.full";
|
|
37
|
+
export const URL_PATH = "url.path";
|
|
38
|
+
export const URL_QUERY = "url.query";
|
|
39
|
+
export const URL_SCHEME = "url.scheme";
|
|
40
|
+
|
|
41
|
+
// Http Attribute Keys
|
|
42
|
+
export const HTTP_REQUEST_METHOD = "http.request.method";
|
|
43
|
+
export const HTTP_REQUEST_METHOD_ORIGINAL = "http.request.method_original";
|
|
44
|
+
export const HTTP_REQUEST_HEADER = "http.request.header";
|
|
45
|
+
export const HTTP_RESPONSE_STATUS_CODE = "http.response.status_code";
|
|
46
|
+
export const HTTP_RESPONSE_HEADER = "http.response.header";
|
|
47
|
+
export const HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
|
|
48
|
+
|
|
49
|
+
// Event Names
|
|
50
|
+
export const EVENT_NAMES = {
|
|
51
|
+
PAGE_VIEW: "browser.page_view",
|
|
52
|
+
NAVIGATION_TIMING: "browser.navigation_timing",
|
|
53
|
+
WEB_VITAL: "browser.web_vital",
|
|
54
|
+
ERROR: "browser.error",
|
|
55
|
+
};
|
|
56
|
+
export const SPAN_EVENT_NAME_EXCEPTION = "exception";
|
|
57
|
+
|
|
58
|
+
// Log Severities
|
|
59
|
+
export const LOG_SEVERITIES = {
|
|
60
|
+
UNSPECIFIED: 0,
|
|
61
|
+
TRACE: 1,
|
|
62
|
+
DEBUG: 5,
|
|
63
|
+
INFO: 9,
|
|
64
|
+
WARN: 13,
|
|
65
|
+
ERROR: 17,
|
|
66
|
+
FATAL: 21,
|
|
67
|
+
};
|
|
68
|
+
export type LOG_SEVERITY_TEXT = keyof typeof LOG_SEVERITIES;
|
|
69
|
+
export type LOG_SEVERITY_NUMBER = (typeof LOG_SEVERITIES)[LOG_SEVERITY_TEXT];
|
|
70
|
+
|
|
71
|
+
// Page View Event Attributes
|
|
72
|
+
// SEE: https://github.com/open-telemetry/semantic-conventions/pull/1910/files
|
|
73
|
+
export const PAGE_VIEW_TYPE = "type";
|
|
74
|
+
export const PAGE_VIEW_TYPE_VALUES = {
|
|
75
|
+
INITIAL: 0,
|
|
76
|
+
VIRTUAL: 1,
|
|
77
|
+
};
|
|
78
|
+
export const PAGE_VIEW_CHANGE_STATE = "change_state";
|
|
79
|
+
export const PAGE_VIEW_CHANGE_STATE_VALUES = {
|
|
80
|
+
PUSH: "pushState",
|
|
81
|
+
REPLACE: "replaceState",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Span Status
|
|
85
|
+
export const SPAN_STATUS_UNSET = 0;
|
|
86
|
+
export const SPAN_STATUS_OK = 1; // This is here for completion, status ok is reserved for use by application developers
|
|
87
|
+
export const SPAN_STATUS_ERROR = 2;
|
|
88
|
+
|
|
89
|
+
// Span Kind
|
|
90
|
+
// See: https://github.com/open-telemetry/opentelemetry-proto/blob/ac3242b03157295e4ee9e616af53b81517b06559/opentelemetry/proto/trace/v1/trace.proto#L143-L169
|
|
91
|
+
export const SPAN_KIND_CLIENT = 3;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { onLastChance } from "../utils/on-last-chance";
|
|
2
|
+
import { doc } from "../utils";
|
|
3
|
+
import { setTimeout } from "../utils/timers";
|
|
4
|
+
|
|
5
|
+
const SCHEDULE_DELAY_MILLIS = 1000;
|
|
6
|
+
const MAX_QUEUE_SIZE = 15;
|
|
7
|
+
|
|
8
|
+
export type Batcher<T> = {
|
|
9
|
+
send(item: T): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function newBatcher<T>(sendInternal: (items: T[]) => void): Batcher<T> {
|
|
13
|
+
const queuedItems: T[] = [];
|
|
14
|
+
let pendingFlushTimeout: ReturnType<typeof setTimeout> | null;
|
|
15
|
+
|
|
16
|
+
// We attempt batching of messages to be more efficient on the client, network and
|
|
17
|
+
// server-side. While the connection is either a persistent HTTP 2 connection or
|
|
18
|
+
// an HTTP 1.1 connection with keep-alive, there is still some overhead involved
|
|
19
|
+
// in having many small messages.
|
|
20
|
+
//
|
|
21
|
+
// For this reason we attempt batching. When batching we must be careful to
|
|
22
|
+
// force a transmission when the document is unloaded.
|
|
23
|
+
onLastChance(flush);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
send(item: T): void {
|
|
27
|
+
if (isWindowHidden()) {
|
|
28
|
+
// We cannot guarantee that we will ever get time to transmit data in a batched
|
|
29
|
+
// format when the window is hidden, as this might occur while the document is
|
|
30
|
+
// being unloaded. Immediately force a transmission in these cases.
|
|
31
|
+
sendInternal([item]);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
queuedItems.push(item);
|
|
36
|
+
if (queuedItems.length >= MAX_QUEUE_SIZE) {
|
|
37
|
+
flush();
|
|
38
|
+
} else if (pendingFlushTimeout == null) {
|
|
39
|
+
pendingFlushTimeout = setTimeout(flush, SCHEDULE_DELAY_MILLIS);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function flush() {
|
|
45
|
+
if (pendingFlushTimeout != null) {
|
|
46
|
+
clearTimeout(pendingFlushTimeout);
|
|
47
|
+
pendingFlushTimeout = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (queuedItems.length > 0) {
|
|
51
|
+
sendInternal(queuedItems.slice());
|
|
52
|
+
queuedItems.length = 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isWindowHidden() {
|
|
58
|
+
return doc?.visibilityState !== "visible";
|
|
59
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { vars } from "../vars";
|
|
2
|
+
import { noop, warn, fetch } from "../utils";
|
|
3
|
+
|
|
4
|
+
const BEACON_BODY_SIZE_LIMIT = 60000;
|
|
5
|
+
|
|
6
|
+
export async function send(path: string, body: unknown): Promise<void> {
|
|
7
|
+
const jsonString = JSON.stringify(body);
|
|
8
|
+
let requestBody: ArrayBuffer | string = jsonString;
|
|
9
|
+
let byteLength = jsonString.length;
|
|
10
|
+
let isCompressed = false;
|
|
11
|
+
|
|
12
|
+
// Try to compress if supported
|
|
13
|
+
if (typeof CompressionStream !== "undefined") {
|
|
14
|
+
requestBody = await compressWithGzip(jsonString);
|
|
15
|
+
byteLength = requestBody.byteLength;
|
|
16
|
+
isCompressed = true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Send to all endpoints in parallel
|
|
20
|
+
await Promise.all(
|
|
21
|
+
vars.endpoints.map(async (endpoint) => {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(endpoint["url"]);
|
|
24
|
+
url.pathname = url.pathname + (url.pathname.endsWith("/") ? path.substring(1) : path);
|
|
25
|
+
|
|
26
|
+
const headers: HeadersInit = {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
Authorization: `Bearer ${endpoint["authToken"]}`,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (endpoint.dataset) {
|
|
32
|
+
headers["Dash0-Dataset"] = endpoint["dataset"];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Try to compress if supported
|
|
36
|
+
if (isCompressed) {
|
|
37
|
+
headers["Content-Encoding"] = "gzip";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!fetch) {
|
|
41
|
+
warn("Unable to send telemetry, fetch is not defined");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers,
|
|
48
|
+
body: requestBody,
|
|
49
|
+
|
|
50
|
+
// The keepalive flag is related to the window.sendBeacon API. This in turn has size limitations.
|
|
51
|
+
keepalive: byteLength <= BEACON_BODY_SIZE_LIMIT,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// read the body so the connection can be closed
|
|
55
|
+
response.text().catch(noop);
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
warn(`Failed to send telemetry to ${url}: ${response.status} ${response.statusText}`);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
warn(`Error sending telemetry to ${endpoint.url}${path}:`, error);
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function compressWithGzip(data: string): Promise<ArrayBuffer> {
|
|
68
|
+
const blob = new Blob([data]);
|
|
69
|
+
const stream = blob.stream();
|
|
70
|
+
const compressedStream = stream.pipeThrough(new CompressionStream("gzip"));
|
|
71
|
+
return new Response(compressedStream).arrayBuffer();
|
|
72
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ExportLogsServiceRequest, ExportTraceServiceRequest, LogRecord, Span } from "../../types/otlp";
|
|
2
|
+
import { newBatcher } from "./batcher";
|
|
3
|
+
import { send } from "./fetch";
|
|
4
|
+
import { vars } from "../vars";
|
|
5
|
+
import { createRateLimiter } from "../utils/rate-limit";
|
|
6
|
+
import { debug, error } from "../utils";
|
|
7
|
+
|
|
8
|
+
const logBatcher = newBatcher<LogRecord>(sendLogs);
|
|
9
|
+
const spanBatcher = newBatcher<Span>(sendSpans);
|
|
10
|
+
|
|
11
|
+
let rateLimiter: (() => boolean) | undefined;
|
|
12
|
+
|
|
13
|
+
function isRateLimited() {
|
|
14
|
+
if (!rateLimiter) {
|
|
15
|
+
rateLimiter = createRateLimiter({
|
|
16
|
+
maxCalls: 8096,
|
|
17
|
+
maxCallsPerTenMinutes: 4096,
|
|
18
|
+
maxCallsPerTenSeconds: 128,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return rateLimiter();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sendLog(log: LogRecord): void {
|
|
26
|
+
if (isRateLimited()) {
|
|
27
|
+
debug("Transport rate limit. Will not send item.", log);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
logBatcher.send(log);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sendLogs(logs: LogRecord[]): void {
|
|
35
|
+
send("/v1/logs", {
|
|
36
|
+
resourceLogs: [
|
|
37
|
+
{
|
|
38
|
+
resource: vars.resource,
|
|
39
|
+
scopeLogs: [
|
|
40
|
+
{
|
|
41
|
+
scope: vars.scope,
|
|
42
|
+
logRecords: logs,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
} satisfies ExportLogsServiceRequest).catch((err) => {
|
|
48
|
+
error("Failed to transmit logs", err);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sendSpan(span: Span): void {
|
|
53
|
+
if (isRateLimited()) {
|
|
54
|
+
debug("Transport rate limit. Will not send item.", span);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
spanBatcher.send(span);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sendSpans(spans: Span[]): void {
|
|
62
|
+
send("/v1/traces", {
|
|
63
|
+
resourceSpans: [
|
|
64
|
+
{
|
|
65
|
+
resource: vars.resource,
|
|
66
|
+
scopeSpans: [
|
|
67
|
+
{
|
|
68
|
+
scope: vars.scope,
|
|
69
|
+
spans: spans,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
} satisfies ExportTraceServiceRequest).catch((err) => {
|
|
75
|
+
error("Failed to transmit spans", err);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { noop } from "./fn";
|
|
2
|
+
|
|
3
|
+
type Logger = (...args: any[]) => void;
|
|
4
|
+
|
|
5
|
+
export const log: Logger = createLogger("log");
|
|
6
|
+
export const info: Logger = createLogger("info");
|
|
7
|
+
export const warn: Logger = createLogger("warn");
|
|
8
|
+
export const error: Logger = createLogger("error");
|
|
9
|
+
export const debug: Logger = createLogger("debug");
|
|
10
|
+
|
|
11
|
+
function createLogger(method: Extract<keyof Console, "log" | "info" | "warn" | "error" | "debug">): Logger {
|
|
12
|
+
if (typeof console === "undefined" || typeof console.log !== "function" || typeof console.log.apply !== "function") {
|
|
13
|
+
return noop;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (console[method] && typeof console[method].apply === "function") {
|
|
17
|
+
return function () {
|
|
18
|
+
console[method].apply(console, arguments as any);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return function () {
|
|
23
|
+
console.log.apply(console, arguments as any);
|
|
24
|
+
};
|
|
25
|
+
}
|
package/src/utils/fn.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// aliasing globals for improved minification
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-restricted-globals */
|
|
4
|
+
|
|
5
|
+
export type WindowType = typeof window;
|
|
6
|
+
|
|
7
|
+
// Avoid blowing up in an ssr context. It is important to check via typeof here because window might not even be declared when imported in ssr.
|
|
8
|
+
export const win: typeof window | undefined = typeof window !== "undefined" ? window : undefined;
|
|
9
|
+
export const doc: typeof window.document | undefined = win?.document;
|
|
10
|
+
export const nav: typeof navigator | undefined = win?.navigator;
|
|
11
|
+
export const loc: typeof location | undefined = typeof location !== "undefined" ? location : undefined;
|
|
12
|
+
export const perf =
|
|
13
|
+
win?.performance || (win as any)?.webkitPerformance || (win as any)?.msPerformance || (win as any)?.mozPerformance;
|
|
14
|
+
export const encodeURIComponent: ((arg: string) => string) | undefined = win?.encodeURIComponent;
|
|
15
|
+
export const fetch = win?.fetch;
|
|
16
|
+
export const localStorage: Storage | null = (function () {
|
|
17
|
+
try {
|
|
18
|
+
return win?.localStorage ?? null;
|
|
19
|
+
} catch {
|
|
20
|
+
// localStorage access is not permitted in certain security modes, e.g.
|
|
21
|
+
// when cookies are completely disabled in web browsers.
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
export const sessionStorage: Storage | null = (function () {
|
|
26
|
+
try {
|
|
27
|
+
return win?.sessionStorage ?? null;
|
|
28
|
+
} catch {
|
|
29
|
+
// sessionStorage access is not permitted in certain security modes, e.g.
|
|
30
|
+
// when cookies are completely disabled in web browsers.
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Exposed via this module to enable testing.
|
|
37
|
+
*/
|
|
38
|
+
export function sendBeacon(url: string, data: string): boolean {
|
|
39
|
+
return nav?.sendBeacon(url, data) ?? false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* eslint-enable no-restricted-globals */
|
package/src/utils/id.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const SPAN_ID_BYTES = 8;
|
|
2
|
+
export const TRACE_ID_BYTES = 16;
|
|
3
|
+
export const PAGE_LOAD_ID_BYTES = TRACE_ID_BYTES;
|
|
4
|
+
export const SESSION_ID_BYTES = TRACE_ID_BYTES;
|
|
5
|
+
export const TAB_ID_BYTES = SPAN_ID_BYTES;
|
|
6
|
+
|
|
7
|
+
const SHARED_CHAR_CODES_ARRAY = Array(32);
|
|
8
|
+
export function generateUniqueId(byteCount: number): string {
|
|
9
|
+
for (let i = 0; i < byteCount * 2; i++) {
|
|
10
|
+
SHARED_CHAR_CODES_ARRAY[i] = Math.floor(Math.random() * 16) + 48;
|
|
11
|
+
// valid hex characters in the range 48-57 and 97-102
|
|
12
|
+
if (SHARED_CHAR_CODES_ARRAY[i] >= 58) {
|
|
13
|
+
SHARED_CHAR_CODES_ARRAY[i] += 39;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return String.fromCharCode.apply(null, SHARED_CHAR_CODES_ARRAY.slice(0, byteCount * 2));
|
|
17
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { expect, describe, it } from "vitest";
|
|
2
|
+
import { generateUniqueId } from "./id";
|
|
3
|
+
|
|
4
|
+
// Create a wrapper function for testing
|
|
5
|
+
function generateId(length: number): string {
|
|
6
|
+
if (typeof length !== "number") {
|
|
7
|
+
throw new Error("Length must be a number");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (length < 1) {
|
|
11
|
+
throw new Error("Length must be greater than 0");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return generateUniqueId(Math.ceil(length / 2)).substring(0, length);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("generateId", () => {
|
|
18
|
+
it("returns a string of the expected length", () => {
|
|
19
|
+
const id = generateId(10);
|
|
20
|
+
expect(id).toHaveLength(10);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns a unique ID on each call", () => {
|
|
24
|
+
const id1 = generateId(10);
|
|
25
|
+
const id2 = generateId(10);
|
|
26
|
+
expect(id1).not.toBe(id2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("throws an error if length is less than 1", () => {
|
|
30
|
+
expect(() => generateId(0)).toThrow("Length must be greater than 0");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("throws an error if length is not a number", () => {
|
|
34
|
+
expect(() => generateId("abc" as any)).toThrow("Length must be a number");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("handles large lengths without errors", () => {
|
|
38
|
+
const id = generateId(1000);
|
|
39
|
+
expect(id).toHaveLength(1000);
|
|
40
|
+
});
|
|
41
|
+
});
|