@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.
- 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 +30 -0
- package/dist/modules/api/session-recording_test.js +36 -0
- package/dist/modules/api/start-view.js +47 -0
- package/dist/modules/api/start-view_test.js +91 -0
- package/dist/modules/entrypoint/npm-package.js +2 -0
- package/dist/modules/entrypoint/npm-package_test.js +7 -0
- package/dist/modules/entrypoint/script.js +5 -0
- package/dist/modules/entrypoint/session-recording-script.js +19 -0
- package/dist/modules/entrypoint/session-recording.js +11 -0
- package/dist/modules/instrumentations/navigation/event.js +42 -10
- package/dist/modules/instrumentations/navigation/event_test.js +128 -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 +165 -0
- package/dist/modules/instrumentations/session-recording/index_test.js +230 -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 +17 -0
- package/dist/types/api/session-recording_test.d.ts +1 -0
- package/dist/types/api/start-view.d.ts +33 -0
- package/dist/types/api/start-view_test.d.ts +1 -0
- package/dist/types/entrypoint/npm-package.d.ts +4 -0
- package/dist/types/entrypoint/npm-package_test.d.ts +1 -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/navigation/event.d.ts +18 -0
- package/dist/types/instrumentations/navigation/event_test.d.ts +1 -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 +11 -3
- package/src/api/init.ts +19 -1
- package/src/api/init_test.ts +33 -1
- package/src/api/session-recording.ts +40 -0
- package/src/api/session-recording_test.ts +44 -0
- package/src/api/start-view.ts +68 -0
- package/src/api/start-view_test.ts +125 -0
- package/src/entrypoint/npm-package.ts +4 -0
- package/src/entrypoint/npm-package_test.ts +8 -0
- package/src/entrypoint/script.ts +5 -0
- package/src/entrypoint/session-recording-script.ts +23 -0
- package/src/entrypoint/session-recording.ts +13 -0
- package/src/instrumentations/navigation/event.ts +63 -15
- package/src/instrumentations/navigation/event_test.ts +171 -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 +175 -0
- package/src/instrumentations/session-recording/index_test.ts +273 -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,38 @@
|
|
|
1
|
+
import { SessionRecordingEvent } from "../../types/session-recording";
|
|
2
|
+
export type Chunk = {
|
|
3
|
+
/**
|
|
4
|
+
* Zero-based, monotonic within one recording.
|
|
5
|
+
*/
|
|
6
|
+
seq: number;
|
|
7
|
+
/**
|
|
8
|
+
* The rrweb events as a serialized JSON array.
|
|
9
|
+
*/
|
|
10
|
+
body: string;
|
|
11
|
+
eventCount: number;
|
|
12
|
+
hasSnapshot: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Milliseconds since the unix epoch of the first and the last event in the chunk.
|
|
15
|
+
*/
|
|
16
|
+
startTime: number;
|
|
17
|
+
endTime: number;
|
|
18
|
+
};
|
|
19
|
+
export type ChunkerOptions = {
|
|
20
|
+
maxBytes: number;
|
|
21
|
+
maxMillis: number;
|
|
22
|
+
onChunk: (chunk: Chunk) => void;
|
|
23
|
+
};
|
|
24
|
+
export type Chunker = {
|
|
25
|
+
add(event: SessionRecordingEvent): void;
|
|
26
|
+
flush(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Drops buffered events without emitting a chunk and cancels the pending time-based flush. Used when the
|
|
29
|
+
* recorder failed to start after it already emitted events, so no stray chunk is transmitted later.
|
|
30
|
+
*/
|
|
31
|
+
discard(): void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Buffers rrweb events and hands them out as chunks. A chunk closes when its serialized size reaches
|
|
35
|
+
* `maxBytes`, when `maxMillis` have passed since its first event, when a new full snapshot begins, or when
|
|
36
|
+
* `flush()` is called. Events are serialized once, on arrival, so a flush is a join and not a second stringify.
|
|
37
|
+
*/
|
|
38
|
+
export declare function newChunker(opts: ChunkerOptions): Chunker;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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.25.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",
|
|
@@ -117,7 +125,7 @@
|
|
|
117
125
|
"resolutions": {
|
|
118
126
|
"conventional-changelog-conventionalcommits": "8.0.0"
|
|
119
127
|
},
|
|
120
|
-
"packageManager": "pnpm@
|
|
128
|
+
"packageManager": "pnpm@10.33.4",
|
|
121
129
|
"publishConfig": {
|
|
122
130
|
"access": "public"
|
|
123
131
|
}
|
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,40 @@
|
|
|
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
|
+
export function startSessionRecording(recorder?: SessionRecorder): void {
|
|
20
|
+
// The script entrypoint forwards dash0("startSessionRecording", ...) arguments without type checking,
|
|
21
|
+
// so malformed calls must degrade to a logged no-op instead of throwing. An uncaught throw here would
|
|
22
|
+
// abort the command-queue drain and drop all subsequently queued api calls.
|
|
23
|
+
const r = recorder ?? (win as any)?.[GLOBAL_RECORDER_KEY];
|
|
24
|
+
if (typeof r !== "function") {
|
|
25
|
+
debug(
|
|
26
|
+
"startSessionRecording requires a recorder. Import `recorder` from `@dash0/sdk-web/session-recording` or load dash0-session-recording.iife.js. Ignoring call."
|
|
27
|
+
);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
registerSessionRecorder(r as SessionRecorder);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Stops the running session recording and transmits any buffered events. Calling this when no recording is
|
|
36
|
+
* running is a no-op.
|
|
37
|
+
*/
|
|
38
|
+
export function stopSessionRecording(): void {
|
|
39
|
+
stopRecording();
|
|
40
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { transmitManualPageViewEvent } from "../instrumentations/navigation/event";
|
|
2
|
+
import { AttributeValueType } from "../utils/otel";
|
|
3
|
+
import { AnyValue } from "../types/otlp";
|
|
4
|
+
import { debug, nowNanos, win } from "../utils";
|
|
5
|
+
import { vars } from "../vars";
|
|
6
|
+
|
|
7
|
+
export type StartViewOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Optionally override the url reflected in `page.url.*` attributes for this view.
|
|
10
|
+
* Accepts an absolute or relative url; relative urls are resolved against the current
|
|
11
|
+
* `location.href`. Falls back to the real `location.href` if omitted or invalid.
|
|
12
|
+
* This is display-only: calling startView never navigates or mutates history/location.
|
|
13
|
+
*/
|
|
14
|
+
url?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Additional attributes to include with the page view.
|
|
18
|
+
* Added after the SDK-generated attributes, so they can override them.
|
|
19
|
+
*/
|
|
20
|
+
attributes?: Record<string, AttributeValueType | AnyValue>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manually records a page view, side-effect free: this never calls `history.pushState` /
|
|
25
|
+
* `history.replaceState` and never mutates `location`. Intended for single-page applications
|
|
26
|
+
* that own their own router and cannot let the SDK touch navigation state (e.g. Electron apps
|
|
27
|
+
* serving the whole app from one root URL, where automatic page-view tracking would report
|
|
28
|
+
* every screen as "/").
|
|
29
|
+
*
|
|
30
|
+
* The emitted event is indistinguishable from an automatic virtual page view downstream
|
|
31
|
+
* (same `browser.page_view` event name, same `type` value), with two differences: it is never
|
|
32
|
+
* accompanied by a `change_state` value, since no history mutation occurred, and the
|
|
33
|
+
* `pageViewInstrumentation`'s `generateMetadata` callback is not invoked for manual views —
|
|
34
|
+
* supply title and attributes directly instead.
|
|
35
|
+
*
|
|
36
|
+
* @param name The name of the view, e.g. "/settings". Transmitted as the page view's title.
|
|
37
|
+
* @param opts Additional page view details.
|
|
38
|
+
*/
|
|
39
|
+
export function startView(name: string, opts?: StartViewOptions) {
|
|
40
|
+
if (vars.endpoints.length === 0) {
|
|
41
|
+
debug("Dash0 SDK has not been initialized. Ignoring startView call.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The script entrypoint forwards dash0("startView", ...) arguments without type checking,
|
|
46
|
+
// so malformed calls must degrade to a logged no-op instead of throwing. An uncaught throw
|
|
47
|
+
// here would abort the command-queue drain and drop all subsequently queued api calls.
|
|
48
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
49
|
+
debug("startView requires a non-empty view name. Ignoring startView call.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let url: URL | undefined;
|
|
54
|
+
if (opts?.url != null) {
|
|
55
|
+
try {
|
|
56
|
+
url = new URL(opts.url, win?.location.href);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
debug("Failed to parse startView url option. Falling back to the current location.", e);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
transmitManualPageViewEvent({
|
|
63
|
+
timeUnixNano: nowNanos(),
|
|
64
|
+
title: name,
|
|
65
|
+
url,
|
|
66
|
+
attributes: opts?.attributes,
|
|
67
|
+
});
|
|
68
|
+
}
|