@dash0/sdk-web 0.13.5 → 0.14.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/src/api/init.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Endpoint, Vars, vars } from "../vars";
1
+ import { vars } from "../vars";
2
2
  import {
3
3
  DEPLOYMENT_ENVIRONMENT_NAME,
4
4
  DEPLOYMENT_ID,
@@ -23,57 +23,12 @@ import {
23
23
  import { trackSessions } from "./session";
24
24
  import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
25
25
  import { startErrorInstrumentation } from "../instrumentations/errors";
26
- import { addAttribute, AttributeValueType } from "../utils/otel";
26
+ import { addAttribute } from "../utils/otel";
27
27
  import { instrumentFetch } from "../instrumentations/http/fetch";
28
28
  import { startNavigationInstrumentation } from "../instrumentations/navigation";
29
29
  import { merge } from "ts-deepmerge";
30
30
  import { initializeTabId } from "../utils/tab-id";
31
- import { AnyValue } from "../types/otlp";
32
-
33
- export type InitOptions = {
34
- serviceName: string;
35
- serviceVersion?: string;
36
- environment?: string;
37
- deploymentName?: string;
38
- deploymentId?: string;
39
-
40
- /**
41
- * Additional attributes to include with transmitted signals
42
- */
43
- additionalSignalAttributes?: Record<string, AttributeValueType | AnyValue>;
44
-
45
- /**
46
- * OTLP endpoints to which the generated telemetry should be sent to.
47
- */
48
- endpoint: Endpoint | Endpoint[];
49
-
50
- /**
51
- * The session inactivity timeout. Session inactivity is the maximum
52
- * allowed time to pass between two page loads before the session is considered
53
- * to be expired. Also think of cache time-to-idle configuration options.
54
- */
55
- sessionInactivityTimeoutMillis?: number;
56
-
57
- /**
58
- * The default session termination timeout. Session termination is the maximum
59
- * allowed time to pass since session start before the session is considered
60
- * to be expired. Also think of cache time-to-live configuration options.
61
- */
62
- sessionTerminationTimeoutMillis?: number;
63
- } & Partial<
64
- Pick<
65
- Vars,
66
- | "ignoreUrls"
67
- | "ignoreErrorMessages"
68
- | "wrapEventHandlers"
69
- | "wrapTimers"
70
- | "propagateTraceHeadersCorsURLs"
71
- | "maxWaitForResourceTimingsMillis"
72
- | "maxToleranceForResourceTimingsMillis"
73
- | "headersToCapture"
74
- | "pageViewInstrumentation"
75
- >
76
- >;
31
+ import { InitOptions, InstrumentationName } from "../types/options";
77
32
 
78
33
  let hasBeenInitialised: boolean = false;
79
34
 
@@ -112,6 +67,7 @@ export function init(opts: InitOptions) {
112
67
  "maxWaitForResourceTimingsMillis",
113
68
  "maxToleranceForResourceTimingsMillis",
114
69
  "headersToCapture",
70
+ "urlAttributeScrubber",
115
71
  "pageViewInstrumentation",
116
72
  ])
117
73
  )
@@ -121,10 +77,19 @@ export function init(opts: InitOptions) {
121
77
  initializeSignalAttributes(opts);
122
78
  initializeTabId();
123
79
  trackSessions(opts.sessionInactivityTimeoutMillis, opts.sessionTerminationTimeoutMillis);
124
- startNavigationInstrumentation();
125
- startWebVitalsInstrumentation();
126
- startErrorInstrumentation();
127
- instrumentFetch();
80
+
81
+ if (isInstrumentationEnabled("@dash0/navigation", opts)) {
82
+ startNavigationInstrumentation();
83
+ }
84
+ if (isInstrumentationEnabled("@dash0/web-vitals", opts)) {
85
+ startWebVitalsInstrumentation();
86
+ }
87
+ if (isInstrumentationEnabled("@dash0/error", opts)) {
88
+ startErrorInstrumentation();
89
+ }
90
+ if (isInstrumentationEnabled("@dash0/fetch", opts)) {
91
+ instrumentFetch();
92
+ }
128
93
 
129
94
  hasBeenInitialised = true;
130
95
  }
@@ -219,3 +184,11 @@ function detectDeploymentId(opts: InitOptions): string | undefined {
219
184
  return undefined;
220
185
  }
221
186
  }
187
+
188
+ function isInstrumentationEnabled(name: InstrumentationName, opts: InitOptions): boolean {
189
+ const instrumentations = opts.enabledInstrumentations;
190
+
191
+ if (!instrumentations) return true;
192
+
193
+ return instrumentations.includes(name);
194
+ }
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { InitOptions, InstrumentationName } from "../types/options";
3
+
4
+ // Mock all the instrumentation modules
5
+ vi.mock("../instrumentations/web-vitals", () => ({
6
+ startWebVitalsInstrumentation: vi.fn(),
7
+ }));
8
+
9
+ vi.mock("../instrumentations/errors", () => ({
10
+ startErrorInstrumentation: vi.fn(),
11
+ }));
12
+
13
+ vi.mock("../instrumentations/http/fetch", () => ({
14
+ instrumentFetch: vi.fn(),
15
+ }));
16
+
17
+ vi.mock("../instrumentations/navigation", () => ({
18
+ startNavigationInstrumentation: vi.fn(),
19
+ }));
20
+
21
+ import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
22
+ import { startErrorInstrumentation } from "../instrumentations/errors";
23
+ import { instrumentFetch } from "../instrumentations/http/fetch";
24
+ import { startNavigationInstrumentation } from "../instrumentations/navigation";
25
+
26
+ describe("init", () => {
27
+ const baseOptions: InitOptions = {
28
+ serviceName: "test-service",
29
+ endpoint: { url: "https://test-endpoint.com", authToken: "invalid" },
30
+ };
31
+
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ // Reset the hasBeenInitialised flag by re-importing the module
35
+ vi.resetModules();
36
+ });
37
+
38
+ afterEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ describe("instrumentation enablement", () => {
43
+ it("should enable all instrumentations when enabledInstrumentations is undefined", async () => {
44
+ const { init } = await import("./init");
45
+
46
+ init({
47
+ ...baseOptions,
48
+ enabledInstrumentations: undefined,
49
+ });
50
+
51
+ expect(startNavigationInstrumentation).toHaveBeenCalled();
52
+ expect(startWebVitalsInstrumentation).toHaveBeenCalled();
53
+ expect(startErrorInstrumentation).toHaveBeenCalled();
54
+ expect(instrumentFetch).toHaveBeenCalled();
55
+ });
56
+
57
+ const instrumentations: InstrumentationName[] = [
58
+ "@dash0/navigation",
59
+ "@dash0/web-vitals",
60
+ "@dash0/error",
61
+ "@dash0/fetch",
62
+ ];
63
+ const instrumentationMocks = {
64
+ "@dash0/navigation": startNavigationInstrumentation,
65
+ "@dash0/web-vitals": startWebVitalsInstrumentation,
66
+ "@dash0/error": startErrorInstrumentation,
67
+ "@dash0/fetch": instrumentFetch,
68
+ };
69
+
70
+ instrumentations.forEach((instrumentation) => {
71
+ it(`should enable ${instrumentation} instrumentation when present in enabledInstrumentations array`, async () => {
72
+ const { init } = await import("./init");
73
+
74
+ init({
75
+ ...baseOptions,
76
+ enabledInstrumentations: [instrumentation],
77
+ });
78
+
79
+ expect(instrumentationMocks[instrumentation]).toHaveBeenCalled();
80
+
81
+ // Check that other instrumentations are not called
82
+ Object.entries(instrumentationMocks).forEach(([name, mock]) => {
83
+ if (name !== instrumentation) {
84
+ expect(mock).not.toHaveBeenCalled();
85
+ }
86
+ });
87
+ });
88
+
89
+ it(`should not enable ${instrumentation} instrumentation when not present in enabledInstrumentations array`, async () => {
90
+ const { init } = await import("./init");
91
+
92
+ const otherInstrumentations = instrumentations.filter((i) => i !== instrumentation);
93
+
94
+ init({
95
+ ...baseOptions,
96
+ enabledInstrumentations: otherInstrumentations,
97
+ });
98
+
99
+ expect(instrumentationMocks[instrumentation]).not.toHaveBeenCalled();
100
+
101
+ // Check that other instrumentations are called
102
+ otherInstrumentations.forEach((name) => {
103
+ expect(instrumentationMocks[name]).toHaveBeenCalled();
104
+ });
105
+ });
106
+ });
107
+ });
108
+ });
@@ -1,7 +1,19 @@
1
1
  import { addAttribute, AttrPrefix, withPrefix } from "../utils/otel";
2
2
  import { URL_DOMAIN, URL_FRAGMENT, URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME } from "../semantic-conventions";
3
- import { parseUrl } from "../utils";
3
+ import { identity, parseUrl } from "../utils";
4
4
  import { KeyValue } from "../types/otlp";
5
+ import { vars } from "../vars";
6
+
7
+ export type UrlAttributeRecord = {
8
+ [URL_FULL]: string;
9
+ [URL_PATH]?: string | undefined;
10
+ [URL_DOMAIN]?: string | undefined;
11
+ [URL_SCHEME]?: string | undefined;
12
+ [URL_FRAGMENT]?: string | undefined;
13
+ [URL_QUERY]?: string | undefined;
14
+ };
15
+
16
+ export type UrlAttributeScrubber = (attr: UrlAttributeRecord) => UrlAttributeRecord;
5
17
 
6
18
  export function addUrlAttributes(attributes: KeyValue[], url: string | URL, prefix?: AttrPrefix) {
7
19
  const applyPrefix = withPrefix(prefix);
@@ -11,17 +23,27 @@ export function addUrlAttributes(attributes: KeyValue[], url: string | URL, pref
11
23
  if (parsed.username) parsed.username = "REDACTED";
12
24
  if (parsed.password) parsed.password = "REDACTED";
13
25
 
14
- addAttribute(attributes, applyPrefix(URL_FULL), parsed.href);
15
- addAttribute(attributes, applyPrefix(URL_PATH), parsed.pathname);
16
- addAttribute(attributes, applyPrefix(URL_DOMAIN), parsed.hostname);
17
- addAttribute(attributes, applyPrefix(URL_SCHEME), parsed.protocol.replace(":", ""));
18
- if (parsed.hash) {
19
- addAttribute(attributes, applyPrefix(URL_FRAGMENT), parsed.hash.replace("#", ""));
20
- }
21
- if (parsed.search) {
22
- addAttribute(attributes, applyPrefix(URL_QUERY), parsed.search.replace("?", ""));
23
- }
26
+ const attrs = vars.urlAttributeScrubber({
27
+ [URL_FULL]: parsed.href,
28
+ [URL_PATH]: parsed.pathname,
29
+ [URL_DOMAIN]: parsed.hostname,
30
+ [URL_SCHEME]: parsed.protocol.replace(":", ""),
31
+ [URL_FRAGMENT]: parsed.hash ? parsed.hash.replace("#", "") : undefined,
32
+ [URL_QUERY]: parsed.search ? parsed.search.replace("?", "") : undefined,
33
+ });
34
+
35
+ Object.entries(attrs).forEach(([key, value]) => {
36
+ if (value !== undefined) {
37
+ addAttribute(attributes, applyPrefix(key), value);
38
+ }
39
+ });
24
40
  } catch (_e) {
25
- addAttribute(attributes, applyPrefix(URL_FULL), String(url));
41
+ // This is fallback handling in case the url failed to parse or the user defined attribute scrubber behaved unexpectedly
42
+ // If the user did not attempt scrubbing we'll supply the full url for debugging purposes, otherwise we drop all url
43
+ // attributes
44
+ if (vars.urlAttributeScrubber === identity) {
45
+ // `identity` is the default assignment for this option
46
+ addAttribute(attributes, applyPrefix(URL_FULL), String(url));
47
+ }
26
48
  }
27
49
  }