@dash0/sdk-web 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/dash0.iife.js +1 -1
- package/dist/dash0.iife.js.map +1 -1
- package/dist/dash0.js +1 -1
- package/dist/dash0.js.map +1 -1
- package/dist/dash0.umd.cjs +1 -1
- package/dist/dash0.umd.cjs.map +1 -1
- package/dist/modules/api/init.js +4 -0
- package/dist/modules/api/init_test.js +7 -0
- package/dist/modules/api/start-view.js +47 -0
- package/dist/modules/api/start-view_test.js +91 -0
- package/dist/modules/entrypoint/npm-package.js +1 -0
- package/dist/modules/entrypoint/npm-package_test.js +7 -0
- package/dist/modules/entrypoint/script.js +2 -0
- package/dist/modules/instrumentations/http/fetch.js +90 -119
- package/dist/modules/instrumentations/http/fetch_test.js +47 -0
- package/dist/modules/instrumentations/http/utils.js +67 -3
- package/dist/modules/instrumentations/http/xhr.js +340 -0
- package/dist/modules/instrumentations/http/xhr_test.js +705 -0
- package/dist/modules/instrumentations/navigation/event.js +42 -10
- package/dist/modules/instrumentations/navigation/event_test.js +128 -0
- package/dist/modules/utils/wrap.js +16 -3
- package/dist/modules/utils/wrap_test.js +31 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/api/start-view.d.ts +33 -0
- package/dist/types/api/start-view_test.d.ts +1 -0
- package/dist/types/entrypoint/npm-package.d.ts +2 -0
- package/dist/types/entrypoint/npm-package_test.d.ts +1 -0
- package/dist/types/instrumentations/http/utils.d.ts +6 -1
- package/dist/types/instrumentations/http/xhr.d.ts +1 -0
- package/dist/types/instrumentations/http/xhr_test.d.ts +1 -0
- package/dist/types/instrumentations/navigation/event.d.ts +18 -0
- package/dist/types/instrumentations/navigation/event_test.d.ts +1 -0
- package/dist/types/types/options.d.ts +1 -1
- package/dist/types/utils/wrap_test.d.ts +1 -0
- package/package.json +3 -2
- package/src/api/init.ts +4 -0
- package/src/api/init_test.ts +8 -0
- package/src/api/start-view.ts +68 -0
- package/src/api/start-view_test.ts +125 -0
- package/src/entrypoint/npm-package.ts +2 -0
- package/src/entrypoint/npm-package_test.ts +8 -0
- package/src/entrypoint/script.ts +2 -0
- package/src/instrumentations/http/fetch.ts +120 -159
- package/src/instrumentations/http/fetch_test.ts +58 -0
- package/src/instrumentations/http/utils.ts +90 -3
- package/src/instrumentations/http/xhr.ts +431 -0
- package/src/instrumentations/http/xhr_test.ts +850 -0
- package/src/instrumentations/navigation/event.ts +63 -15
- package/src/instrumentations/navigation/event_test.ts +171 -0
- package/src/types/options.ts +6 -1
- package/src/utils/wrap.ts +15 -3
- package/src/utils/wrap_test.ts +41 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addAttribute, getTraceContextForPageLoad } from "../../utils/otel";
|
|
1
|
+
import { addAttribute, addAttributes, getTraceContextForPageLoad } from "../../utils/otel";
|
|
2
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
3
|
import { doc, NO_VALUE_FALLBACK } from "../../utils";
|
|
4
4
|
import { addCommonAttributes } from "../../attributes";
|
|
@@ -9,23 +9,26 @@ function getPageViewMeta(url) {
|
|
|
9
9
|
return {};
|
|
10
10
|
return vars.pageViewInstrumentation.generateMetadata?.(url) ?? {};
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
const meta = getPageViewMeta(url);
|
|
12
|
+
function buildAndSendPageViewLog(opts) {
|
|
14
13
|
const attributes = [];
|
|
15
14
|
addAttribute(attributes, EVENT_NAME, EVENT_NAMES.PAGE_VIEW);
|
|
16
|
-
if (
|
|
17
|
-
Object.entries(
|
|
15
|
+
if (opts.metaAttributes) {
|
|
16
|
+
Object.entries(opts.metaAttributes).forEach(([key, value]) => addAttribute(attributes, key, value));
|
|
18
17
|
}
|
|
19
|
-
addCommonAttributes(attributes, { url });
|
|
18
|
+
addCommonAttributes(attributes, { url: opts.url });
|
|
19
|
+
// Add custom attributes last to allow overrides
|
|
20
|
+
addAttributes(attributes, opts.customAttributes);
|
|
20
21
|
const bodyAttributes = [];
|
|
21
|
-
addAttribute(bodyAttributes, "title",
|
|
22
|
+
addAttribute(bodyAttributes, "title", opts.title ?? doc?.title ?? NO_VALUE_FALLBACK);
|
|
22
23
|
if (doc?.referrer) {
|
|
23
24
|
addAttribute(bodyAttributes, "referrer", doc.referrer);
|
|
24
25
|
}
|
|
25
|
-
addAttribute(bodyAttributes, PAGE_VIEW_TYPE,
|
|
26
|
-
|
|
26
|
+
addAttribute(bodyAttributes, PAGE_VIEW_TYPE, opts.pageViewType);
|
|
27
|
+
if (opts.changeState) {
|
|
28
|
+
addAttribute(bodyAttributes, PAGE_VIEW_CHANGE_STATE, opts.changeState);
|
|
29
|
+
}
|
|
27
30
|
const log = {
|
|
28
|
-
timeUnixNano: timeUnixNano,
|
|
31
|
+
timeUnixNano: opts.timeUnixNano,
|
|
29
32
|
attributes: attributes,
|
|
30
33
|
severityNumber: LOG_SEVERITIES.INFO,
|
|
31
34
|
severityText: "INFO",
|
|
@@ -42,3 +45,32 @@ export function transmitPageViewEvent(timeUnixNano, url, virtual, replaced) {
|
|
|
42
45
|
}
|
|
43
46
|
sendLog(log);
|
|
44
47
|
}
|
|
48
|
+
export function transmitPageViewEvent(timeUnixNano, url, virtual, replaced) {
|
|
49
|
+
const meta = getPageViewMeta(url);
|
|
50
|
+
buildAndSendPageViewLog({
|
|
51
|
+
timeUnixNano,
|
|
52
|
+
url,
|
|
53
|
+
title: meta.title,
|
|
54
|
+
metaAttributes: meta.attributes,
|
|
55
|
+
pageViewType: virtual ? PAGE_VIEW_TYPE_VALUES.VIRTUAL : PAGE_VIEW_TYPE_VALUES.INITIAL,
|
|
56
|
+
changeState: replaced ? PAGE_VIEW_CHANGE_STATE_VALUES.REPLACE : PAGE_VIEW_CHANGE_STATE_VALUES.PUSH,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Emits a manually-triggered page view log, e.g. from the public `startView` API.
|
|
61
|
+
* Deliberately does NOT read `vars.pageViewInstrumentation.generateMetadata` and does NOT
|
|
62
|
+
* include a `change_state` body key (a manual view is neither a pushState nor replaceState).
|
|
63
|
+
* The emitted `type` is PAGE_VIEW_TYPE_VALUES.VIRTUAL, matching automatic virtual page views —
|
|
64
|
+
* manual views are deliberately indistinguishable from virtual ones downstream.
|
|
65
|
+
* Caller-provided attributes are added after the SDK-generated ones so they can override them,
|
|
66
|
+
* matching the sendEvent semantics.
|
|
67
|
+
*/
|
|
68
|
+
export function transmitManualPageViewEvent(opts) {
|
|
69
|
+
buildAndSendPageViewLog({
|
|
70
|
+
timeUnixNano: opts.timeUnixNano,
|
|
71
|
+
url: opts.url,
|
|
72
|
+
title: opts.title,
|
|
73
|
+
customAttributes: opts.attributes,
|
|
74
|
+
pageViewType: PAGE_VIEW_TYPE_VALUES.VIRTUAL,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
vi.mock("../../transport", () => ({
|
|
3
|
+
sendLog: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
import { sendLog } from "../../transport";
|
|
6
|
+
import { transmitPageViewEvent, transmitManualPageViewEvent } from "./event";
|
|
7
|
+
import { vars } from "../../vars";
|
|
8
|
+
const sendLogMock = sendLog;
|
|
9
|
+
describe("transmitPageViewEvent", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
sendLogMock.mockClear();
|
|
12
|
+
vars.pageViewInstrumentation = { trackVirtualPageViews: true, includeParts: [] };
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
it("transmits an initial page view with type=INITIAL and change_state=pushState", () => {
|
|
18
|
+
transmitPageViewEvent("1700000000000000000", new URL("https://example.com/landing"));
|
|
19
|
+
expect(sendLogMock).toHaveBeenCalledTimes(1);
|
|
20
|
+
const log = sendLogMock.mock.calls[0][0];
|
|
21
|
+
expect(log.timeUnixNano).toBe("1700000000000000000");
|
|
22
|
+
expect(log.severityNumber).toBe(9);
|
|
23
|
+
expect(log.severityText).toBe("INFO");
|
|
24
|
+
expect(log.attributes).toEqual(expect.arrayContaining([{ key: "event.name", value: { stringValue: "browser.page_view" } }]));
|
|
25
|
+
const bodyValues = log.body?.kvlistValue?.values;
|
|
26
|
+
expect(bodyValues).toEqual(expect.arrayContaining([
|
|
27
|
+
{ key: "type", value: { doubleValue: 0 } },
|
|
28
|
+
{ key: "change_state", value: { stringValue: "pushState" } },
|
|
29
|
+
]));
|
|
30
|
+
});
|
|
31
|
+
it("transmits a virtual page view with type=VIRTUAL and change_state=replaceState when replaced", () => {
|
|
32
|
+
transmitPageViewEvent("1700000000000000000", new URL("https://example.com/settings"), true, true);
|
|
33
|
+
const log = sendLogMock.mock.calls[0][0];
|
|
34
|
+
const bodyValues = log.body?.kvlistValue?.values;
|
|
35
|
+
expect(bodyValues).toEqual(expect.arrayContaining([
|
|
36
|
+
{ key: "type", value: { doubleValue: 1 } },
|
|
37
|
+
{ key: "change_state", value: { stringValue: "replaceState" } },
|
|
38
|
+
]));
|
|
39
|
+
});
|
|
40
|
+
it("applies generateMetadata title and attributes when provided", () => {
|
|
41
|
+
vars.pageViewInstrumentation = {
|
|
42
|
+
trackVirtualPageViews: true,
|
|
43
|
+
includeParts: [],
|
|
44
|
+
generateMetadata: () => ({ title: "Custom Title", attributes: { "app.screen": "settings" } }),
|
|
45
|
+
};
|
|
46
|
+
transmitPageViewEvent("1700000000000000000", new URL("https://example.com/settings"), true);
|
|
47
|
+
const log = sendLogMock.mock.calls[0][0];
|
|
48
|
+
expect(log.attributes).toEqual(expect.arrayContaining([{ key: "app.screen", value: { stringValue: "settings" } }]));
|
|
49
|
+
const bodyValues = log.body?.kvlistValue?.values;
|
|
50
|
+
expect(bodyValues).toEqual(expect.arrayContaining([{ key: "title", value: { stringValue: "Custom Title" } }]));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("transmitManualPageViewEvent", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
sendLogMock.mockClear();
|
|
56
|
+
vars.pageViewInstrumentation = {
|
|
57
|
+
trackVirtualPageViews: true,
|
|
58
|
+
includeParts: [],
|
|
59
|
+
generateMetadata: () => ({ title: "should not be used", attributes: { should_not_appear: true } }),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
vars.pageViewInstrumentation = { trackVirtualPageViews: true, includeParts: [] };
|
|
65
|
+
});
|
|
66
|
+
it("emits type=VIRTUAL and no change_state key", () => {
|
|
67
|
+
transmitManualPageViewEvent({ timeUnixNano: "1700000000000000000", title: "/settings" });
|
|
68
|
+
const log = sendLogMock.mock.calls[0][0];
|
|
69
|
+
const bodyValues = log.body?.kvlistValue?.values;
|
|
70
|
+
expect(bodyValues).toEqual(expect.arrayContaining([
|
|
71
|
+
{ key: "type", value: { doubleValue: 1 } },
|
|
72
|
+
{ key: "title", value: { stringValue: "/settings" } },
|
|
73
|
+
]));
|
|
74
|
+
expect(bodyValues.find((v) => v.key === "change_state")).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
it("does not invoke vars.pageViewInstrumentation.generateMetadata", () => {
|
|
77
|
+
const generateMetadata = vi.fn().mockReturnValue({ title: "ignored" });
|
|
78
|
+
vars.pageViewInstrumentation = { trackVirtualPageViews: true, includeParts: [], generateMetadata };
|
|
79
|
+
transmitManualPageViewEvent({ timeUnixNano: "1700000000000000000", title: "/settings" });
|
|
80
|
+
expect(generateMetadata).not.toHaveBeenCalled();
|
|
81
|
+
const log = sendLogMock.mock.calls[0][0];
|
|
82
|
+
const bodyValues = log.body?.kvlistValue?.values;
|
|
83
|
+
expect(bodyValues).toEqual(expect.arrayContaining([{ key: "title", value: { stringValue: "/settings" } }]));
|
|
84
|
+
});
|
|
85
|
+
it("merges custom attributes into signal attributes", () => {
|
|
86
|
+
transmitManualPageViewEvent({
|
|
87
|
+
timeUnixNano: "1700000000000000000",
|
|
88
|
+
title: "/settings",
|
|
89
|
+
attributes: { "app.screen": "settings", "app.tab_index": 2 },
|
|
90
|
+
});
|
|
91
|
+
const log = sendLogMock.mock.calls[0][0];
|
|
92
|
+
expect(log.attributes).toEqual(expect.arrayContaining([
|
|
93
|
+
{ key: "app.screen", value: { stringValue: "settings" } },
|
|
94
|
+
{ key: "app.tab_index", value: { doubleValue: 2 } },
|
|
95
|
+
]));
|
|
96
|
+
});
|
|
97
|
+
it("adds custom attributes after SDK-generated ones so they can override", () => {
|
|
98
|
+
transmitManualPageViewEvent({
|
|
99
|
+
timeUnixNano: "1700000000000000000",
|
|
100
|
+
title: "/settings",
|
|
101
|
+
url: new URL("https://example.com/real-path"),
|
|
102
|
+
attributes: { "page.url.path": "/custom-path" },
|
|
103
|
+
});
|
|
104
|
+
const log = sendLogMock.mock.calls[0][0];
|
|
105
|
+
const pathAttributes = log.attributes.filter((attr) => attr.key === "page.url.path");
|
|
106
|
+
expect(pathAttributes.length).toBeGreaterThan(1);
|
|
107
|
+
expect(pathAttributes[pathAttributes.length - 1]).toEqual({
|
|
108
|
+
key: "page.url.path",
|
|
109
|
+
value: { stringValue: "/custom-path" },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
it("passes url through to page.url.* attributes when provided", () => {
|
|
113
|
+
transmitManualPageViewEvent({
|
|
114
|
+
timeUnixNano: "1700000000000000000",
|
|
115
|
+
title: "/settings",
|
|
116
|
+
url: new URL("https://example.com/settings"),
|
|
117
|
+
});
|
|
118
|
+
const log = sendLogMock.mock.calls[0][0];
|
|
119
|
+
expect(log.attributes).toEqual(expect.arrayContaining([{ key: "page.url.path", value: { stringValue: "/settings" } }]));
|
|
120
|
+
});
|
|
121
|
+
it("includes referrer the same way the auto path does", () => {
|
|
122
|
+
transmitManualPageViewEvent({ timeUnixNano: "1700000000000000000", title: "/settings" });
|
|
123
|
+
const log = sendLogMock.mock.calls[0][0];
|
|
124
|
+
const bodyValues = log.body?.kvlistValue?.values;
|
|
125
|
+
// doc.referrer is empty string in jsdom by default, so "referrer" key should be absent
|
|
126
|
+
expect(bodyValues.find((v) => v.key === "referrer")).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { debug } from "./debug";
|
|
1
|
+
import { debug, warn } from "./debug";
|
|
2
2
|
const INSTRUMENTED_BY_DASH0_SYMBOL = Symbol.for("INSTRUMENTED_BY_DASH0");
|
|
3
3
|
function isAlreadyInstrumented(objOrFunction) {
|
|
4
4
|
// @ts-expect-error -- typescript does not know about this hidden marker and we're not going to tell it 🤫
|
|
@@ -18,6 +18,19 @@ export function wrap(module, target, wrapper) {
|
|
|
18
18
|
debug(`${String(target)} has already been instrumented, skipping`);
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
try {
|
|
22
|
+
const wrapped = wrapper(original);
|
|
23
|
+
// Mark the replacement as well: a later wrap() call reads the replacement from module[target],
|
|
24
|
+
// so marking only the original would let repeated instrumentation wrap the wrapper.
|
|
25
|
+
markAsInstrumented(original);
|
|
26
|
+
if (wrapped) {
|
|
27
|
+
markAsInstrumented(wrapped);
|
|
28
|
+
}
|
|
29
|
+
module[target] = wrapped;
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
// Pages can lock properties via Object.defineProperty/Object.freeze -- failing to install
|
|
33
|
+
// one instrumentation must not throw out of init() and abort the remaining ones.
|
|
34
|
+
warn(`failed to instrument ${String(target)}, the page may have locked the property`, e);
|
|
35
|
+
}
|
|
23
36
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { wrap } from "./wrap";
|
|
3
|
+
describe("wrap", () => {
|
|
4
|
+
it("replaces the target with the wrapper's return value", () => {
|
|
5
|
+
const original = () => "original";
|
|
6
|
+
const module = { fn: original };
|
|
7
|
+
wrap(module, "fn", (orig) => () => `wrapped ${orig()}`);
|
|
8
|
+
expect(module.fn()).toBe("wrapped original");
|
|
9
|
+
});
|
|
10
|
+
it("does not wrap the same target twice", () => {
|
|
11
|
+
const module = { fn: () => "original" };
|
|
12
|
+
const wrapper = vi.fn((orig) => () => `wrapped ${orig()}`);
|
|
13
|
+
wrap(module, "fn", wrapper);
|
|
14
|
+
wrap(module, "fn", wrapper);
|
|
15
|
+
expect(wrapper).toHaveBeenCalledTimes(1);
|
|
16
|
+
expect(module.fn()).toBe("wrapped original");
|
|
17
|
+
});
|
|
18
|
+
it("skips undefined targets", () => {
|
|
19
|
+
const module = {};
|
|
20
|
+
const wrapper = vi.fn();
|
|
21
|
+
expect(() => wrap(module, "fn", wrapper)).not.toThrow();
|
|
22
|
+
expect(wrapper).not.toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
it("does not throw when the page locked the target property", () => {
|
|
25
|
+
const original = () => "original";
|
|
26
|
+
const module = {};
|
|
27
|
+
Object.defineProperty(module, "fn", { value: original, writable: false, configurable: false });
|
|
28
|
+
expect(() => wrap(module, "fn", (orig) => () => `wrapped ${orig()}`)).not.toThrow();
|
|
29
|
+
expect(module.fn).toBe(original);
|
|
30
|
+
});
|
|
31
|
+
});
|