@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,23 @@
|
|
|
1
|
+
import { SessionRecorder } from "../../types/session-recording";
|
|
2
|
+
/**
|
|
3
|
+
* Global set by `dash0-session-recording.iife.js`. Read by `armSessionRecording()` and by
|
|
4
|
+
* `startSessionRecording()` when called without a recorder.
|
|
5
|
+
*/
|
|
6
|
+
export declare const GLOBAL_RECORDER_KEY = "dash0Recorder";
|
|
7
|
+
/**
|
|
8
|
+
* Makes a recorder available. Called from the public `startSessionRecording` API, which the
|
|
9
|
+
* `dash0-session-recording.iife.js` script and npm consumers use. Recording starts as soon as both a recorder
|
|
10
|
+
* is registered and `init()` has armed session recording, in either order.
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerSessionRecorder(r: SessionRecorder): void;
|
|
13
|
+
/**
|
|
14
|
+
* Called from `init()` once configuration is in place and the session is sampled.
|
|
15
|
+
*
|
|
16
|
+
* Recorder precedence: `sessionRecording.recorder` from the init options, then a recorder registered through
|
|
17
|
+
* `startSessionRecording(recorder)`, then `window.dash0Recorder`. The last one is set by
|
|
18
|
+
* `dash0-session-recording.iife.js`, and is the only handover that works when that script executes before the
|
|
19
|
+
* initializer snippet has defined the `dash0` command queue.
|
|
20
|
+
*/
|
|
21
|
+
export declare function armSessionRecording(): void;
|
|
22
|
+
export declare function stopSessionRecording(): void;
|
|
23
|
+
export declare function isSessionRecording(): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LogRecord } from "../../types/otlp";
|
|
2
|
+
import { Chunk } from "./chunker";
|
|
3
|
+
export type RecordingStream = {
|
|
4
|
+
/**
|
|
5
|
+
* Identifies one recorder run. All chunks of the run share it.
|
|
6
|
+
*/
|
|
7
|
+
recordingId: string;
|
|
8
|
+
/**
|
|
9
|
+
* Trace context shared by all chunks of the run. The trace ID embeds the session ID.
|
|
10
|
+
*/
|
|
11
|
+
traceId: string;
|
|
12
|
+
spanId: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function buildSessionRecordingLog(stream: RecordingStream, chunk: Chunk): LogRecord;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -23,6 +23,11 @@ export declare const WINDOW_WIDTH = "browser.window.width";
|
|
|
23
23
|
export declare const WINDOW_HEIGHT = "browser.window.height";
|
|
24
24
|
export declare const NETWORK_CONNECTION_TYPE = "network.connection.subtype";
|
|
25
25
|
export declare const EXCEPTION_COMPONENT_STACK = "exception.component_stack";
|
|
26
|
+
export declare const SESSION_RECORDING_ID = "dash0.session_recording.id";
|
|
27
|
+
export declare const SESSION_RECORDING_SEQ = "dash0.session_recording.seq";
|
|
28
|
+
export declare const SESSION_RECORDING_EVENT_COUNT = "dash0.session_recording.event_count";
|
|
29
|
+
export declare const SESSION_RECORDING_HAS_SNAPSHOT = "dash0.session_recording.has_snapshot";
|
|
30
|
+
export declare const SESSION_RECORDING_END_TIME_UNIX_NANO = "dash0.session_recording.end_time_unix_nano";
|
|
26
31
|
export declare const USER_ID = "user.id";
|
|
27
32
|
export declare const USER_NAME = "user.name";
|
|
28
33
|
export declare const USER_FULL_NAME = "user.full_name";
|
|
@@ -51,6 +56,7 @@ export declare const EVENT_NAMES: {
|
|
|
51
56
|
NAVIGATION_TIMING: string;
|
|
52
57
|
WEB_VITAL: string;
|
|
53
58
|
ERROR: string;
|
|
59
|
+
SESSION_RECORDING: string;
|
|
54
60
|
};
|
|
55
61
|
export declare const SPAN_EVENT_NAME_EXCEPTION = "exception";
|
|
56
62
|
export declare const LOG_SEVERITIES: {
|
|
@@ -1 +1,8 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type SendOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Compress this request regardless of `vars.enableTransportCompression`. Used for payloads that are
|
|
4
|
+
* large and highly compressible, such as session recording chunks.
|
|
5
|
+
*/
|
|
6
|
+
compress?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function send(path: string, body: unknown, opts?: SendOptions): Promise<void>;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { SendOptions } from "./fetch";
|
|
1
2
|
import { LogRecord, Span } from "../types/otlp";
|
|
2
3
|
export declare function sendLog(log: LogRecord): void;
|
|
4
|
+
/**
|
|
5
|
+
* Transmits a session recording chunk as a single log record. Chunks bypass the log batcher: batching 15 chunks
|
|
6
|
+
* of up to `chunkMaxBytes` each would produce requests far beyond the keepalive body limit.
|
|
7
|
+
*
|
|
8
|
+
* Chunks are gzipped by default because replay JSON compresses roughly 8:1. Pass `compress: false` when the
|
|
9
|
+
* request must be issued synchronously, i.e. while the document is being unloaded: compression is asynchronous
|
|
10
|
+
* and the page may be gone before `fetch()` is ever called.
|
|
11
|
+
*/
|
|
12
|
+
export declare function sendSessionRecordingChunk(log: LogRecord, opts?: SendOptions): void;
|
|
3
13
|
export declare function sendSpan(span: Span | undefined): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AttributeValueType } from "../utils/otel";
|
|
2
2
|
import { AnyValue } from "./otlp";
|
|
3
3
|
import { Endpoint, Vars, PropagatorConfig } from "../vars";
|
|
4
|
-
export type InstrumentationName = "@dash0/navigation" | "@dash0/web-vitals" | "@dash0/error" | "@dash0/fetch" | "@dash0/xhr";
|
|
4
|
+
export type InstrumentationName = "@dash0/navigation" | "@dash0/web-vitals" | "@dash0/error" | "@dash0/fetch" | "@dash0/xhr" | "@dash0/session-recording";
|
|
5
5
|
/**
|
|
6
6
|
* VCS (version control) context describing the build the SDK is running
|
|
7
7
|
* inside. Used both as the public manual-override shape on `InitOptions.vcs`
|
|
@@ -110,4 +110,4 @@ export type InitOptions = {
|
|
|
110
110
|
* Each propagator defines which header type to send for matching URLs.
|
|
111
111
|
*/
|
|
112
112
|
propagators?: PropagatorConfig[];
|
|
113
|
-
} & Partial<Pick<Vars, "ignoreUrls" | "ignoreErrorMessages" | "wrapEventHandlers" | "wrapTimers" | "propagateTraceHeadersCorsURLs" | "maxWaitForResourceTimingsMillis" | "maxToleranceForResourceTimingsMillis" | "headersToCapture" | "urlAttributeScrubber" | "pageViewInstrumentation" | "enableTransportCompression">>;
|
|
113
|
+
} & Partial<Pick<Vars, "ignoreUrls" | "ignoreErrorMessages" | "wrapEventHandlers" | "wrapTimers" | "propagateTraceHeadersCorsURLs" | "maxWaitForResourceTimingsMillis" | "maxToleranceForResourceTimingsMillis" | "headersToCapture" | "urlAttributeScrubber" | "pageViewInstrumentation" | "sessionRecording" | "enableTransportCompression">>;
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
* A function that starts recording and returns a function that stops it.
|
|
26
|
+
* `record` from `@rrweb/record` (re-exported by `@dash0/sdk-web/session-recording`) has this shape.
|
|
27
|
+
*/
|
|
28
|
+
export type SessionRecorder = (options: SessionRecorderOptions) => (() => void) | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The shape of an rrweb event the SDK relies on. rrweb events carry more data, which the SDK
|
|
31
|
+
* forwards untouched inside the chunk body.
|
|
32
|
+
*/
|
|
33
|
+
export type SessionRecordingEvent = {
|
|
34
|
+
/**
|
|
35
|
+
* rrweb EventType. 2 is FullSnapshot, 4 is Meta.
|
|
36
|
+
*/
|
|
37
|
+
type: number;
|
|
38
|
+
/**
|
|
39
|
+
* Milliseconds since the unix epoch.
|
|
40
|
+
*/
|
|
41
|
+
timestamp: number;
|
|
42
|
+
data?: unknown;
|
|
43
|
+
};
|
|
44
|
+
export type SessionRecordingSettings = {
|
|
45
|
+
/**
|
|
46
|
+
* The percentage of sessions for which a recording is captured. Must be a number between 0 and 100.
|
|
47
|
+
* The decision is deterministic per session ID and uses the same hash as `sessionSamplingRate`, so
|
|
48
|
+
* recorded sessions are always a subset of the sessions for which telemetry is transmitted.
|
|
49
|
+
*
|
|
50
|
+
* @default 100
|
|
51
|
+
*/
|
|
52
|
+
samplingRate?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Replace the value of every visible input, textarea and select with asterisks before it leaves the browser.
|
|
55
|
+
* `<input type="hidden">` values are element attributes and are not masked; use `blockSelector` for those.
|
|
56
|
+
* Set to `false` only when you know no form on the page accepts sensitive data.
|
|
57
|
+
*
|
|
58
|
+
* @default true
|
|
59
|
+
*/
|
|
60
|
+
maskAllInputs?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* CSS selector for elements whose text content must be masked. Use `"*"` to mask all text on the page.
|
|
63
|
+
*/
|
|
64
|
+
maskTextSelector?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Elements with this class have their text content masked.
|
|
67
|
+
*
|
|
68
|
+
* @default "dash0-mask"
|
|
69
|
+
*/
|
|
70
|
+
maskTextClass?: string | RegExp;
|
|
71
|
+
/**
|
|
72
|
+
* Elements with this class are not recorded at all. A placeholder with the same dimensions
|
|
73
|
+
* is shown in the replay instead.
|
|
74
|
+
*
|
|
75
|
+
* @default "dash0-block"
|
|
76
|
+
*/
|
|
77
|
+
blockClass?: string | RegExp;
|
|
78
|
+
/**
|
|
79
|
+
* CSS selector for elements that are not recorded at all.
|
|
80
|
+
*/
|
|
81
|
+
blockSelector?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Custom function to mask input values. Receives the raw value and the element, and must return the masked value.
|
|
84
|
+
*/
|
|
85
|
+
maskInputFn?: (text: string, element: HTMLElement | null) => string;
|
|
86
|
+
/**
|
|
87
|
+
* Custom function to mask text nodes. Receives the raw text and the parent element, and must return the masked text.
|
|
88
|
+
*/
|
|
89
|
+
maskTextFn?: (text: string, element: HTMLElement | null) => string;
|
|
90
|
+
/**
|
|
91
|
+
* Record the content of canvas elements. This is expensive and off by default.
|
|
92
|
+
*
|
|
93
|
+
* @default false
|
|
94
|
+
*/
|
|
95
|
+
recordCanvas?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Collect fonts so the replay renders with the same typefaces. Adds payload size.
|
|
98
|
+
*
|
|
99
|
+
* @default false
|
|
100
|
+
*/
|
|
101
|
+
collectFonts?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* The maximum serialized size of one chunk in bytes. When the buffered events reach this size, a chunk is
|
|
104
|
+
* transmitted. A single rrweb event larger than this (typically a full snapshot) is transmitted on its own.
|
|
105
|
+
*
|
|
106
|
+
* @default 48000
|
|
107
|
+
*/
|
|
108
|
+
chunkMaxBytes?: number;
|
|
109
|
+
/**
|
|
110
|
+
* The maximum time buffered events wait before they are transmitted as a chunk.
|
|
111
|
+
*
|
|
112
|
+
* @default 5000
|
|
113
|
+
*/
|
|
114
|
+
chunkMaxMillis?: number;
|
|
115
|
+
/**
|
|
116
|
+
* How often the recorder takes a new full snapshot of the DOM, in milliseconds. A replay can start
|
|
117
|
+
* from any chunk that contains a full snapshot.
|
|
118
|
+
*
|
|
119
|
+
* @default 300000
|
|
120
|
+
*/
|
|
121
|
+
checkoutEveryNms?: number;
|
|
122
|
+
/**
|
|
123
|
+
* The recorder to use. Pass `recorder` from `@dash0/sdk-web/session-recording`. When omitted, the SDK waits for
|
|
124
|
+
* a recorder to be registered through `startSessionRecording(recorder)` or through the
|
|
125
|
+
* `dash0-session-recording.iife.js` script.
|
|
126
|
+
*/
|
|
127
|
+
recorder?: SessionRecorder;
|
|
128
|
+
};
|
package/dist/types/vars.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AttributeValueType } from "./utils/otel";
|
|
2
2
|
import { AnyValue, InstrumentationScope, KeyValue, Resource } from "./types/otlp";
|
|
3
3
|
import { UrlAttributeScrubber } from "./attributes";
|
|
4
|
+
import { SessionRecordingSettings } from "./types/session-recording";
|
|
4
5
|
export type PropagatorType = "traceparent" | "xray";
|
|
5
6
|
export type PropagatorConfig = {
|
|
6
7
|
type: PropagatorType;
|
|
@@ -140,6 +141,11 @@ export type Vars = {
|
|
|
140
141
|
*/
|
|
141
142
|
urlAttributeScrubber: UrlAttributeScrubber;
|
|
142
143
|
pageViewInstrumentation: PageViewInstrumentationSettings;
|
|
144
|
+
/**
|
|
145
|
+
* Session recording (replay) settings. Recording only starts when a recorder is provided, either through
|
|
146
|
+
* `sessionRecording.recorder`, `startSessionRecording(recorder)`, or the `dash0-session-recording.iife.js` script.
|
|
147
|
+
*/
|
|
148
|
+
sessionRecording: SessionRecordingSettings;
|
|
143
149
|
/**
|
|
144
150
|
* Enables telemetry transport compression using gzip.
|
|
145
151
|
* experimental - in rare cases causes Chrome to crash to use at your own risk.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dash0/sdk-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Dash0's Web SDK to collect telemetry from end-users' web browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/dash0.umd.cjs",
|
|
@@ -11,14 +11,20 @@
|
|
|
11
11
|
"types": "./dist/types/entrypoint/npm-package.d.ts",
|
|
12
12
|
"require": "./dist/dash0.umd.cjs",
|
|
13
13
|
"default": "./dist/dash0.js"
|
|
14
|
+
},
|
|
15
|
+
"./session-recording": {
|
|
16
|
+
"types": "./dist/types/entrypoint/session-recording.d.ts",
|
|
17
|
+
"require": "./dist/dash0-session-recording.umd.cjs",
|
|
18
|
+
"default": "./dist/dash0-session-recording.js"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"types": "./dist/types/entrypoint/npm-package.d.ts",
|
|
17
22
|
"scripts": {
|
|
18
|
-
"build": "run-s build:clean build:typescript build:rollup build:stats",
|
|
23
|
+
"build": "run-s build:clean build:typescript build:rollup build:verify build:stats",
|
|
19
24
|
"build:clean": "rm -rf dist",
|
|
20
25
|
"build:typescript": "tsc -b",
|
|
21
26
|
"build:rollup": "rollup -c -m",
|
|
27
|
+
"build:verify": "node scripts/verify-bundles.mjs",
|
|
22
28
|
"build:stats": "echo \"\nFile Stats:\" && ls dist/*.js | xargs -I '%' bash -c 'echo \"%: $(./node_modules/.bin/gzip-size % --include-original) (gzip)\"'",
|
|
23
29
|
"prettier:all": "prettier . --write --cache --list-different",
|
|
24
30
|
"prettier:check": "prettier . --check",
|
|
@@ -29,6 +35,7 @@
|
|
|
29
35
|
"test:unit:watch": "vitest",
|
|
30
36
|
"test:e2e": "wdio run ./test/e2e/wdio.conf.ts",
|
|
31
37
|
"test:e2e:local": "pnpm run build && wdio run ./test/e2e/wdio.local.conf.ts",
|
|
38
|
+
"sink": "node scripts/otlp-sink.mjs",
|
|
32
39
|
"test:e2e:server": "SERVER_PORTS='8010,8011,8012' node test/e2e/server/index.mjs",
|
|
33
40
|
"test:e2e:tunnel": "dotenv -- bash -c './.lambdatest/v3/LT --user $LT_USERNAME --key $LT_ACCESS_KEY'",
|
|
34
41
|
"test:e2e:live": "run-p test:e2e:server test:e2e:tunnel",
|
|
@@ -71,6 +78,7 @@
|
|
|
71
78
|
"web-vitals": "^5.0.3"
|
|
72
79
|
},
|
|
73
80
|
"devDependencies": {
|
|
81
|
+
"@rrweb/record": "^2.1.1",
|
|
74
82
|
"@babel/core": "^7.26.10",
|
|
75
83
|
"@babel/preset-env": "^7.26.9",
|
|
76
84
|
"@release-it/conventional-changelog": "^10.0.1",
|
package/src/api/init.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { initializeTabId } from "../utils/tab-id";
|
|
|
35
35
|
import { InitOptions, InstrumentationName } from "../types/options";
|
|
36
36
|
import { BrowserBuildEnv, pickFirstString } from "./browser-env";
|
|
37
37
|
import { applyVcsResourceAttributes } from "./vcs";
|
|
38
|
+
import { armSessionRecording } from "../instrumentations/session-recording";
|
|
38
39
|
|
|
39
40
|
declare const process: { env?: BrowserBuildEnv } | undefined;
|
|
40
41
|
|
|
@@ -86,6 +87,7 @@ export function init(opts: InitOptions) {
|
|
|
86
87
|
"headersToCapture",
|
|
87
88
|
"urlAttributeScrubber",
|
|
88
89
|
"pageViewInstrumentation",
|
|
90
|
+
"sessionRecording",
|
|
89
91
|
"enableTransportCompression",
|
|
90
92
|
])
|
|
91
93
|
)
|
|
@@ -124,6 +126,9 @@ export function init(opts: InitOptions) {
|
|
|
124
126
|
if (isInstrumentationEnabled("@dash0/xhr", opts)) {
|
|
125
127
|
instrumentXhr();
|
|
126
128
|
}
|
|
129
|
+
if (isInstrumentationEnabled("@dash0/session-recording", opts)) {
|
|
130
|
+
armSessionRecording();
|
|
131
|
+
}
|
|
127
132
|
|
|
128
133
|
hasBeenInitialised = true;
|
|
129
134
|
}
|
|
@@ -303,7 +308,10 @@ function merge<T extends Record<string, unknown>>(target: T, source: Partial<T>)
|
|
|
303
308
|
dstVal !== null &&
|
|
304
309
|
!Array.isArray(dstVal)
|
|
305
310
|
) {
|
|
306
|
-
|
|
311
|
+
// Like the top-level rule above, an explicit `undefined` inside a nested object means "not provided" and
|
|
312
|
+
// must not erase the default. Otherwise `sessionRecording: { maskAllInputs: someUnsetFlag }` would
|
|
313
|
+
// silently turn input masking off.
|
|
314
|
+
result[key] = { ...dstVal, ...withoutUndefined(srcVal as Record<string, unknown>) } as T[keyof T];
|
|
307
315
|
} else {
|
|
308
316
|
result[key] = srcVal as T[keyof T];
|
|
309
317
|
}
|
|
@@ -311,3 +319,13 @@ function merge<T extends Record<string, unknown>>(target: T, source: Partial<T>)
|
|
|
311
319
|
}
|
|
312
320
|
return result;
|
|
313
321
|
}
|
|
322
|
+
|
|
323
|
+
function withoutUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
|
|
324
|
+
const result: Partial<T> = {};
|
|
325
|
+
for (const key of Object.keys(obj) as Array<keyof T>) {
|
|
326
|
+
if (obj[key] !== undefined) {
|
|
327
|
+
result[key] = obj[key];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
package/src/api/init_test.ts
CHANGED
|
@@ -38,6 +38,10 @@ vi.mock("../instrumentations/navigation", () => ({
|
|
|
38
38
|
startNavigationInstrumentation: vi.fn(),
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
+
vi.mock("../instrumentations/session-recording", () => ({
|
|
42
|
+
armSessionRecording: vi.fn(),
|
|
43
|
+
}));
|
|
44
|
+
|
|
41
45
|
// Mock the utils module to control loc.hostname
|
|
42
46
|
vi.mock("../utils", async () => {
|
|
43
47
|
const actual = await vi.importActual("../utils");
|
|
@@ -52,6 +56,7 @@ import { instrumentFetch } from "../instrumentations/http/fetch";
|
|
|
52
56
|
import { instrumentXhr } from "../instrumentations/http/xhr";
|
|
53
57
|
import { startNavigationInstrumentation } from "../instrumentations/navigation";
|
|
54
58
|
import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
|
|
59
|
+
import { armSessionRecording } from "../instrumentations/session-recording";
|
|
55
60
|
|
|
56
61
|
describe("init", () => {
|
|
57
62
|
const baseOptions: InitOptions = {
|
|
@@ -78,6 +83,31 @@ describe("init", () => {
|
|
|
78
83
|
vi.clearAllMocks();
|
|
79
84
|
});
|
|
80
85
|
|
|
86
|
+
describe("nested option merging", () => {
|
|
87
|
+
it("keeps nested defaults when an override is explicitly undefined", () => {
|
|
88
|
+
init({
|
|
89
|
+
...baseOptions,
|
|
90
|
+
sessionRecording: { maskAllInputs: undefined, chunkMaxMillis: 1000 },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(vars.sessionRecording.maskAllInputs).toBe(true);
|
|
94
|
+
expect(vars.sessionRecording.blockClass).toBe("dash0-block");
|
|
95
|
+
expect(vars.sessionRecording.chunkMaxMillis).toBe(1000);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("still lets an explicit false override a nested default", () => {
|
|
99
|
+
init({
|
|
100
|
+
...baseOptions,
|
|
101
|
+
sessionRecording: { maskAllInputs: false },
|
|
102
|
+
pageViewInstrumentation: { trackVirtualPageViews: false },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(vars.sessionRecording.maskAllInputs).toBe(false);
|
|
106
|
+
expect(vars.pageViewInstrumentation.trackVirtualPageViews).toBe(false);
|
|
107
|
+
expect(vars.pageViewInstrumentation.includeParts).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
81
111
|
describe("instrumentation enablement", () => {
|
|
82
112
|
it("should enable all instrumentations when enabledInstrumentations is undefined", async () => {
|
|
83
113
|
init({
|
|
@@ -98,13 +128,15 @@ describe("init", () => {
|
|
|
98
128
|
"@dash0/error",
|
|
99
129
|
"@dash0/fetch",
|
|
100
130
|
"@dash0/xhr",
|
|
131
|
+
"@dash0/session-recording",
|
|
101
132
|
];
|
|
102
|
-
const instrumentationMocks = {
|
|
133
|
+
const instrumentationMocks: Record<InstrumentationName, () => void> = {
|
|
103
134
|
"@dash0/navigation": startNavigationInstrumentation,
|
|
104
135
|
"@dash0/web-vitals": startWebVitalsInstrumentation,
|
|
105
136
|
"@dash0/error": startErrorInstrumentation,
|
|
106
137
|
"@dash0/fetch": instrumentFetch,
|
|
107
138
|
"@dash0/xhr": instrumentXhr,
|
|
139
|
+
"@dash0/session-recording": armSessionRecording,
|
|
108
140
|
};
|
|
109
141
|
|
|
110
142
|
instrumentations.forEach((instrumentation) => {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { debug, win } from "../utils";
|
|
2
|
+
import { SessionRecorder } from "../types/session-recording";
|
|
3
|
+
import {
|
|
4
|
+
GLOBAL_RECORDER_KEY,
|
|
5
|
+
registerSessionRecorder,
|
|
6
|
+
stopSessionRecording as stopRecording,
|
|
7
|
+
} from "../instrumentations/session-recording";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Starts session recording with the given recorder. Pass `recorder` from `@dash0/sdk-web/session-recording`.
|
|
11
|
+
* When `recorder` is omitted, the SDK looks for `window.dash0Recorder`, which the
|
|
12
|
+
* `dash0-session-recording.iife.js` script sets. Calling this is not required when that script is used: `init()`
|
|
13
|
+
* picks up `window.dash0Recorder` on its own, regardless of the order in which the scripts execute.
|
|
14
|
+
*
|
|
15
|
+
* Recording only starts once `init()` has run with a sampled session. It is safe to call this before `init()`;
|
|
16
|
+
* the recorder is kept and recording starts as soon as the SDK is initialized. The recording is transmitted
|
|
17
|
+
* as `browser.session_recording` log records that share one trace ID, which embeds the session ID.
|
|
18
|
+
*
|
|
19
|
+
* Only the visible document is recorded. A tab that is hidden — switched away from, or opened in the
|
|
20
|
+
* background — stops recording and flushes what it buffered, and starts a fresh recording when it is shown
|
|
21
|
+
* again. So a session is a sequence of recordings that do not overlap in time, which is what lets the whole
|
|
22
|
+
* session, tab switches included, be replayed as one.
|
|
23
|
+
*/
|
|
24
|
+
export function startSessionRecording(recorder?: SessionRecorder): void {
|
|
25
|
+
// The script entrypoint forwards dash0("startSessionRecording", ...) arguments without type checking,
|
|
26
|
+
// so malformed calls must degrade to a logged no-op instead of throwing. An uncaught throw here would
|
|
27
|
+
// abort the command-queue drain and drop all subsequently queued api calls.
|
|
28
|
+
const r = recorder ?? (win as any)?.[GLOBAL_RECORDER_KEY];
|
|
29
|
+
if (typeof r !== "function") {
|
|
30
|
+
debug(
|
|
31
|
+
"startSessionRecording requires a recorder. Import `recorder` from `@dash0/sdk-web/session-recording` or load dash0-session-recording.iife.js. Ignoring call."
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
registerSessionRecorder(r as SessionRecorder);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Stops the running session recording and transmits any buffered events. Calling this when no recording is
|
|
41
|
+
* running is a no-op.
|
|
42
|
+
*
|
|
43
|
+
* Unlike the automatic pause while a tab is hidden, this is final: recording does not resume when the tab
|
|
44
|
+
* becomes visible again. Call `startSessionRecording()` to record again.
|
|
45
|
+
*/
|
|
46
|
+
export function stopSessionRecording(): void {
|
|
47
|
+
stopRecording();
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("../instrumentations/session-recording", () => ({
|
|
4
|
+
GLOBAL_RECORDER_KEY: "dash0Recorder",
|
|
5
|
+
registerSessionRecorder: vi.fn(),
|
|
6
|
+
stopSessionRecording: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { registerSessionRecorder, stopSessionRecording as stopImpl } from "../instrumentations/session-recording";
|
|
10
|
+
import { startSessionRecording, stopSessionRecording } from "./session-recording";
|
|
11
|
+
import { win } from "../utils";
|
|
12
|
+
|
|
13
|
+
const globalObject = win as any;
|
|
14
|
+
|
|
15
|
+
describe("startSessionRecording api", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
delete globalObject.dash0Recorder;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("registers an explicitly passed recorder", () => {
|
|
22
|
+
const recorder = vi.fn();
|
|
23
|
+
startSessionRecording(recorder as any);
|
|
24
|
+
expect(registerSessionRecorder).toHaveBeenCalledWith(recorder);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("falls back to window.dash0Recorder", () => {
|
|
28
|
+
const recorder = vi.fn();
|
|
29
|
+
globalObject.dash0Recorder = recorder;
|
|
30
|
+
startSessionRecording();
|
|
31
|
+
expect(registerSessionRecorder).toHaveBeenCalledWith(recorder);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("ignores calls without a usable recorder instead of throwing", () => {
|
|
35
|
+
expect(() => startSessionRecording()).not.toThrow();
|
|
36
|
+
expect(() => startSessionRecording("nope" as any)).not.toThrow();
|
|
37
|
+
expect(registerSessionRecorder).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("delegates stop", () => {
|
|
41
|
+
stopSessionRecording();
|
|
42
|
+
expect(stopImpl).toHaveBeenCalledTimes(1);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -10,6 +10,7 @@ export * from "../api/log-level";
|
|
|
10
10
|
export { terminateSession } from "../api/session";
|
|
11
11
|
export { reportError } from "../api/report-error";
|
|
12
12
|
export { startView } from "../api/start-view";
|
|
13
|
+
export { startSessionRecording, stopSessionRecording } from "../api/session-recording";
|
|
13
14
|
|
|
14
15
|
// Additional utility types
|
|
15
16
|
export type { AttributeValueType } from "../utils/otel";
|
|
@@ -17,6 +18,7 @@ export type { AnyValue } from "../types/otlp";
|
|
|
17
18
|
export type { PageViewMeta, PropagatorConfig, PropagatorType } from "../vars";
|
|
18
19
|
export type { UrlAttributeScrubber, UrlAttributeRecord } from "../attributes/url";
|
|
19
20
|
export type { StartViewOptions } from "../api/start-view";
|
|
21
|
+
export type { SessionRecorder, SessionRecorderOptions, SessionRecordingSettings } from "../types/session-recording";
|
|
20
22
|
|
|
21
23
|
export function init(opts: InitOptions): void {
|
|
22
24
|
debug(`${INIT_MESSAGE} (via package)`);
|
package/src/entrypoint/script.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { addSignalAttribute, removeSignalAttribute } from "../api/attributes";
|
|
|
9
9
|
import { sendEvent } from "../api/events";
|
|
10
10
|
import { setActiveLogLevel } from "../api/log-level";
|
|
11
11
|
import { startView } from "../api/start-view";
|
|
12
|
+
import { startSessionRecording, stopSessionRecording } from "../api/session-recording";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* All the APIs exposed through the script tag via `dash0('{{api name}}')`
|
|
@@ -24,6 +25,8 @@ const scriptApis = {
|
|
|
24
25
|
setActiveLogLevel,
|
|
25
26
|
sendEvent,
|
|
26
27
|
startView,
|
|
28
|
+
startSessionRecording,
|
|
29
|
+
stopSessionRecording,
|
|
27
30
|
} as const;
|
|
28
31
|
|
|
29
32
|
type GlobalObject = {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script-tag entrypoint, built to `dist/dash0-session-recording.iife.js`.
|
|
3
|
+
*
|
|
4
|
+
* This bundle contains only rrweb's recorder. It must not import any SDK module (see eslint.config.js), so the
|
|
5
|
+
* main SDK bundle stays the single owner of configuration, session and transport state. It hands the recorder
|
|
6
|
+
* to the SDK in two ways, so it can be loaded in any order relative to the initializer snippet and `dash0.iife.js`:
|
|
7
|
+
*
|
|
8
|
+
* - `window.dash0Recorder`: picked up by the SDK when `init()` runs. Covers the case where this bundle executes
|
|
9
|
+
* before the initializer snippet has defined the `dash0` command queue.
|
|
10
|
+
* - `dash0("startSessionRecording", record)`: covers the case where the SDK is already initialized, or the
|
|
11
|
+
* command queue exists and will be drained on `init()`.
|
|
12
|
+
*/
|
|
13
|
+
/* eslint-disable no-restricted-globals */
|
|
14
|
+
import { record } from "@rrweb/record";
|
|
15
|
+
|
|
16
|
+
type Dash0Global = ((...args: unknown[]) => void) | undefined;
|
|
17
|
+
|
|
18
|
+
(window as unknown as { dash0Recorder?: unknown }).dash0Recorder = record;
|
|
19
|
+
|
|
20
|
+
const dash0 = (window as unknown as { dash0?: Dash0Global }).dash0;
|
|
21
|
+
if (typeof dash0 === "function") {
|
|
22
|
+
dash0("startSessionRecording", record);
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm entrypoint of `@dash0/sdk-web/session-recording`.
|
|
3
|
+
*
|
|
4
|
+
* This bundle contains only rrweb's recorder. It must not import any SDK module (see eslint.config.js), so the
|
|
5
|
+
* main SDK bundle stays the single owner of configuration, session and transport state.
|
|
6
|
+
*/
|
|
7
|
+
import { record } from "@rrweb/record";
|
|
8
|
+
import type { SessionRecorder } from "../types/session-recording";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The rrweb recorder. Pass it to `init({ sessionRecording: { recorder } })` or to `startSessionRecording(recorder)`.
|
|
12
|
+
*/
|
|
13
|
+
export const recorder = record as unknown as SessionRecorder;
|