@dash0/sdk-web 0.23.0 → 0.25.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 (91) hide show
  1. package/README.md +5 -0
  2. package/dist/dash0-session-recording.iife.js +2 -0
  3. package/dist/dash0-session-recording.iife.js.map +1 -0
  4. package/dist/dash0-session-recording.js +2 -0
  5. package/dist/dash0-session-recording.js.map +1 -0
  6. package/dist/dash0-session-recording.umd.cjs +2 -0
  7. package/dist/dash0-session-recording.umd.cjs.map +1 -0
  8. package/dist/dash0.iife.js +1 -1
  9. package/dist/dash0.iife.js.map +1 -1
  10. package/dist/dash0.js +1 -1
  11. package/dist/dash0.js.map +1 -1
  12. package/dist/dash0.umd.cjs +1 -1
  13. package/dist/dash0.umd.cjs.map +1 -1
  14. package/dist/modules/api/init.js +18 -1
  15. package/dist/modules/api/init_test.js +27 -0
  16. package/dist/modules/api/session-recording.js +30 -0
  17. package/dist/modules/api/session-recording_test.js +36 -0
  18. package/dist/modules/api/start-view.js +47 -0
  19. package/dist/modules/api/start-view_test.js +91 -0
  20. package/dist/modules/entrypoint/npm-package.js +2 -0
  21. package/dist/modules/entrypoint/npm-package_test.js +7 -0
  22. package/dist/modules/entrypoint/script.js +5 -0
  23. package/dist/modules/entrypoint/session-recording-script.js +19 -0
  24. package/dist/modules/entrypoint/session-recording.js +11 -0
  25. package/dist/modules/instrumentations/navigation/event.js +42 -10
  26. package/dist/modules/instrumentations/navigation/event_test.js +128 -0
  27. package/dist/modules/instrumentations/session-recording/chunker.js +70 -0
  28. package/dist/modules/instrumentations/session-recording/chunker_test.js +125 -0
  29. package/dist/modules/instrumentations/session-recording/index.js +165 -0
  30. package/dist/modules/instrumentations/session-recording/index_test.js +230 -0
  31. package/dist/modules/instrumentations/session-recording/log.js +23 -0
  32. package/dist/modules/instrumentations/session-recording/log_test.js +46 -0
  33. package/dist/modules/semantic-conventions.js +7 -0
  34. package/dist/modules/transport/fetch.js +2 -2
  35. package/dist/modules/transport/fetch_test.js +25 -0
  36. package/dist/modules/transport/index.js +44 -11
  37. package/dist/modules/transport/index_test.js +73 -0
  38. package/dist/modules/types/session-recording.js +1 -0
  39. package/dist/modules/vars.js +11 -0
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/dist/types/api/session-recording.d.ts +17 -0
  42. package/dist/types/api/session-recording_test.d.ts +1 -0
  43. package/dist/types/api/start-view.d.ts +33 -0
  44. package/dist/types/api/start-view_test.d.ts +1 -0
  45. package/dist/types/entrypoint/npm-package.d.ts +4 -0
  46. package/dist/types/entrypoint/npm-package_test.d.ts +1 -0
  47. package/dist/types/entrypoint/session-recording-script.d.ts +1 -0
  48. package/dist/types/entrypoint/session-recording.d.ts +5 -0
  49. package/dist/types/instrumentations/navigation/event.d.ts +18 -0
  50. package/dist/types/instrumentations/navigation/event_test.d.ts +1 -0
  51. package/dist/types/instrumentations/session-recording/chunker.d.ts +38 -0
  52. package/dist/types/instrumentations/session-recording/chunker_test.d.ts +1 -0
  53. package/dist/types/instrumentations/session-recording/index.d.ts +23 -0
  54. package/dist/types/instrumentations/session-recording/index_test.d.ts +1 -0
  55. package/dist/types/instrumentations/session-recording/log.d.ts +14 -0
  56. package/dist/types/instrumentations/session-recording/log_test.d.ts +1 -0
  57. package/dist/types/semantic-conventions.d.ts +6 -0
  58. package/dist/types/transport/fetch.d.ts +8 -1
  59. package/dist/types/transport/index.d.ts +10 -0
  60. package/dist/types/transport/index_test.d.ts +1 -0
  61. package/dist/types/types/options.d.ts +2 -2
  62. package/dist/types/types/session-recording.d.ts +128 -0
  63. package/dist/types/vars.d.ts +6 -0
  64. package/package.json +11 -3
  65. package/src/api/init.ts +19 -1
  66. package/src/api/init_test.ts +33 -1
  67. package/src/api/session-recording.ts +40 -0
  68. package/src/api/session-recording_test.ts +44 -0
  69. package/src/api/start-view.ts +68 -0
  70. package/src/api/start-view_test.ts +125 -0
  71. package/src/entrypoint/npm-package.ts +4 -0
  72. package/src/entrypoint/npm-package_test.ts +8 -0
  73. package/src/entrypoint/script.ts +5 -0
  74. package/src/entrypoint/session-recording-script.ts +23 -0
  75. package/src/entrypoint/session-recording.ts +13 -0
  76. package/src/instrumentations/navigation/event.ts +63 -15
  77. package/src/instrumentations/navigation/event_test.ts +171 -0
  78. package/src/instrumentations/session-recording/chunker.ts +118 -0
  79. package/src/instrumentations/session-recording/chunker_test.ts +147 -0
  80. package/src/instrumentations/session-recording/index.ts +175 -0
  81. package/src/instrumentations/session-recording/index_test.ts +273 -0
  82. package/src/instrumentations/session-recording/log.ts +48 -0
  83. package/src/instrumentations/session-recording/log_test.ts +60 -0
  84. package/src/semantic-conventions.ts +8 -0
  85. package/src/transport/fetch.ts +10 -2
  86. package/src/transport/fetch_test.ts +30 -0
  87. package/src/transport/index.ts +64 -25
  88. package/src/transport/index_test.ts +90 -0
  89. package/src/types/options.ts +3 -1
  90. package/src/types/session-recording.ts +144 -0
  91. package/src/vars.ts +18 -0
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { buildSessionRecordingLog } from "./log";
3
+ import { Chunk } from "./chunker";
4
+
5
+ vi.mock("../../api/session", () => ({ sessionId: "aabbccdd11223344" }));
6
+
7
+ describe("session recording log record", () => {
8
+ const stream = {
9
+ recordingId: "0123456789abcdef0123456789abcdef",
10
+ traceId: "d04200aabbccdd112233440123456789",
11
+ spanId: "0123456789abcdef",
12
+ };
13
+
14
+ const chunk: Chunk = {
15
+ seq: 3,
16
+ body: '[{"type":2,"timestamp":1700000000000}]',
17
+ eventCount: 1,
18
+ hasSnapshot: true,
19
+ startTime: 1700000000000,
20
+ endTime: 1700000004500,
21
+ };
22
+
23
+ it("builds an INFO log record with the chunk body as a string", () => {
24
+ const log = buildSessionRecordingLog(stream, chunk);
25
+
26
+ expect(log.timeUnixNano).toBe("1700000000000000000");
27
+ expect(log.severityNumber).toBe(9);
28
+ expect(log.severityText).toBe("INFO");
29
+ expect(log.body).toEqual({ stringValue: chunk.body });
30
+ expect(log.traceId).toBe(stream.traceId);
31
+ expect(log.spanId).toBe(stream.spanId);
32
+ });
33
+
34
+ it("stamps the event name, the stream and the chunk metadata as attributes", () => {
35
+ const log = buildSessionRecordingLog(stream, chunk);
36
+
37
+ expect(log.attributes).toEqual(
38
+ expect.arrayContaining([
39
+ { key: "event.name", value: { stringValue: "browser.session_recording" } },
40
+ { key: "session.id", value: { stringValue: "aabbccdd11223344" } },
41
+ { key: "dash0.session_recording.id", value: { stringValue: stream.recordingId } },
42
+ { key: "dash0.session_recording.seq", value: { intValue: "3" } },
43
+ { key: "dash0.session_recording.event_count", value: { intValue: "1" } },
44
+ { key: "dash0.session_recording.has_snapshot", value: { boolValue: true } },
45
+ { key: "dash0.session_recording.end_time_unix_nano", value: { intValue: "1700000004500000000" } },
46
+ ])
47
+ );
48
+ });
49
+
50
+ it("keeps the trace and span ids stable across chunks of one stream", () => {
51
+ const first = buildSessionRecordingLog(stream, chunk);
52
+ const second = buildSessionRecordingLog(stream, { ...chunk, seq: 4, hasSnapshot: false });
53
+
54
+ expect(second.traceId).toBe(first.traceId);
55
+ expect(second.spanId).toBe(first.spanId);
56
+ expect(second.attributes).toEqual(
57
+ expect.arrayContaining([{ key: "dash0.session_recording.has_snapshot", value: { boolValue: false } }])
58
+ );
59
+ });
60
+ });
@@ -29,6 +29,13 @@ export const WINDOW_HEIGHT = "browser.window.height";
29
29
  export const NETWORK_CONNECTION_TYPE = "network.connection.subtype";
30
30
  export const EXCEPTION_COMPONENT_STACK = "exception.component_stack";
31
31
 
32
+ // Session Recording Attribute Keys
33
+ export const SESSION_RECORDING_ID = "dash0.session_recording.id";
34
+ export const SESSION_RECORDING_SEQ = "dash0.session_recording.seq";
35
+ export const SESSION_RECORDING_EVENT_COUNT = "dash0.session_recording.event_count";
36
+ export const SESSION_RECORDING_HAS_SNAPSHOT = "dash0.session_recording.has_snapshot";
37
+ export const SESSION_RECORDING_END_TIME_UNIX_NANO = "dash0.session_recording.end_time_unix_nano";
38
+
32
39
  // User Attribute Keys
33
40
  export const USER_ID = "user.id";
34
41
  export const USER_NAME = "user.name";
@@ -68,6 +75,7 @@ export const EVENT_NAMES = {
68
75
  NAVIGATION_TIMING: "browser.navigation_timing",
69
76
  WEB_VITAL: "browser.web_vital",
70
77
  ERROR: "browser.error",
78
+ SESSION_RECORDING: "browser.session_recording",
71
79
  };
72
80
  export const SPAN_EVENT_NAME_EXCEPTION = "exception";
73
81
 
@@ -9,7 +9,15 @@ const BEACON_BODY_SIZE_LIMIT = 60000;
9
9
  let pendingBodySize = 0;
10
10
  let pendingRequestCount = 0;
11
11
 
12
- export async function send(path: string, body: unknown): Promise<void> {
12
+ export type SendOptions = {
13
+ /**
14
+ * Compress this request regardless of `vars.enableTransportCompression`. Used for payloads that are
15
+ * large and highly compressible, such as session recording chunks.
16
+ */
17
+ compress?: boolean;
18
+ };
19
+
20
+ export async function send(path: string, body: unknown, opts?: SendOptions): Promise<void> {
13
21
  debug("Transmitting telemetry to endpoints", body);
14
22
 
15
23
  const jsonString = JSON.stringify(body);
@@ -18,7 +26,7 @@ export async function send(path: string, body: unknown): Promise<void> {
18
26
  let isCompressed = false;
19
27
 
20
28
  // Try to compress if supported
21
- if (typeof CompressionStream !== "undefined" && vars.enableTransportCompression) {
29
+ if (typeof CompressionStream !== "undefined" && (vars.enableTransportCompression || opts?.compress)) {
22
30
  requestBody = await compressWithGzip(jsonString);
23
31
  byteLength = requestBody.byteLength;
24
32
  isCompressed = true;
@@ -334,4 +334,34 @@ describe("fetch transport keepalive handling", () => {
334
334
  expect(call[1].keepalive).toBe(true);
335
335
  });
336
336
  });
337
+
338
+ describe("compress option", () => {
339
+ it("gzips the body when opts.compress is set even though enableTransportCompression is off", async () => {
340
+ // jsdom's Blob has no stream(); use Node's Blob, which CompressionStream can consume.
341
+ const { Blob: NodeBlob } = await import("node:buffer");
342
+ vi.stubGlobal("Blob", NodeBlob);
343
+ vars.enableTransportCompression = false;
344
+
345
+ try {
346
+ await send("/v1/logs", { data: "x".repeat(1000) }, { compress: true });
347
+ } finally {
348
+ vi.unstubAllGlobals();
349
+ }
350
+
351
+ expect(fetchMock).toHaveBeenCalledTimes(1);
352
+ const init = fetchMock.mock.calls[0]![1];
353
+ expect(init.headers["Content-Encoding"]).toBe("gzip");
354
+ expect(init.body).toBeInstanceOf(ArrayBuffer);
355
+ });
356
+
357
+ it("does not gzip without opts.compress when enableTransportCompression is off", async () => {
358
+ vars.enableTransportCompression = false;
359
+
360
+ await send("/v1/logs", { data: "x".repeat(1000) });
361
+
362
+ const init = fetchMock.mock.calls[0]![1];
363
+ expect(init.headers["Content-Encoding"]).toBeUndefined();
364
+ expect(typeof init.body).toBe("string");
365
+ });
366
+ });
337
367
  });
@@ -1,5 +1,5 @@
1
1
  import { newBatcher } from "./batcher";
2
- import { send } from "./fetch";
2
+ import { send, SendOptions } from "./fetch";
3
3
  import { vars } from "../vars";
4
4
  import { debug, error, createRateLimiter } from "../utils";
5
5
  import { ExportLogsServiceRequest, ExportTraceServiceRequest, LogRecord, Span } from "../types/otlp";
@@ -7,18 +7,34 @@ import { ExportLogsServiceRequest, ExportTraceServiceRequest, LogRecord, Span }
7
7
  const logBatcher = newBatcher<LogRecord>(sendLogs);
8
8
  const spanBatcher = newBatcher<Span>(sendSpans);
9
9
 
10
- let rateLimiter: (() => boolean) | undefined;
10
+ /**
11
+ * Returns an `isRateLimited()` check whose limiter is created on first use, so the module can be imported
12
+ * without touching timers.
13
+ */
14
+ function lazyRateLimiter(opts: Parameters<typeof createRateLimiter>[0]): () => boolean {
15
+ let limiter: (() => boolean) | undefined;
16
+ return () => {
17
+ if (!limiter) {
18
+ limiter = createRateLimiter(opts);
19
+ }
20
+ return limiter();
21
+ };
22
+ }
11
23
 
12
- function isRateLimited() {
13
- if (!rateLimiter) {
14
- rateLimiter = createRateLimiter({
15
- maxCallsPerTenMinutes: 4096,
16
- maxCallsPerTenSeconds: 128,
17
- });
18
- }
24
+ const isRateLimited = lazyRateLimiter({
25
+ maxCallsPerTenMinutes: 4096,
26
+ maxCallsPerTenSeconds: 128,
27
+ });
19
28
 
20
- return rateLimiter();
21
- }
29
+ // Session recording chunks get their own budget. They are large and periodic, and must neither consume the
30
+ // shared budget of spans and logs nor be starved by it. The budget is deliberately generous: every chunk
31
+ // depends on the ones before it, so a dropped chunk makes the replay unrenderable until the next full snapshot
32
+ // (`checkoutEveryNms`). At the default `chunkMaxBytes` of 48 KB, 64 chunks per 10 s allow a ~3 MB mutation
33
+ // burst (a heavy first render), and 512 per 10 min cap a runaway page at ~25 MB of uncompressed replay JSON.
34
+ const isSessionRecordingRateLimited = lazyRateLimiter({
35
+ maxCallsPerTenMinutes: 512,
36
+ maxCallsPerTenSeconds: 64,
37
+ });
22
38
 
23
39
  export function sendLog(log: LogRecord): void {
24
40
  if (!vars.isSessionSampled) return;
@@ -31,20 +47,43 @@ export function sendLog(log: LogRecord): void {
31
47
  logBatcher.send(log);
32
48
  }
33
49
 
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) => {
50
+ /**
51
+ * Transmits a session recording chunk as a single log record. Chunks bypass the log batcher: batching 15 chunks
52
+ * of up to `chunkMaxBytes` each would produce requests far beyond the keepalive body limit.
53
+ *
54
+ * Chunks are gzipped by default because replay JSON compresses roughly 8:1. Pass `compress: false` when the
55
+ * request must be issued synchronously, i.e. while the document is being unloaded: compression is asynchronous
56
+ * and the page may be gone before `fetch()` is ever called.
57
+ */
58
+ export function sendSessionRecordingChunk(log: LogRecord, opts?: SendOptions): void {
59
+ if (!vars.isSessionSampled) return;
60
+
61
+ if (isSessionRecordingRateLimited()) {
62
+ debug("Session recording rate limit. Will not send chunk.", log);
63
+ return;
64
+ }
65
+
66
+ sendLogs([log], { compress: opts?.compress ?? true });
67
+ }
68
+
69
+ function sendLogs(logs: LogRecord[], opts?: SendOptions): void {
70
+ send(
71
+ "/v1/logs",
72
+ {
73
+ resourceLogs: [
74
+ {
75
+ resource: vars.resource,
76
+ scopeLogs: [
77
+ {
78
+ scope: vars.scope,
79
+ logRecords: logs,
80
+ },
81
+ ],
82
+ },
83
+ ],
84
+ } satisfies ExportLogsServiceRequest,
85
+ opts
86
+ ).catch((err) => {
48
87
  error("Failed to transmit logs", err);
49
88
  });
50
89
  }
@@ -0,0 +1,90 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { LogRecord } from "../types/otlp";
3
+
4
+ vi.mock("./fetch", () => ({
5
+ send: vi.fn(() => Promise.resolve()),
6
+ }));
7
+
8
+ // The SDK's timer wrapper captures the window timers at import time, before vitest installs fake timers.
9
+ // Resolve the globals lazily so vi.useFakeTimers() drives the rate limiter's reset intervals.
10
+ vi.mock("../utils/timers", () => ({
11
+ setTimeout: (...args: Parameters<typeof globalThis.setTimeout>) => globalThis.setTimeout(...args),
12
+ clearTimeout: (...args: Parameters<typeof globalThis.clearTimeout>) => globalThis.clearTimeout(...args),
13
+ setInterval: (...args: Parameters<typeof globalThis.setInterval>) => globalThis.setInterval(...args),
14
+ clearInterval: (...args: Parameters<typeof globalThis.clearInterval>) => globalThis.clearInterval(...args),
15
+ }));
16
+
17
+ type Module = typeof import("./index");
18
+
19
+ function log(seq: number): LogRecord {
20
+ return {
21
+ timeUnixNano: "1",
22
+ severityNumber: 9,
23
+ severityText: "INFO",
24
+ body: { stringValue: "[]" },
25
+ attributes: [{ key: "dash0.session_recording.seq", value: { intValue: String(seq) } }],
26
+ };
27
+ }
28
+
29
+ describe("sendSessionRecordingChunk", () => {
30
+ let mod: Module;
31
+ let vars: typeof import("../vars").vars;
32
+ let send: ReturnType<typeof vi.fn>;
33
+
34
+ beforeEach(async () => {
35
+ vi.resetModules();
36
+ vi.useFakeTimers();
37
+ mod = await import("./index");
38
+ vars = (await import("../vars")).vars;
39
+ send = (await import("./fetch")).send as any;
40
+ send.mockClear();
41
+ vars.isSessionSampled = true;
42
+ });
43
+
44
+ afterEach(() => {
45
+ vi.useRealTimers();
46
+ });
47
+
48
+ it("sends each chunk as its own compressed /v1/logs request, bypassing the log batcher", () => {
49
+ mod.sendSessionRecordingChunk(log(0));
50
+ mod.sendSessionRecordingChunk(log(1));
51
+
52
+ expect(send).toHaveBeenCalledTimes(2);
53
+ for (const [path, body, opts] of send.mock.calls) {
54
+ expect(path).toBe("/v1/logs");
55
+ expect(body.resourceLogs[0].scopeLogs[0].logRecords).toHaveLength(1);
56
+ expect(opts).toEqual({ compress: true });
57
+ }
58
+ });
59
+
60
+ it("sends uncompressed when asked to, so the unload flush reaches fetch synchronously", () => {
61
+ mod.sendSessionRecordingChunk(log(0), { compress: false });
62
+
63
+ expect(send).toHaveBeenCalledTimes(1);
64
+ expect(send.mock.calls[0]![2]).toEqual({ compress: false });
65
+ });
66
+
67
+ it("does not send when the session is not sampled", () => {
68
+ vars.isSessionSampled = false;
69
+ mod.sendSessionRecordingChunk(log(0));
70
+ expect(send).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it("drops chunks beyond its own burst budget without touching the shared log budget", () => {
74
+ for (let i = 0; i < 70; i++) {
75
+ mod.sendSessionRecordingChunk(log(i));
76
+ }
77
+ expect(send).toHaveBeenCalledTimes(64);
78
+
79
+ // Regular logs are batched, not sent immediately; the point is that they are still accepted.
80
+ mod.sendLog(log(100));
81
+ vi.advanceTimersByTime(10_000);
82
+ const logPaths = send.mock.calls.slice(64).map((c) => c[0]);
83
+ expect(logPaths).toContain("/v1/logs");
84
+
85
+ // The ten-second window has reset, so recording chunks flow again.
86
+ send.mockClear();
87
+ mod.sendSessionRecordingChunk(log(71));
88
+ expect(send).toHaveBeenCalledTimes(1);
89
+ });
90
+ });
@@ -7,7 +7,8 @@ export type InstrumentationName =
7
7
  | "@dash0/web-vitals"
8
8
  | "@dash0/error"
9
9
  | "@dash0/fetch"
10
- | "@dash0/xhr";
10
+ | "@dash0/xhr"
11
+ | "@dash0/session-recording";
11
12
 
12
13
  /**
13
14
  * VCS (version control) context describing the build the SDK is running
@@ -141,6 +142,7 @@ export type InitOptions = {
141
142
  | "headersToCapture"
142
143
  | "urlAttributeScrubber"
143
144
  | "pageViewInstrumentation"
145
+ | "sessionRecording"
144
146
  | "enableTransportCompression"
145
147
  >
146
148
  >;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The subset of rrweb's `record()` option surface the SDK forwards. Declared structurally so the
3
+ * public API of `@dash0/sdk-web` does not depend on `@rrweb/types`; rrweb's `record` satisfies it.
4
+ */
5
+ export type SessionRecorderOptions = {
6
+ /**
7
+ * Called by the recorder for every rrweb event. `isCheckout` is true for the events that
8
+ * start a new full snapshot (see `checkoutEveryNms`).
9
+ */
10
+ emit: (event: SessionRecordingEvent, isCheckout?: boolean) => void;
11
+ checkoutEveryNms?: number;
12
+ maskAllInputs?: boolean;
13
+ maskTextClass?: string | RegExp;
14
+ maskTextSelector?: string;
15
+ maskInputFn?: (text: string, element: HTMLElement | null) => string;
16
+ maskTextFn?: (text: string, element: HTMLElement | null) => string;
17
+ blockClass?: string | RegExp;
18
+ blockSelector?: string;
19
+ ignoreClass?: string;
20
+ recordCanvas?: boolean;
21
+ collectFonts?: boolean;
22
+ inlineStylesheet?: boolean;
23
+ };
24
+
25
+ /**
26
+ * A function that starts recording and returns a function that stops it.
27
+ * `record` from `@rrweb/record` (re-exported by `@dash0/sdk-web/session-recording`) has this shape.
28
+ */
29
+ export type SessionRecorder = (options: SessionRecorderOptions) => (() => void) | undefined;
30
+
31
+ /**
32
+ * The shape of an rrweb event the SDK relies on. rrweb events carry more data, which the SDK
33
+ * forwards untouched inside the chunk body.
34
+ */
35
+ export type SessionRecordingEvent = {
36
+ /**
37
+ * rrweb EventType. 2 is FullSnapshot, 4 is Meta.
38
+ */
39
+ type: number;
40
+ /**
41
+ * Milliseconds since the unix epoch.
42
+ */
43
+ timestamp: number;
44
+ data?: unknown;
45
+ };
46
+
47
+ export type SessionRecordingSettings = {
48
+ /**
49
+ * The percentage of sessions for which a recording is captured. Must be a number between 0 and 100.
50
+ * The decision is deterministic per session ID and uses the same hash as `sessionSamplingRate`, so
51
+ * recorded sessions are always a subset of the sessions for which telemetry is transmitted.
52
+ *
53
+ * @default 100
54
+ */
55
+ samplingRate?: number;
56
+
57
+ /**
58
+ * Replace the value of every visible input, textarea and select with asterisks before it leaves the browser.
59
+ * `<input type="hidden">` values are element attributes and are not masked; use `blockSelector` for those.
60
+ * Set to `false` only when you know no form on the page accepts sensitive data.
61
+ *
62
+ * @default true
63
+ */
64
+ maskAllInputs?: boolean;
65
+
66
+ /**
67
+ * CSS selector for elements whose text content must be masked. Use `"*"` to mask all text on the page.
68
+ */
69
+ maskTextSelector?: string;
70
+
71
+ /**
72
+ * Elements with this class have their text content masked.
73
+ *
74
+ * @default "dash0-mask"
75
+ */
76
+ maskTextClass?: string | RegExp;
77
+
78
+ /**
79
+ * Elements with this class are not recorded at all. A placeholder with the same dimensions
80
+ * is shown in the replay instead.
81
+ *
82
+ * @default "dash0-block"
83
+ */
84
+ blockClass?: string | RegExp;
85
+
86
+ /**
87
+ * CSS selector for elements that are not recorded at all.
88
+ */
89
+ blockSelector?: string;
90
+
91
+ /**
92
+ * Custom function to mask input values. Receives the raw value and the element, and must return the masked value.
93
+ */
94
+ maskInputFn?: (text: string, element: HTMLElement | null) => string;
95
+
96
+ /**
97
+ * Custom function to mask text nodes. Receives the raw text and the parent element, and must return the masked text.
98
+ */
99
+ maskTextFn?: (text: string, element: HTMLElement | null) => string;
100
+
101
+ /**
102
+ * Record the content of canvas elements. This is expensive and off by default.
103
+ *
104
+ * @default false
105
+ */
106
+ recordCanvas?: boolean;
107
+
108
+ /**
109
+ * Collect fonts so the replay renders with the same typefaces. Adds payload size.
110
+ *
111
+ * @default false
112
+ */
113
+ collectFonts?: boolean;
114
+
115
+ /**
116
+ * The maximum serialized size of one chunk in bytes. When the buffered events reach this size, a chunk is
117
+ * transmitted. A single rrweb event larger than this (typically a full snapshot) is transmitted on its own.
118
+ *
119
+ * @default 48000
120
+ */
121
+ chunkMaxBytes?: number;
122
+
123
+ /**
124
+ * The maximum time buffered events wait before they are transmitted as a chunk.
125
+ *
126
+ * @default 5000
127
+ */
128
+ chunkMaxMillis?: number;
129
+
130
+ /**
131
+ * How often the recorder takes a new full snapshot of the DOM, in milliseconds. A replay can start
132
+ * from any chunk that contains a full snapshot.
133
+ *
134
+ * @default 300000
135
+ */
136
+ checkoutEveryNms?: number;
137
+
138
+ /**
139
+ * The recorder to use. Pass `recorder` from `@dash0/sdk-web/session-recording`. When omitted, the SDK waits for
140
+ * a recorder to be registered through `startSessionRecording(recorder)` or through the
141
+ * `dash0-session-recording.iife.js` script.
142
+ */
143
+ recorder?: SessionRecorder;
144
+ };
package/src/vars.ts CHANGED
@@ -2,6 +2,7 @@ import { AttributeValueType } from "./utils/otel";
2
2
  import { AnyValue, InstrumentationScope, KeyValue, Resource } from "./types/otlp";
3
3
  import { UrlAttributeScrubber } from "./attributes";
4
4
  import { identity } from "./utils";
5
+ import { SessionRecordingSettings } from "./types/session-recording";
5
6
 
6
7
  export type PropagatorType = "traceparent" | "xray";
7
8
 
@@ -166,6 +167,12 @@ export type Vars = {
166
167
 
167
168
  pageViewInstrumentation: PageViewInstrumentationSettings;
168
169
 
170
+ /**
171
+ * Session recording (replay) settings. Recording only starts when a recorder is provided, either through
172
+ * `sessionRecording.recorder`, `startSessionRecording(recorder)`, or the `dash0-session-recording.iife.js` script.
173
+ */
174
+ sessionRecording: SessionRecordingSettings;
175
+
169
176
  /**
170
177
  * Enables telemetry transport compression using gzip.
171
178
  * experimental - in rare cases causes Chrome to crash to use at your own risk.
@@ -203,6 +210,17 @@ export const vars: Vars = {
203
210
  trackVirtualPageViews: true,
204
211
  includeParts: [],
205
212
  },
213
+ sessionRecording: {
214
+ samplingRate: 100,
215
+ maskAllInputs: true,
216
+ maskTextClass: "dash0-mask",
217
+ blockClass: "dash0-block",
218
+ recordCanvas: false,
219
+ collectFonts: false,
220
+ chunkMaxBytes: 48000,
221
+ chunkMaxMillis: 5000,
222
+ checkoutEveryNms: 300000,
223
+ },
206
224
  enableTransportCompression: false,
207
225
  isSessionSampled: true,
208
226
  };