@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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/dash0.iife.js +1 -1
  3. package/dist/dash0.iife.js.map +1 -1
  4. package/dist/dash0.js +1 -1
  5. package/dist/dash0.js.map +1 -1
  6. package/dist/dash0.umd.cjs +1 -1
  7. package/dist/dash0.umd.cjs.map +1 -1
  8. package/dist/modules/api/init.js +4 -0
  9. package/dist/modules/api/init_test.js +7 -0
  10. package/dist/modules/api/start-view.js +47 -0
  11. package/dist/modules/api/start-view_test.js +91 -0
  12. package/dist/modules/entrypoint/npm-package.js +1 -0
  13. package/dist/modules/entrypoint/npm-package_test.js +7 -0
  14. package/dist/modules/entrypoint/script.js +2 -0
  15. package/dist/modules/instrumentations/http/fetch.js +90 -119
  16. package/dist/modules/instrumentations/http/fetch_test.js +47 -0
  17. package/dist/modules/instrumentations/http/utils.js +67 -3
  18. package/dist/modules/instrumentations/http/xhr.js +340 -0
  19. package/dist/modules/instrumentations/http/xhr_test.js +705 -0
  20. package/dist/modules/instrumentations/navigation/event.js +42 -10
  21. package/dist/modules/instrumentations/navigation/event_test.js +128 -0
  22. package/dist/modules/utils/wrap.js +16 -3
  23. package/dist/modules/utils/wrap_test.js +31 -0
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/dist/types/api/start-view.d.ts +33 -0
  26. package/dist/types/api/start-view_test.d.ts +1 -0
  27. package/dist/types/entrypoint/npm-package.d.ts +2 -0
  28. package/dist/types/entrypoint/npm-package_test.d.ts +1 -0
  29. package/dist/types/instrumentations/http/utils.d.ts +6 -1
  30. package/dist/types/instrumentations/http/xhr.d.ts +1 -0
  31. package/dist/types/instrumentations/http/xhr_test.d.ts +1 -0
  32. package/dist/types/instrumentations/navigation/event.d.ts +18 -0
  33. package/dist/types/instrumentations/navigation/event_test.d.ts +1 -0
  34. package/dist/types/types/options.d.ts +1 -1
  35. package/dist/types/utils/wrap_test.d.ts +1 -0
  36. package/package.json +3 -2
  37. package/src/api/init.ts +4 -0
  38. package/src/api/init_test.ts +8 -0
  39. package/src/api/start-view.ts +68 -0
  40. package/src/api/start-view_test.ts +125 -0
  41. package/src/entrypoint/npm-package.ts +2 -0
  42. package/src/entrypoint/npm-package_test.ts +8 -0
  43. package/src/entrypoint/script.ts +2 -0
  44. package/src/instrumentations/http/fetch.ts +120 -159
  45. package/src/instrumentations/http/fetch_test.ts +58 -0
  46. package/src/instrumentations/http/utils.ts +90 -3
  47. package/src/instrumentations/http/xhr.ts +431 -0
  48. package/src/instrumentations/http/xhr_test.ts +850 -0
  49. package/src/instrumentations/navigation/event.ts +63 -15
  50. package/src/instrumentations/navigation/event_test.ts +171 -0
  51. package/src/types/options.ts +6 -1
  52. package/src/utils/wrap.ts +15 -3
  53. package/src/utils/wrap_test.ts +41 -0
@@ -1,4 +1,4 @@
1
- import { addAttribute, getTraceContextForPageLoad } from "../../utils/otel";
1
+ import { addAttribute, addAttributes, AttributeValueType, getTraceContextForPageLoad } from "../../utils/otel";
2
2
  import {
3
3
  EVENT_NAME,
4
4
  EVENT_NAMES,
@@ -12,7 +12,7 @@ import { doc, NO_VALUE_FALLBACK } from "../../utils";
12
12
  import { addCommonAttributes } from "../../attributes";
13
13
  import { sendLog } from "../../transport";
14
14
  import { PageViewMeta, vars } from "../../vars";
15
- import { KeyValue, LogRecord } from "../../types/otlp";
15
+ import { AnyValue, KeyValue, LogRecord } from "../../types/otlp";
16
16
 
17
17
  function getPageViewMeta(url?: URL): PageViewMeta {
18
18
  if (!url) return {};
@@ -20,32 +20,41 @@ function getPageViewMeta(url?: URL): PageViewMeta {
20
20
  return vars.pageViewInstrumentation.generateMetadata?.(url) ?? {};
21
21
  }
22
22
 
23
- export function transmitPageViewEvent(timeUnixNano: string, url?: URL, virtual?: boolean, replaced?: boolean) {
24
- const meta = getPageViewMeta(url);
23
+ type BuildPageViewLogOptions = {
24
+ timeUnixNano: string;
25
+ url?: URL;
26
+ title?: string;
27
+ metaAttributes?: Record<string, AttributeValueType | AnyValue>;
28
+ customAttributes?: Record<string, AttributeValueType | AnyValue>;
29
+ pageViewType: (typeof PAGE_VIEW_TYPE_VALUES)[keyof typeof PAGE_VIEW_TYPE_VALUES];
30
+ changeState?: (typeof PAGE_VIEW_CHANGE_STATE_VALUES)[keyof typeof PAGE_VIEW_CHANGE_STATE_VALUES];
31
+ };
25
32
 
33
+ function buildAndSendPageViewLog(opts: BuildPageViewLogOptions) {
26
34
  const attributes: KeyValue[] = [];
27
35
  addAttribute(attributes, EVENT_NAME, EVENT_NAMES.PAGE_VIEW);
28
36
 
29
- if (meta.attributes) {
30
- Object.entries(meta.attributes).forEach(([key, value]) => addAttribute(attributes, key, value));
37
+ if (opts.metaAttributes) {
38
+ Object.entries(opts.metaAttributes).forEach(([key, value]) => addAttribute(attributes, key, value));
31
39
  }
32
- addCommonAttributes(attributes, { url });
40
+ addCommonAttributes(attributes, { url: opts.url });
41
+
42
+ // Add custom attributes last to allow overrides
43
+ addAttributes(attributes, opts.customAttributes);
33
44
 
34
45
  const bodyAttributes: KeyValue[] = [];
35
- addAttribute(bodyAttributes, "title", meta.title ?? doc?.title ?? NO_VALUE_FALLBACK);
46
+ addAttribute(bodyAttributes, "title", opts.title ?? doc?.title ?? NO_VALUE_FALLBACK);
36
47
  if (doc?.referrer) {
37
48
  addAttribute(bodyAttributes, "referrer", doc.referrer);
38
49
  }
39
50
 
40
- addAttribute(bodyAttributes, PAGE_VIEW_TYPE, virtual ? PAGE_VIEW_TYPE_VALUES.VIRTUAL : PAGE_VIEW_TYPE_VALUES.INITIAL);
41
- addAttribute(
42
- bodyAttributes,
43
- PAGE_VIEW_CHANGE_STATE,
44
- replaced ? PAGE_VIEW_CHANGE_STATE_VALUES.REPLACE : PAGE_VIEW_CHANGE_STATE_VALUES.PUSH
45
- );
51
+ addAttribute(bodyAttributes, PAGE_VIEW_TYPE, opts.pageViewType);
52
+ if (opts.changeState) {
53
+ addAttribute(bodyAttributes, PAGE_VIEW_CHANGE_STATE, opts.changeState);
54
+ }
46
55
 
47
56
  const log: LogRecord = {
48
- timeUnixNano: timeUnixNano,
57
+ timeUnixNano: opts.timeUnixNano,
49
58
  attributes: attributes,
50
59
  severityNumber: LOG_SEVERITIES.INFO,
51
60
  severityText: "INFO",
@@ -64,3 +73,42 @@ export function transmitPageViewEvent(timeUnixNano: string, url?: URL, virtual?:
64
73
 
65
74
  sendLog(log);
66
75
  }
76
+
77
+ export function transmitPageViewEvent(timeUnixNano: string, url?: URL, virtual?: boolean, replaced?: boolean) {
78
+ const meta = getPageViewMeta(url);
79
+
80
+ buildAndSendPageViewLog({
81
+ timeUnixNano,
82
+ url,
83
+ title: meta.title,
84
+ metaAttributes: meta.attributes,
85
+ pageViewType: virtual ? PAGE_VIEW_TYPE_VALUES.VIRTUAL : PAGE_VIEW_TYPE_VALUES.INITIAL,
86
+ changeState: replaced ? PAGE_VIEW_CHANGE_STATE_VALUES.REPLACE : PAGE_VIEW_CHANGE_STATE_VALUES.PUSH,
87
+ });
88
+ }
89
+
90
+ export type ManualPageViewOptions = {
91
+ timeUnixNano: string;
92
+ url?: URL;
93
+ title: string;
94
+ attributes?: Record<string, AttributeValueType | AnyValue>;
95
+ };
96
+
97
+ /**
98
+ * Emits a manually-triggered page view log, e.g. from the public `startView` API.
99
+ * Deliberately does NOT read `vars.pageViewInstrumentation.generateMetadata` and does NOT
100
+ * include a `change_state` body key (a manual view is neither a pushState nor replaceState).
101
+ * The emitted `type` is PAGE_VIEW_TYPE_VALUES.VIRTUAL, matching automatic virtual page views —
102
+ * manual views are deliberately indistinguishable from virtual ones downstream.
103
+ * Caller-provided attributes are added after the SDK-generated ones so they can override them,
104
+ * matching the sendEvent semantics.
105
+ */
106
+ export function transmitManualPageViewEvent(opts: ManualPageViewOptions) {
107
+ buildAndSendPageViewLog({
108
+ timeUnixNano: opts.timeUnixNano,
109
+ url: opts.url,
110
+ title: opts.title,
111
+ customAttributes: opts.attributes,
112
+ pageViewType: PAGE_VIEW_TYPE_VALUES.VIRTUAL,
113
+ });
114
+ }
@@ -0,0 +1,171 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { KeyValue, LogRecord } from "../../types/otlp";
3
+
4
+ vi.mock("../../transport", () => ({
5
+ sendLog: vi.fn(),
6
+ }));
7
+
8
+ import { sendLog } from "../../transport";
9
+ import { transmitPageViewEvent, transmitManualPageViewEvent } from "./event";
10
+ import { vars } from "../../vars";
11
+
12
+ const sendLogMock = sendLog as unknown as ReturnType<typeof vi.fn>;
13
+
14
+ describe("transmitPageViewEvent", () => {
15
+ beforeEach(() => {
16
+ sendLogMock.mockClear();
17
+ vars.pageViewInstrumentation = { trackVirtualPageViews: true, includeParts: [] };
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.clearAllMocks();
22
+ });
23
+
24
+ it("transmits an initial page view with type=INITIAL and change_state=pushState", () => {
25
+ transmitPageViewEvent("1700000000000000000", new URL("https://example.com/landing"));
26
+
27
+ expect(sendLogMock).toHaveBeenCalledTimes(1);
28
+ const log = sendLogMock.mock.calls[0]![0] as LogRecord;
29
+
30
+ expect(log.timeUnixNano).toBe("1700000000000000000");
31
+ expect(log.severityNumber).toBe(9);
32
+ expect(log.severityText).toBe("INFO");
33
+ expect(log.attributes).toEqual(
34
+ expect.arrayContaining([{ key: "event.name", value: { stringValue: "browser.page_view" } }])
35
+ );
36
+
37
+ const bodyValues = log.body?.kvlistValue?.values as KeyValue[];
38
+ expect(bodyValues).toEqual(
39
+ expect.arrayContaining([
40
+ { key: "type", value: { doubleValue: 0 } },
41
+ { key: "change_state", value: { stringValue: "pushState" } },
42
+ ])
43
+ );
44
+ });
45
+
46
+ it("transmits a virtual page view with type=VIRTUAL and change_state=replaceState when replaced", () => {
47
+ transmitPageViewEvent("1700000000000000000", new URL("https://example.com/settings"), true, true);
48
+
49
+ const log = sendLogMock.mock.calls[0]![0] as LogRecord;
50
+ const bodyValues = log.body?.kvlistValue?.values as KeyValue[];
51
+ expect(bodyValues).toEqual(
52
+ expect.arrayContaining([
53
+ { key: "type", value: { doubleValue: 1 } },
54
+ { key: "change_state", value: { stringValue: "replaceState" } },
55
+ ])
56
+ );
57
+ });
58
+
59
+ it("applies generateMetadata title and attributes when provided", () => {
60
+ vars.pageViewInstrumentation = {
61
+ trackVirtualPageViews: true,
62
+ includeParts: [],
63
+ generateMetadata: () => ({ title: "Custom Title", attributes: { "app.screen": "settings" } }),
64
+ };
65
+
66
+ transmitPageViewEvent("1700000000000000000", new URL("https://example.com/settings"), true);
67
+
68
+ const log = sendLogMock.mock.calls[0]![0] as LogRecord;
69
+ expect(log.attributes).toEqual(expect.arrayContaining([{ key: "app.screen", value: { stringValue: "settings" } }]));
70
+ const bodyValues = log.body?.kvlistValue?.values as KeyValue[];
71
+ expect(bodyValues).toEqual(expect.arrayContaining([{ key: "title", value: { stringValue: "Custom Title" } }]));
72
+ });
73
+ });
74
+
75
+ describe("transmitManualPageViewEvent", () => {
76
+ beforeEach(() => {
77
+ sendLogMock.mockClear();
78
+ vars.pageViewInstrumentation = {
79
+ trackVirtualPageViews: true,
80
+ includeParts: [],
81
+ generateMetadata: () => ({ title: "should not be used", attributes: { should_not_appear: true } }),
82
+ };
83
+ });
84
+
85
+ afterEach(() => {
86
+ vi.clearAllMocks();
87
+ vars.pageViewInstrumentation = { trackVirtualPageViews: true, includeParts: [] };
88
+ });
89
+
90
+ it("emits type=VIRTUAL and no change_state key", () => {
91
+ transmitManualPageViewEvent({ timeUnixNano: "1700000000000000000", title: "/settings" });
92
+
93
+ const log = sendLogMock.mock.calls[0]![0] as LogRecord;
94
+ const bodyValues = log.body?.kvlistValue?.values as KeyValue[];
95
+
96
+ expect(bodyValues).toEqual(
97
+ expect.arrayContaining([
98
+ { key: "type", value: { doubleValue: 1 } },
99
+ { key: "title", value: { stringValue: "/settings" } },
100
+ ])
101
+ );
102
+ expect(bodyValues.find((v) => v.key === "change_state")).toBeUndefined();
103
+ });
104
+
105
+ it("does not invoke vars.pageViewInstrumentation.generateMetadata", () => {
106
+ const generateMetadata = vi.fn().mockReturnValue({ title: "ignored" });
107
+ vars.pageViewInstrumentation = { trackVirtualPageViews: true, includeParts: [], generateMetadata };
108
+
109
+ transmitManualPageViewEvent({ timeUnixNano: "1700000000000000000", title: "/settings" });
110
+
111
+ expect(generateMetadata).not.toHaveBeenCalled();
112
+ const log = sendLogMock.mock.calls[0]![0] as LogRecord;
113
+ const bodyValues = log.body?.kvlistValue?.values as KeyValue[];
114
+ expect(bodyValues).toEqual(expect.arrayContaining([{ key: "title", value: { stringValue: "/settings" } }]));
115
+ });
116
+
117
+ it("merges custom attributes into signal attributes", () => {
118
+ transmitManualPageViewEvent({
119
+ timeUnixNano: "1700000000000000000",
120
+ title: "/settings",
121
+ attributes: { "app.screen": "settings", "app.tab_index": 2 },
122
+ });
123
+
124
+ const log = sendLogMock.mock.calls[0]![0] as LogRecord;
125
+ expect(log.attributes).toEqual(
126
+ expect.arrayContaining([
127
+ { key: "app.screen", value: { stringValue: "settings" } },
128
+ { key: "app.tab_index", value: { doubleValue: 2 } },
129
+ ])
130
+ );
131
+ });
132
+
133
+ it("adds custom attributes after SDK-generated ones so they can override", () => {
134
+ transmitManualPageViewEvent({
135
+ timeUnixNano: "1700000000000000000",
136
+ title: "/settings",
137
+ url: new URL("https://example.com/real-path"),
138
+ attributes: { "page.url.path": "/custom-path" },
139
+ });
140
+
141
+ const log = sendLogMock.mock.calls[0]![0] as LogRecord;
142
+ const pathAttributes = (log.attributes as KeyValue[]).filter((attr) => attr.key === "page.url.path");
143
+ expect(pathAttributes.length).toBeGreaterThan(1);
144
+ expect(pathAttributes[pathAttributes.length - 1]).toEqual({
145
+ key: "page.url.path",
146
+ value: { stringValue: "/custom-path" },
147
+ });
148
+ });
149
+
150
+ it("passes url through to page.url.* attributes when provided", () => {
151
+ transmitManualPageViewEvent({
152
+ timeUnixNano: "1700000000000000000",
153
+ title: "/settings",
154
+ url: new URL("https://example.com/settings"),
155
+ });
156
+
157
+ const log = sendLogMock.mock.calls[0]![0] as LogRecord;
158
+ expect(log.attributes).toEqual(
159
+ expect.arrayContaining([{ key: "page.url.path", value: { stringValue: "/settings" } }])
160
+ );
161
+ });
162
+
163
+ it("includes referrer the same way the auto path does", () => {
164
+ transmitManualPageViewEvent({ timeUnixNano: "1700000000000000000", title: "/settings" });
165
+
166
+ const log = sendLogMock.mock.calls[0]![0] as LogRecord;
167
+ const bodyValues = log.body?.kvlistValue?.values as KeyValue[];
168
+ // doc.referrer is empty string in jsdom by default, so "referrer" key should be absent
169
+ expect(bodyValues.find((v) => v.key === "referrer")).toBeUndefined();
170
+ });
171
+ });
@@ -2,7 +2,12 @@ import { AttributeValueType } from "../utils/otel";
2
2
  import { AnyValue } from "./otlp";
3
3
  import { Endpoint, Vars, PropagatorConfig } from "../vars";
4
4
 
5
- export type InstrumentationName = "@dash0/navigation" | "@dash0/web-vitals" | "@dash0/error" | "@dash0/fetch";
5
+ export type InstrumentationName =
6
+ | "@dash0/navigation"
7
+ | "@dash0/web-vitals"
8
+ | "@dash0/error"
9
+ | "@dash0/fetch"
10
+ | "@dash0/xhr";
6
11
 
7
12
  /**
8
13
  * VCS (version control) context describing the build the SDK is running
package/src/utils/wrap.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { debug } from "./debug";
1
+ import { debug, warn } from "./debug";
2
2
 
3
3
  const INSTRUMENTED_BY_DASH0_SYMBOL = Symbol.for("INSTRUMENTED_BY_DASH0");
4
4
 
@@ -29,6 +29,18 @@ export function wrap<ModuleType extends object, TargetNameType extends keyof Mod
29
29
  return;
30
30
  }
31
31
 
32
- markAsInstrumented(original);
33
- module[target] = wrapper(original);
32
+ try {
33
+ const wrapped = wrapper(original);
34
+ // Mark the replacement as well: a later wrap() call reads the replacement from module[target],
35
+ // so marking only the original would let repeated instrumentation wrap the wrapper.
36
+ markAsInstrumented(original);
37
+ if (wrapped) {
38
+ markAsInstrumented(wrapped);
39
+ }
40
+ module[target] = wrapped;
41
+ } catch (e) {
42
+ // Pages can lock properties via Object.defineProperty/Object.freeze -- failing to install
43
+ // one instrumentation must not throw out of init() and abort the remaining ones.
44
+ warn(`failed to instrument ${String(target)}, the page may have locked the property`, e);
45
+ }
34
46
  }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { wrap } from "./wrap";
3
+
4
+ describe("wrap", () => {
5
+ it("replaces the target with the wrapper's return value", () => {
6
+ const original = () => "original";
7
+ const module = { fn: original };
8
+
9
+ wrap(module, "fn", (orig) => () => `wrapped ${orig()}`);
10
+
11
+ expect(module.fn()).toBe("wrapped original");
12
+ });
13
+
14
+ it("does not wrap the same target twice", () => {
15
+ const module = { fn: () => "original" };
16
+ const wrapper = vi.fn((orig: () => string) => () => `wrapped ${orig()}`);
17
+
18
+ wrap(module, "fn", wrapper);
19
+ wrap(module, "fn", wrapper);
20
+
21
+ expect(wrapper).toHaveBeenCalledTimes(1);
22
+ expect(module.fn()).toBe("wrapped original");
23
+ });
24
+
25
+ it("skips undefined targets", () => {
26
+ const module: { fn?: () => string } = {};
27
+ const wrapper = vi.fn();
28
+
29
+ expect(() => wrap(module, "fn", wrapper)).not.toThrow();
30
+ expect(wrapper).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it("does not throw when the page locked the target property", () => {
34
+ const original = () => "original";
35
+ const module = {} as { fn: () => string };
36
+ Object.defineProperty(module, "fn", { value: original, writable: false, configurable: false });
37
+
38
+ expect(() => wrap(module, "fn", (orig) => () => `wrapped ${orig()}`)).not.toThrow();
39
+ expect(module.fn).toBe(original);
40
+ });
41
+ });