@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,88 @@
|
|
|
1
|
+
import { isSupported, getItem, setItem, removeItem, SESSION_ID_BYTES } from "../utils";
|
|
2
|
+
import { debug, generateUniqueId, now } from "../utils";
|
|
3
|
+
import { info, warn } from "../utils";
|
|
4
|
+
const SESSION_STORAGE_KEY = "d0_session";
|
|
5
|
+
const STORAGE_SEPARATOR_KEY = "#";
|
|
6
|
+
const DEFAULT_SESSION_INACTIVITY_TIMEOUT_MILLIS = 1000 * 60 * 60 * 3;
|
|
7
|
+
const DEFAULT_SESSION_TERMINATION_TIMEOUT_MILLIS = 1000 * 60 * 60 * 6;
|
|
8
|
+
const MAX_ALLOWED_SESSION_TIMEOUT_MILLIS = 1000 * 60 * 60 * 24;
|
|
9
|
+
export let sessionId = null;
|
|
10
|
+
export function trackSessions(sessionInactivityTimeoutMillis, sessionTerminationTimeoutMillis) {
|
|
11
|
+
if (!isSupported) {
|
|
12
|
+
debug("Storage API is not available and session tracking is therefore not supported.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!sessionInactivityTimeoutMillis) {
|
|
16
|
+
sessionInactivityTimeoutMillis = DEFAULT_SESSION_INACTIVITY_TIMEOUT_MILLIS;
|
|
17
|
+
}
|
|
18
|
+
if (!sessionTerminationTimeoutMillis) {
|
|
19
|
+
sessionTerminationTimeoutMillis = DEFAULT_SESSION_TERMINATION_TIMEOUT_MILLIS;
|
|
20
|
+
}
|
|
21
|
+
sessionInactivityTimeoutMillis = Math.min(sessionInactivityTimeoutMillis, MAX_ALLOWED_SESSION_TIMEOUT_MILLIS);
|
|
22
|
+
sessionTerminationTimeoutMillis = Math.min(sessionTerminationTimeoutMillis, MAX_ALLOWED_SESSION_TIMEOUT_MILLIS);
|
|
23
|
+
try {
|
|
24
|
+
const storedValue = getItem(SESSION_STORAGE_KEY);
|
|
25
|
+
let session = parseSession(storedValue);
|
|
26
|
+
if (session && !isSessionValid(session, sessionInactivityTimeoutMillis, sessionTerminationTimeoutMillis)) {
|
|
27
|
+
session = null;
|
|
28
|
+
}
|
|
29
|
+
if (session) {
|
|
30
|
+
session.lastActivityTime = now();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
session = {
|
|
34
|
+
id: generateUniqueId(SESSION_ID_BYTES),
|
|
35
|
+
startTime: now(),
|
|
36
|
+
lastActivityTime: now(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
setItem(SESSION_STORAGE_KEY, serializeSession(session));
|
|
40
|
+
sessionId = session.id;
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
warn("Failed to record session information", e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function terminateSession() {
|
|
47
|
+
sessionId = null;
|
|
48
|
+
if (!isSupported) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
removeItem(SESSION_STORAGE_KEY);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
info("Failed to terminate session", e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function parseSession(storedValue) {
|
|
59
|
+
if (!storedValue) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const values = storedValue.split(STORAGE_SEPARATOR_KEY);
|
|
63
|
+
if (values.length < 3) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const id = values[0];
|
|
67
|
+
const startTime = parseInt(values[1], 10);
|
|
68
|
+
const lastActivityTime = parseInt(values[2], 10);
|
|
69
|
+
if (!id || isNaN(startTime) || isNaN(lastActivityTime)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
startTime,
|
|
75
|
+
lastActivityTime,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function serializeSession(session) {
|
|
79
|
+
return session.id + STORAGE_SEPARATOR_KEY + session.startTime + STORAGE_SEPARATOR_KEY + session.lastActivityTime;
|
|
80
|
+
}
|
|
81
|
+
function isSessionValid(session, sessionInactivityTimeoutMillis, sessionTerminationTimeoutMillis) {
|
|
82
|
+
const minAllowedLastActivityTime = now() - sessionInactivityTimeoutMillis;
|
|
83
|
+
if (session.lastActivityTime < minAllowedLastActivityTime) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const minAllowedStartTime = now() - sessionTerminationTimeoutMillis;
|
|
87
|
+
return session.startTime >= minAllowedStartTime;
|
|
88
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { nav, NO_VALUE_FALLBACK, win } from "../utils";
|
|
2
|
+
import { BROWSER_TAB_ID, NETWORK_CONNECTION_TYPE, SESSION_ID, WINDOW_HEIGHT, WINDOW_WIDTH, } from "../semantic-conventions";
|
|
3
|
+
import { sessionId } from "../api/session";
|
|
4
|
+
import { vars } from "../vars";
|
|
5
|
+
import { addAttribute } from "../utils/otel";
|
|
6
|
+
import { addUrlAttributes } from "./url";
|
|
7
|
+
import { tabId } from "../utils/tab-id";
|
|
8
|
+
export function addCommonAttributes(attributes, options) {
|
|
9
|
+
for (let i = 0; i < vars.signalAttributes.length; i++) {
|
|
10
|
+
attributes.push(vars.signalAttributes[i]);
|
|
11
|
+
}
|
|
12
|
+
addUrlAttributes(attributes, options?.url ?? win?.location.href ?? NO_VALUE_FALLBACK);
|
|
13
|
+
if (sessionId) {
|
|
14
|
+
addAttribute(attributes, SESSION_ID, sessionId);
|
|
15
|
+
}
|
|
16
|
+
if (tabId) {
|
|
17
|
+
addAttribute(attributes, BROWSER_TAB_ID, tabId);
|
|
18
|
+
}
|
|
19
|
+
addAttribute(attributes, WINDOW_WIDTH, win?.innerWidth ?? NO_VALUE_FALLBACK);
|
|
20
|
+
addAttribute(attributes, WINDOW_HEIGHT, win?.innerHeight ?? NO_VALUE_FALLBACK);
|
|
21
|
+
// @ts-expect-error -- TypeScript is not aware of navigator.connection.effectiveType
|
|
22
|
+
const connectionType = nav?.connection?.effectiveType;
|
|
23
|
+
if (connectionType) {
|
|
24
|
+
addAttribute(attributes, NETWORK_CONNECTION_TYPE, connectionType);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { addAttribute } from "../utils/otel";
|
|
2
|
+
import { URL_DOMAIN, URL_FRAGMENT, URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME } from "../semantic-conventions";
|
|
3
|
+
import { parseUrl } from "../utils/url";
|
|
4
|
+
export function addUrlAttributes(attributes, url) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = parseUrl(url);
|
|
7
|
+
if (parsed.username)
|
|
8
|
+
parsed.username = "REDACTED";
|
|
9
|
+
if (parsed.password)
|
|
10
|
+
parsed.password = "REDACTED";
|
|
11
|
+
addAttribute(attributes, URL_FULL, parsed.href);
|
|
12
|
+
addAttribute(attributes, URL_PATH, parsed.pathname);
|
|
13
|
+
addAttribute(attributes, URL_DOMAIN, parsed.hostname);
|
|
14
|
+
addAttribute(attributes, URL_SCHEME, parsed.protocol.replace(":", ""));
|
|
15
|
+
if (parsed.hash) {
|
|
16
|
+
addAttribute(attributes, URL_FRAGMENT, parsed.hash.replace("#", ""));
|
|
17
|
+
}
|
|
18
|
+
if (parsed.search) {
|
|
19
|
+
addAttribute(attributes, URL_QUERY, parsed.search.replace("?", ""));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (_e) {
|
|
23
|
+
addAttribute(attributes, URL_FULL, String(url));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { debug, INIT_MESSAGE } from "../utils";
|
|
2
|
+
import { init as initApi } from "../api/init";
|
|
3
|
+
export * from "../api/identify";
|
|
4
|
+
export * from "../api/debug";
|
|
5
|
+
export * from "../api/attributes";
|
|
6
|
+
export * from "../api/events";
|
|
7
|
+
export { terminateSession } from "../api/session";
|
|
8
|
+
export { reportError } from "../api/report-error";
|
|
9
|
+
export function init(opts) {
|
|
10
|
+
debug(`${INIT_MESSAGE} (via package)`);
|
|
11
|
+
initApi(opts);
|
|
12
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { win, debug, warn, INIT_MESSAGE } from "../utils";
|
|
2
|
+
import { reportError } from "../api/report-error";
|
|
3
|
+
import { identify } from "../api/identify";
|
|
4
|
+
import { debug as debugApi } from "../api/debug";
|
|
5
|
+
import { init as initApi } from "../api/init";
|
|
6
|
+
import { terminateSession } from "../api/session";
|
|
7
|
+
import { addSignalAttribute, removeSignalAttribute } from "../api/attributes";
|
|
8
|
+
import { sendEvent } from "../api/events";
|
|
9
|
+
/**
|
|
10
|
+
* All the APIs exposed through the script tag via `dash0('{{api name}}')`
|
|
11
|
+
*/
|
|
12
|
+
const scriptApis = {
|
|
13
|
+
init: initApi,
|
|
14
|
+
debug: debugApi,
|
|
15
|
+
identify,
|
|
16
|
+
terminateSession,
|
|
17
|
+
reportError,
|
|
18
|
+
addSignalAttribute,
|
|
19
|
+
removeSignalAttribute,
|
|
20
|
+
sendEvent,
|
|
21
|
+
};
|
|
22
|
+
init();
|
|
23
|
+
function init() {
|
|
24
|
+
debug(`${INIT_MESSAGE} (via Script)`);
|
|
25
|
+
const globalObject = win["dash0"];
|
|
26
|
+
if (!globalObject) {
|
|
27
|
+
warn("global 'dash0' not found. Did you use the correct Dash0 Web SDK initializer?");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!globalObject["_q"]) {
|
|
31
|
+
warn("Dash0 Web SDK command queue not defined. Did you add the script tag multiple times to your website?");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
processQueuedApiCalls(globalObject["_q"]);
|
|
35
|
+
addApiCallAfterInitializationSupport();
|
|
36
|
+
}
|
|
37
|
+
function processQueuedApiCalls(apiCalls) {
|
|
38
|
+
for (let i = 0, len = apiCalls.length; i < len; i++) {
|
|
39
|
+
processQueuedApiCall(apiCalls[i]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function processQueuedApiCall(apiCall) {
|
|
43
|
+
const apiName = apiCall[0];
|
|
44
|
+
// @ts-expect-error the APIs are dynamic
|
|
45
|
+
const apiFn = scriptApis[apiName];
|
|
46
|
+
if (!apiFn) {
|
|
47
|
+
warn("Unsupported Dash0 Web SDK api: ", apiCall[0]);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const args = [];
|
|
51
|
+
for (let i = 1; i < apiCall.length; i++) {
|
|
52
|
+
args.push(apiCall[i]);
|
|
53
|
+
}
|
|
54
|
+
apiFn.apply(null, args);
|
|
55
|
+
}
|
|
56
|
+
function addApiCallAfterInitializationSupport() {
|
|
57
|
+
win["dash0"] = function () {
|
|
58
|
+
return processQueuedApiCall(arguments);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY = "__dash0OriginalFunctions";
|
|
2
|
+
// Asynchronous function wrapping: The process of wrapping a listener which goes into one function, e.g.
|
|
3
|
+
//
|
|
4
|
+
// - EventTarget#addEventListener
|
|
5
|
+
// - EventEmitter#on
|
|
6
|
+
//
|
|
7
|
+
// and is removed via another function, e.g.
|
|
8
|
+
//
|
|
9
|
+
// - EventTarget#removeEventListener
|
|
10
|
+
// - EventEmitter#off
|
|
11
|
+
//
|
|
12
|
+
// What is complicated about this, is that these methods identify registered listeners by function reference.
|
|
13
|
+
// When we wrap a function, we naturally change the reference. We must therefore keep track of which
|
|
14
|
+
// original function belongs to what wrapped function.
|
|
15
|
+
//
|
|
16
|
+
// This file provides helpers that help in the typical cases. It is removed from all browser specific APIs
|
|
17
|
+
// in order to allow simple unit test execution.
|
|
18
|
+
//
|
|
19
|
+
// Note that this file follows the behavior outlined in DOM specification. Among others, this means that it is not
|
|
20
|
+
// possible to register the same listener twice.
|
|
21
|
+
// http://dom.spec.whatwg.org
|
|
22
|
+
export function addWrappedFunction(storageTarget, wrappedFunction, valuesForEqualityCheck) {
|
|
23
|
+
if (!storageTarget)
|
|
24
|
+
return wrappedFunction;
|
|
25
|
+
const storage = (storageTarget[WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY] =
|
|
26
|
+
storageTarget[WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY] || []);
|
|
27
|
+
const index = findInStorage(storageTarget, valuesForEqualityCheck);
|
|
28
|
+
if (index !== -1) {
|
|
29
|
+
// already registered. Do not allow re-registration
|
|
30
|
+
return storage[index].wrappedFunction;
|
|
31
|
+
}
|
|
32
|
+
storage.push({
|
|
33
|
+
wrappedFunction,
|
|
34
|
+
valuesForEqualityCheck,
|
|
35
|
+
});
|
|
36
|
+
return wrappedFunction;
|
|
37
|
+
}
|
|
38
|
+
function findInStorage(storageTarget, valuesForEqualityCheck) {
|
|
39
|
+
const storage = storageTarget[WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY];
|
|
40
|
+
for (let i = 0; i < storage.length; i++) {
|
|
41
|
+
const storageItem = storage[i];
|
|
42
|
+
if (matchesEqualityCheck(storageItem.valuesForEqualityCheck, valuesForEqualityCheck)) {
|
|
43
|
+
return i;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
export function popWrappedFunction(storageTarget, valuesForEqualityCheck, fallback) {
|
|
49
|
+
const storage = storageTarget?.[WRAPPED_EVENT_HANDLERS_ORIGINAL_FUNCTION_STORAGE_KEY];
|
|
50
|
+
if (storage == null) {
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
const index = findInStorage(storageTarget, valuesForEqualityCheck);
|
|
54
|
+
if (index === -1) {
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
const storageItem = storage[index];
|
|
58
|
+
storage.splice(index, 1);
|
|
59
|
+
return storageItem.wrappedFunction;
|
|
60
|
+
}
|
|
61
|
+
function matchesEqualityCheck(valuesForEqualityCheckA, valuesForEqualityCheckB) {
|
|
62
|
+
if (valuesForEqualityCheckA.length !== valuesForEqualityCheckB.length) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < valuesForEqualityCheckA.length; i++) {
|
|
66
|
+
if (valuesForEqualityCheckA[i] !== valuesForEqualityCheckB[i]) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
export function addWrappedDomEventListener(storageTarget, wrappedFunction, eventName, eventListener, optionsOrCapture) {
|
|
73
|
+
return addWrappedFunction(storageTarget, wrappedFunction, getDomEventListenerValuesForEqualityCheck(eventName, eventListener, optionsOrCapture));
|
|
74
|
+
}
|
|
75
|
+
function getDomEventListenerValuesForEqualityCheck(eventName, eventListener, optionsOrCapture) {
|
|
76
|
+
return [eventName, eventListener, getDomEventListenerCaptureValue(optionsOrCapture)];
|
|
77
|
+
}
|
|
78
|
+
export function getDomEventListenerCaptureValue(optionsOrCapture) {
|
|
79
|
+
// > Let capture, passive, and once be the result of flattening more options.
|
|
80
|
+
// https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener
|
|
81
|
+
//
|
|
82
|
+
// > To flatten more options, run these steps:
|
|
83
|
+
// > 1. Let capture be the result of flattening options.
|
|
84
|
+
// https://dom.spec.whatwg.org/#event-flatten-more
|
|
85
|
+
//
|
|
86
|
+
// > To flatten options, run these steps:
|
|
87
|
+
// > 1. If options is a boolean, then return options.
|
|
88
|
+
// > 2. Return options’s capture.
|
|
89
|
+
// https://dom.spec.whatwg.org/#concept-flatten-options
|
|
90
|
+
//
|
|
91
|
+
// > dictionary EventListenerOptions {
|
|
92
|
+
// > boolean capture = false;
|
|
93
|
+
// > };
|
|
94
|
+
// https://dom.spec.whatwg.org/#dom-eventlisteneroptions-capture
|
|
95
|
+
if (optionsOrCapture == null) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
else if (typeof optionsOrCapture === "object") {
|
|
99
|
+
return Boolean(optionsOrCapture.capture);
|
|
100
|
+
}
|
|
101
|
+
return Boolean(optionsOrCapture);
|
|
102
|
+
}
|
|
103
|
+
export function popWrappedDomEventListener(storageTarget, eventName, eventListener, optionsOrCapture, fallback) {
|
|
104
|
+
return popWrappedFunction(storageTarget, getDomEventListenerValuesForEqualityCheck(eventName, eventListener, optionsOrCapture), fallback);
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { vars } from "../../vars";
|
|
2
|
+
import { win } from "../../utils";
|
|
3
|
+
import { addWrappedDomEventListener, popWrappedDomEventListener, } from "./async-function-wrapping";
|
|
4
|
+
import { ignoreNextOnErrorEvent } from "./unhandled-error";
|
|
5
|
+
export function startEventHandlerInstrumentation() {
|
|
6
|
+
if (vars.wrapEventHandlers) {
|
|
7
|
+
wrapEventTarget(win?.EventTarget);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function wrapEventTarget(EventTarget) {
|
|
11
|
+
if (!EventTarget ||
|
|
12
|
+
typeof EventTarget.prototype.addEventListener !== "function" ||
|
|
13
|
+
typeof EventTarget.prototype.removeEventListener !== "function") {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
17
|
+
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
|
|
18
|
+
EventTarget.prototype.addEventListener = function wrappedAddEventListener(eventName, fn, optionsOrCapture) {
|
|
19
|
+
if (typeof fn !== "function") {
|
|
20
|
+
return originalAddEventListener.apply(this, arguments);
|
|
21
|
+
}
|
|
22
|
+
// non-deopt arguments copy
|
|
23
|
+
const args = new Array(arguments.length);
|
|
24
|
+
for (let i = 0; i < arguments.length; i++) {
|
|
25
|
+
args[i] = arguments[i];
|
|
26
|
+
}
|
|
27
|
+
args[1] = function wrappedEventListener() {
|
|
28
|
+
try {
|
|
29
|
+
return fn.apply(this, arguments);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
reportError(e);
|
|
33
|
+
ignoreNextOnErrorEvent();
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
args[1] = addWrappedDomEventListener(this, args[1], eventName, fn, optionsOrCapture);
|
|
38
|
+
return originalAddEventListener.apply(this, args);
|
|
39
|
+
};
|
|
40
|
+
EventTarget.prototype.removeEventListener = function wrappedRemoveEventListener(eventName, fn, optionsOrCapture) {
|
|
41
|
+
if (typeof fn !== "function") {
|
|
42
|
+
return originalRemoveEventListener.apply(this, arguments);
|
|
43
|
+
}
|
|
44
|
+
// non-deopt arguments copy
|
|
45
|
+
const args = new Array(arguments.length);
|
|
46
|
+
for (let i = 0; i < arguments.length; i++) {
|
|
47
|
+
args[i] = arguments[i];
|
|
48
|
+
}
|
|
49
|
+
args[1] = popWrappedDomEventListener(this, eventName, fn, optionsOrCapture, fn);
|
|
50
|
+
return originalRemoveEventListener.apply(this, args);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { startOnErrorInstrumentation } from "./unhandled-error";
|
|
2
|
+
import { startUnhandledRejectionInstrumentation } from "./unhandled-promise-rejection";
|
|
3
|
+
import { startEventHandlerInstrumentation } from "./event-handlers";
|
|
4
|
+
import { startTimerInstrumentation } from "./timers";
|
|
5
|
+
export function startErrorInstrumentation() {
|
|
6
|
+
startOnErrorInstrumentation();
|
|
7
|
+
startUnhandledRejectionInstrumentation();
|
|
8
|
+
startEventHandlerInstrumentation();
|
|
9
|
+
startTimerInstrumentation();
|
|
10
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { vars } from "../../vars";
|
|
2
|
+
import { isRunningZoneJs } from "../../utils/timers";
|
|
3
|
+
import { warn, win } from "../../utils";
|
|
4
|
+
import { ignoreNextOnErrorEvent } from "./unhandled-error";
|
|
5
|
+
export function startTimerInstrumentation() {
|
|
6
|
+
if (vars.wrapTimers) {
|
|
7
|
+
if (isRunningZoneJs) {
|
|
8
|
+
warn("We discovered a usage of Zone.js. In order to avoid any incompatibility issues timer wrapping is not going to be enabled.");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
wrapTimer("setTimeout");
|
|
12
|
+
wrapTimer("setInterval");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function wrapTimer(name) {
|
|
16
|
+
const original = win?.[name];
|
|
17
|
+
if (typeof original !== "function") {
|
|
18
|
+
// cannot wrap because fn is not a function – should actually never happen
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
win[name] = function wrappedTimerSetter(fn) {
|
|
22
|
+
// non-deopt arguments copy
|
|
23
|
+
const args = new Array(arguments.length);
|
|
24
|
+
for (let i = 0; i < arguments.length; i++) {
|
|
25
|
+
args[i] = arguments[i];
|
|
26
|
+
}
|
|
27
|
+
args[0] = wrap(fn);
|
|
28
|
+
return original.apply(this, args);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function wrap(fn) {
|
|
32
|
+
if (typeof fn !== "function") {
|
|
33
|
+
// cannot wrap because fn is not a function
|
|
34
|
+
return fn;
|
|
35
|
+
}
|
|
36
|
+
return function wrappedTimerHandler() {
|
|
37
|
+
try {
|
|
38
|
+
return fn.apply(this, arguments);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
reportError(e);
|
|
42
|
+
ignoreNextOnErrorEvent();
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { hasOwnProperty, nowNanos, win } from "../../utils";
|
|
2
|
+
import { isErrorMessageIgnored } from "../../utils/ignore-rules";
|
|
3
|
+
import { sendLog } from "../../transport";
|
|
4
|
+
import { EVENT_NAME, EVENT_NAMES, EXCEPTION_COMPONENT_STACK, EXCEPTION_MESSAGE, EXCEPTION_STACKTRACE, EXCEPTION_TYPE, LOG_SEVERITIES, } from "../../semantic-conventions";
|
|
5
|
+
import { addAttribute } from "../../utils/otel";
|
|
6
|
+
import { addCommonAttributes } from "../../attributes";
|
|
7
|
+
const MAX_ERRORS_TO_REPORT = 100;
|
|
8
|
+
const MAX_STACK_SIZE = 30;
|
|
9
|
+
const MAX_NUMBER_OF_TRACKED_ERRORS = 20;
|
|
10
|
+
let reportedErrors = 0;
|
|
11
|
+
let numberOfDifferentErrorsSeen = 0;
|
|
12
|
+
let seenErrors = {};
|
|
13
|
+
let scheduledTransmissionTimeoutHandle;
|
|
14
|
+
// We are wrapping global listeners. In these, we are catching and rethrowing errors.
|
|
15
|
+
// In older browsers, rethrowing errors actually manipulates the error objects. As a
|
|
16
|
+
// result, it is not possible to just mark an error as reported. The simplest way to
|
|
17
|
+
// avoid double reporting is to temporarily disable the global onError handler…
|
|
18
|
+
let ignoreNextOnError = false;
|
|
19
|
+
export function ignoreNextOnErrorEvent() {
|
|
20
|
+
ignoreNextOnError = true;
|
|
21
|
+
}
|
|
22
|
+
export function startOnErrorInstrumentation() {
|
|
23
|
+
if (!win)
|
|
24
|
+
return;
|
|
25
|
+
const globalOnError = win.onerror;
|
|
26
|
+
win.onerror = function (message, fileName, lineNumber, columnNumber, error) {
|
|
27
|
+
if (ignoreNextOnError) {
|
|
28
|
+
ignoreNextOnError = false;
|
|
29
|
+
if (typeof globalOnError === "function") {
|
|
30
|
+
return globalOnError.apply(this, arguments);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let stack = error && error.stack;
|
|
35
|
+
if (!stack) {
|
|
36
|
+
stack = "at " + fileName + " " + lineNumber;
|
|
37
|
+
if (columnNumber != null) {
|
|
38
|
+
stack += ":" + columnNumber;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
onUnhandledError({ message: String(message), stack });
|
|
42
|
+
if (typeof globalOnError === "function") {
|
|
43
|
+
return globalOnError.apply(this, arguments);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function reportError(error, opts) {
|
|
48
|
+
if (!error) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (typeof error === "string") {
|
|
52
|
+
onUnhandledError({ message: error, opts });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
onUnhandledError({ message: error.message, type: error.name, stack: error.stack, opts });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function onUnhandledError({ message, type, stack, opts }) {
|
|
59
|
+
if (!message || reportedErrors > MAX_ERRORS_TO_REPORT) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (isErrorMessageIgnored(message)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (numberOfDifferentErrorsSeen >= MAX_NUMBER_OF_TRACKED_ERRORS) {
|
|
66
|
+
seenErrors = {};
|
|
67
|
+
numberOfDifferentErrorsSeen = 0;
|
|
68
|
+
}
|
|
69
|
+
message = String(message).substring(0, 300);
|
|
70
|
+
stack = shortenStackTrace(stack);
|
|
71
|
+
const location = win?.location.href;
|
|
72
|
+
const key = message + stack + location;
|
|
73
|
+
let trackedError = seenErrors[key];
|
|
74
|
+
if (trackedError) {
|
|
75
|
+
trackedError.seenCount++;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const attributes = [];
|
|
79
|
+
addAttribute(attributes, EVENT_NAME, EVENT_NAMES.ERROR);
|
|
80
|
+
addAttribute(attributes, EXCEPTION_MESSAGE, message);
|
|
81
|
+
if (type) {
|
|
82
|
+
addAttribute(attributes, EXCEPTION_TYPE, type);
|
|
83
|
+
}
|
|
84
|
+
if (stack) {
|
|
85
|
+
addAttribute(attributes, EXCEPTION_STACKTRACE, stack);
|
|
86
|
+
}
|
|
87
|
+
if (opts?.componentStack) {
|
|
88
|
+
addAttribute(attributes, EXCEPTION_COMPONENT_STACK, opts?.componentStack.substring(0, 2048));
|
|
89
|
+
}
|
|
90
|
+
addCommonAttributes(attributes);
|
|
91
|
+
trackedError = {
|
|
92
|
+
seenCount: 1,
|
|
93
|
+
transmittedCount: 0,
|
|
94
|
+
log: {
|
|
95
|
+
timeUnixNano: nowNanos(),
|
|
96
|
+
attributes: attributes,
|
|
97
|
+
severityNumber: LOG_SEVERITIES.ERROR,
|
|
98
|
+
severityText: "ERROR",
|
|
99
|
+
body: {
|
|
100
|
+
stringValue: message,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
seenErrors[key] = trackedError;
|
|
105
|
+
numberOfDifferentErrorsSeen++;
|
|
106
|
+
}
|
|
107
|
+
scheduleTransmission();
|
|
108
|
+
}
|
|
109
|
+
export function shortenStackTrace(stack) {
|
|
110
|
+
return String(stack || "")
|
|
111
|
+
.split("\n")
|
|
112
|
+
.slice(0, MAX_STACK_SIZE)
|
|
113
|
+
.join("\n");
|
|
114
|
+
}
|
|
115
|
+
function scheduleTransmission() {
|
|
116
|
+
if (scheduledTransmissionTimeoutHandle) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
scheduledTransmissionTimeoutHandle = setTimeout(send, 1000);
|
|
120
|
+
}
|
|
121
|
+
function send() {
|
|
122
|
+
if (scheduledTransmissionTimeoutHandle) {
|
|
123
|
+
clearTimeout(scheduledTransmissionTimeoutHandle);
|
|
124
|
+
scheduledTransmissionTimeoutHandle = null;
|
|
125
|
+
}
|
|
126
|
+
for (const key in seenErrors) {
|
|
127
|
+
if (hasOwnProperty(seenErrors, key)) {
|
|
128
|
+
const seenError = seenErrors[key];
|
|
129
|
+
if (seenError.seenCount > seenError.transmittedCount) {
|
|
130
|
+
sendLog(seenError.log);
|
|
131
|
+
reportedErrors++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
seenErrors = {};
|
|
136
|
+
numberOfDifferentErrorsSeen = 0;
|
|
137
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { win } from "../../utils";
|
|
2
|
+
import { reportError } from "./unhandled-error";
|
|
3
|
+
const MESSAGE_PREFIX = "Unhandled promise rejection: ";
|
|
4
|
+
const STACK_UNAVAILABLE_MESSAGE = "<unavailable because Promise wasn't rejected with an Error object>";
|
|
5
|
+
export function startUnhandledRejectionInstrumentation() {
|
|
6
|
+
if (typeof win?.addEventListener === "function") {
|
|
7
|
+
win.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function onUnhandledRejection(event) {
|
|
11
|
+
if (event.reason == null) {
|
|
12
|
+
reportError({
|
|
13
|
+
message: MESSAGE_PREFIX + "<no reason defined>",
|
|
14
|
+
stack: STACK_UNAVAILABLE_MESSAGE,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
else if (typeof event.reason.message === "string") {
|
|
18
|
+
reportError({
|
|
19
|
+
message: MESSAGE_PREFIX + event.reason.message,
|
|
20
|
+
stack: typeof event.reason.stack === "string" ? event.reason.stack : STACK_UNAVAILABLE_MESSAGE,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else if (typeof event.reason !== "object") {
|
|
24
|
+
reportError({
|
|
25
|
+
message: MESSAGE_PREFIX + event.reason,
|
|
26
|
+
stack: STACK_UNAVAILABLE_MESSAGE,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|