@dash0/sdk-web 0.24.0 → 0.26.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 +5 -0
- package/dist/dash0-session-recording.iife.js +2 -0
- package/dist/dash0-session-recording.iife.js.map +1 -0
- package/dist/dash0-session-recording.js +2 -0
- package/dist/dash0-session-recording.js.map +1 -0
- package/dist/dash0-session-recording.umd.cjs +2 -0
- package/dist/dash0-session-recording.umd.cjs.map +1 -0
- 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 +18 -1
- package/dist/modules/api/init_test.js +27 -0
- package/dist/modules/api/session-recording.js +38 -0
- package/dist/modules/api/session-recording_test.js +36 -0
- package/dist/modules/entrypoint/npm-package.js +1 -0
- package/dist/modules/entrypoint/script.js +3 -0
- package/dist/modules/entrypoint/session-recording-script.js +19 -0
- package/dist/modules/entrypoint/session-recording.js +11 -0
- package/dist/modules/instrumentations/session-recording/chunker.js +70 -0
- package/dist/modules/instrumentations/session-recording/chunker_test.js +125 -0
- package/dist/modules/instrumentations/session-recording/index.js +235 -0
- package/dist/modules/instrumentations/session-recording/index_test.js +293 -0
- package/dist/modules/instrumentations/session-recording/log.js +23 -0
- package/dist/modules/instrumentations/session-recording/log_test.js +46 -0
- package/dist/modules/semantic-conventions.js +7 -0
- package/dist/modules/transport/fetch.js +2 -2
- package/dist/modules/transport/fetch_test.js +25 -0
- package/dist/modules/transport/index.js +44 -11
- package/dist/modules/transport/index_test.js +73 -0
- package/dist/modules/types/session-recording.js +1 -0
- package/dist/modules/vars.js +11 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/api/session-recording.d.ts +25 -0
- package/dist/types/api/session-recording_test.d.ts +1 -0
- package/dist/types/entrypoint/npm-package.d.ts +2 -0
- package/dist/types/entrypoint/session-recording-script.d.ts +1 -0
- package/dist/types/entrypoint/session-recording.d.ts +5 -0
- package/dist/types/instrumentations/session-recording/chunker.d.ts +38 -0
- package/dist/types/instrumentations/session-recording/chunker_test.d.ts +1 -0
- package/dist/types/instrumentations/session-recording/index.d.ts +23 -0
- package/dist/types/instrumentations/session-recording/index_test.d.ts +1 -0
- package/dist/types/instrumentations/session-recording/log.d.ts +14 -0
- package/dist/types/instrumentations/session-recording/log_test.d.ts +1 -0
- package/dist/types/semantic-conventions.d.ts +6 -0
- package/dist/types/transport/fetch.d.ts +8 -1
- package/dist/types/transport/index.d.ts +10 -0
- package/dist/types/transport/index_test.d.ts +1 -0
- package/dist/types/types/options.d.ts +2 -2
- package/dist/types/types/session-recording.d.ts +128 -0
- package/dist/types/vars.d.ts +6 -0
- package/package.json +10 -2
- package/src/api/init.ts +19 -1
- package/src/api/init_test.ts +33 -1
- package/src/api/session-recording.ts +48 -0
- package/src/api/session-recording_test.ts +44 -0
- package/src/entrypoint/npm-package.ts +2 -0
- package/src/entrypoint/script.ts +3 -0
- package/src/entrypoint/session-recording-script.ts +23 -0
- package/src/entrypoint/session-recording.ts +13 -0
- package/src/instrumentations/session-recording/chunker.ts +118 -0
- package/src/instrumentations/session-recording/chunker_test.ts +147 -0
- package/src/instrumentations/session-recording/index.ts +248 -0
- package/src/instrumentations/session-recording/index_test.ts +358 -0
- package/src/instrumentations/session-recording/log.ts +48 -0
- package/src/instrumentations/session-recording/log_test.ts +60 -0
- package/src/semantic-conventions.ts +8 -0
- package/src/transport/fetch.ts +10 -2
- package/src/transport/fetch_test.ts +30 -0
- package/src/transport/index.ts +64 -25
- package/src/transport/index_test.ts +90 -0
- package/src/types/options.ts +3 -1
- package/src/types/session-recording.ts +144 -0
- package/src/vars.ts +18 -0
|
@@ -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
|
+
});
|
package/src/types/options.ts
CHANGED
|
@@ -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
|
};
|