@spotify-confidence/csr-common 0.18.4 → 0.18.6
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/CHANGELOG.md +15 -0
- package/dist/confidence-worker.js +446 -0
- package/dist/index.d.cts +43 -2
- package/dist/index.d.ts +43 -2
- package/package.json +2 -2
- package/src/events.ts +49 -1
- package/src/index.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.18.6](https://github.com/spotify/confidence-sdk-js/compare/csr-common-v0.18.5...csr-common-v0.18.6) (2026-08-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ✨ New Features
|
|
7
|
+
|
|
8
|
+
* **csr-recorder:** capture GraphQL operation names ([#434](https://github.com/spotify/confidence-sdk-js/issues/434)) ([c4610ef](https://github.com/spotify/confidence-sdk-js/commit/c4610ef2945cecf632a3027b58ec373bc8b562e7))
|
|
9
|
+
* **csr:** ship standalone worker file and expose workerUrl option ([#412](https://github.com/spotify/confidence-sdk-js/issues/412)) ([7b38441](https://github.com/spotify/confidence-sdk-js/commit/7b38441e439d28496ee72b2d5a2fdfe6f6ab9b40))
|
|
10
|
+
|
|
11
|
+
## [0.18.5](https://github.com/spotify/confidence-sdk-js/compare/csr-common-v0.18.4...csr-common-v0.18.5) (2026-08-12)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### ✨ New Features
|
|
15
|
+
|
|
16
|
+
* **csr-common:** type incremental interaction data ([#430](https://github.com/spotify/confidence-sdk-js/issues/430)) ([43db5d9](https://github.com/spotify/confidence-sdk-js/commit/43db5d91f5368fab10eb5c97ba116575bc728672))
|
|
17
|
+
|
|
3
18
|
## [0.18.4](https://github.com/spotify/confidence-sdk-js/compare/csr-common-v0.18.3...csr-common-v0.18.4) (2026-08-05)
|
|
4
19
|
|
|
5
20
|
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
//#region src/uploader/worker/web-socket-transport.ts
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket-backed Transport. Internal retry policy: clean server-initiated close after the
|
|
4
|
+
* first successful open → reconnect and resume; abrupt close (or any close before the first
|
|
5
|
+
* open) → fire `onClose` and stop. Frames received while a (re)connect is in progress are
|
|
6
|
+
* buffered and flushed on open.
|
|
7
|
+
*
|
|
8
|
+
* `ready()` resolves on the first successful open and rejects on close-before-open. Callers
|
|
9
|
+
* should await it before treating the Transport as live, so a failure to open can be caught
|
|
10
|
+
* (e.g. 4404 unknown session) and recovered from.
|
|
11
|
+
*/
|
|
12
|
+
var WebSocketTransport = class {
|
|
13
|
+
url;
|
|
14
|
+
ws = null;
|
|
15
|
+
onCloseCb = null;
|
|
16
|
+
onStateChangeCb = null;
|
|
17
|
+
intentionallyClosed = false;
|
|
18
|
+
dead = false;
|
|
19
|
+
/** Frames buffered while a (re)connect is in progress. */
|
|
20
|
+
pending = [];
|
|
21
|
+
readyPromise;
|
|
22
|
+
constructor(url) {
|
|
23
|
+
this.url = url;
|
|
24
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
25
|
+
this.connect(false, resolve, reject);
|
|
26
|
+
});
|
|
27
|
+
this.readyPromise.catch(() => {});
|
|
28
|
+
}
|
|
29
|
+
ready() {
|
|
30
|
+
return this.readyPromise;
|
|
31
|
+
}
|
|
32
|
+
send(frame) {
|
|
33
|
+
if (this.dead || this.intentionallyClosed) return;
|
|
34
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(frame));
|
|
35
|
+
else this.pending.push(frame);
|
|
36
|
+
}
|
|
37
|
+
close(reason = "transport-close") {
|
|
38
|
+
this.intentionallyClosed = true;
|
|
39
|
+
this.ws?.close(1e3, reason);
|
|
40
|
+
}
|
|
41
|
+
onClose(cb) {
|
|
42
|
+
this.onCloseCb = cb;
|
|
43
|
+
}
|
|
44
|
+
onStateChange(cb) {
|
|
45
|
+
this.onStateChangeCb = cb;
|
|
46
|
+
}
|
|
47
|
+
connect(isReconnect, onReady, onReadyFail) {
|
|
48
|
+
const ws = new WebSocket(this.url);
|
|
49
|
+
this.ws = ws;
|
|
50
|
+
let opened = false;
|
|
51
|
+
ws.onopen = () => {
|
|
52
|
+
opened = true;
|
|
53
|
+
onReady?.();
|
|
54
|
+
if (isReconnect) this.onStateChangeCb?.({ connected: true });
|
|
55
|
+
while (this.pending.length > 0) {
|
|
56
|
+
const f = this.pending.shift();
|
|
57
|
+
ws.send(JSON.stringify(f));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
ws.onclose = (event) => {
|
|
61
|
+
if (this.intentionallyClosed) return;
|
|
62
|
+
if (!opened) {
|
|
63
|
+
const reason = `${isReconnect ? "reconnect" : "initial"}-failed code=${event.code}`;
|
|
64
|
+
if (onReadyFail) {
|
|
65
|
+
onReadyFail(new Error(reason));
|
|
66
|
+
this.dead = true;
|
|
67
|
+
} else this.die(reason);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (event.wasClean && (event.code === 1e3 || event.code === 1001)) {
|
|
71
|
+
this.onStateChangeCb?.({ connected: false });
|
|
72
|
+
this.connect(true);
|
|
73
|
+
} else this.die(`close code=${event.code} wasClean=${event.wasClean}`);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
die(reason) {
|
|
77
|
+
this.dead = true;
|
|
78
|
+
this.onCloseCb?.({ reason });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/uploader/worker/csr-client.ts
|
|
83
|
+
/**
|
|
84
|
+
* Single Client implementation that talks to the recording backend's REST + WS protocol.
|
|
85
|
+
* Both dev-server and prod implement the same protocol, so we don't need polymorphism here yet.
|
|
86
|
+
*/
|
|
87
|
+
var CsrClient = class {
|
|
88
|
+
apiUrl;
|
|
89
|
+
clientSecret;
|
|
90
|
+
context;
|
|
91
|
+
websocketUrl;
|
|
92
|
+
log;
|
|
93
|
+
forceRecord;
|
|
94
|
+
constructor(apiUrl, clientSecret, context, websocketUrl, log = () => {}, forceRecord) {
|
|
95
|
+
this.apiUrl = apiUrl;
|
|
96
|
+
this.clientSecret = clientSecret;
|
|
97
|
+
this.context = context;
|
|
98
|
+
this.websocketUrl = websocketUrl;
|
|
99
|
+
this.log = log;
|
|
100
|
+
this.forceRecord = forceRecord;
|
|
101
|
+
}
|
|
102
|
+
async initSession() {
|
|
103
|
+
const url = `${this.trimSlash(this.apiUrl)}/v1/sessions:initSession`;
|
|
104
|
+
this.log(`fetch POST ${url}`);
|
|
105
|
+
const res = await fetch(url, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
clientSecret: this.clientSecret,
|
|
110
|
+
...this.context && Object.keys(this.context).length > 0 ? { context: this.context } : {},
|
|
111
|
+
...this.forceRecord ? { forceRecord: true } : {}
|
|
112
|
+
})
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok) throw new Error(`init-session failed: HTTP ${res.status}`);
|
|
115
|
+
const data = await res.json();
|
|
116
|
+
if (data.skipRecording) return { skipRecording: true };
|
|
117
|
+
if (!data.sessionId || !data.sessionToken) throw new Error("init-session response missing sessionId or sessionToken");
|
|
118
|
+
return {
|
|
119
|
+
sessionId: data.sessionId,
|
|
120
|
+
sessionToken: data.sessionToken
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async openTransport(sessionToken) {
|
|
124
|
+
const wsBase = this.websocketUrl ?? `${this.toWsScheme(this.trimSlash(this.apiUrl))}/sessions/stream`;
|
|
125
|
+
const url = `${wsBase}${wsBase.includes("?") ? "&" : "?"}session_token=${encodeURIComponent(sessionToken)}`;
|
|
126
|
+
this.log(`WebSocket connect ${url.replace(/session_token=[^&]*/, "session_token=[REDACTED]")}`);
|
|
127
|
+
const transport = new WebSocketTransport(url);
|
|
128
|
+
await transport.ready();
|
|
129
|
+
return transport;
|
|
130
|
+
}
|
|
131
|
+
trimSlash(s) {
|
|
132
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
133
|
+
}
|
|
134
|
+
toWsScheme(base) {
|
|
135
|
+
if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
|
|
136
|
+
if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
|
|
137
|
+
return base;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/uploader/worker/core.ts
|
|
142
|
+
const IDLE_GRACE_MS = 5e3;
|
|
143
|
+
let state = { phase: "init" };
|
|
144
|
+
const ports = [];
|
|
145
|
+
let idleTimer = null;
|
|
146
|
+
function cancelIdleTimer() {
|
|
147
|
+
if (idleTimer !== null) {
|
|
148
|
+
clearTimeout(idleTimer);
|
|
149
|
+
idleTimer = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function log(msg) {
|
|
153
|
+
for (const handle of ports) if (handle.debugLogs) handle.port.postMessage({
|
|
154
|
+
type: "log",
|
|
155
|
+
msg
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The first hello "locks in" the session's apiUrl/clientSecret. Any later tab arriving
|
|
160
|
+
* with different values is misconfigured — we reject it rather than silently using the
|
|
161
|
+
* locked values. In SharedWorker mode the `name = hash(clientSecret)` scoping already
|
|
162
|
+
* prevents secret-mismatch from sharing a worker, but this defends against the dedicated
|
|
163
|
+
* path and against future bugs.
|
|
164
|
+
*/
|
|
165
|
+
let lockedConfig = null;
|
|
166
|
+
function registerPort(adapter) {
|
|
167
|
+
cancelIdleTimer();
|
|
168
|
+
const handle = {
|
|
169
|
+
port: adapter,
|
|
170
|
+
hello: null,
|
|
171
|
+
debugLogs: false
|
|
172
|
+
};
|
|
173
|
+
ports.push(handle);
|
|
174
|
+
adapter.onmessage((data) => {
|
|
175
|
+
handleMessage(handle, data);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function handleMessage(handle, message) {
|
|
179
|
+
switch (message.type) {
|
|
180
|
+
case "hello":
|
|
181
|
+
handle.hello = message;
|
|
182
|
+
handle.debugLogs = message.debugLogs ?? false;
|
|
183
|
+
if (rejectIfIncompatible(handle)) return;
|
|
184
|
+
if (state.phase !== "dead" && state.phase !== "skipping") detectDuplicateTab(handle);
|
|
185
|
+
onHello(handle);
|
|
186
|
+
return;
|
|
187
|
+
case "frame":
|
|
188
|
+
onFrame(message.frame);
|
|
189
|
+
return;
|
|
190
|
+
case "bye":
|
|
191
|
+
onBye(handle);
|
|
192
|
+
return;
|
|
193
|
+
default: break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Reject hellos whose `apiUrl`/`clientSecret` don't match the values established by the
|
|
198
|
+
* first hello. Returns true if the port was rejected (caller should not continue
|
|
199
|
+
* processing this hello).
|
|
200
|
+
*/
|
|
201
|
+
function rejectIfIncompatible(handle) {
|
|
202
|
+
if (lockedConfig === null) return false;
|
|
203
|
+
const incoming = handle.hello;
|
|
204
|
+
if (incoming.apiUrl === lockedConfig.apiUrl && incoming.websocketUrl === lockedConfig.websocketUrl && incoming.clientSecret === lockedConfig.clientSecret) return false;
|
|
205
|
+
handle.port.postMessage({
|
|
206
|
+
type: "dead",
|
|
207
|
+
reason: "incompatible-options: apiUrl/websocketUrl/clientSecret differ from the worker session"
|
|
208
|
+
});
|
|
209
|
+
const idx = ports.indexOf(handle);
|
|
210
|
+
if (idx >= 0) ports.splice(idx, 1);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* If another already-connected port has the same `tabId`, this hello is from a duplicate
|
|
215
|
+
* tab (browser "Duplicate" command clones sessionStorage). Mint a fresh `tabId` so the two
|
|
216
|
+
* tabs don't collide on the same `(sessionId, tabId)` Recording. The new tabId is returned
|
|
217
|
+
* to the tab in `welcome` so it can update its own state and sessionStorage.
|
|
218
|
+
*/
|
|
219
|
+
function detectDuplicateTab(handle) {
|
|
220
|
+
const tabId = handle.hello.tabId;
|
|
221
|
+
if (!ports.some((p) => p !== handle && p.hello?.tabId === tabId)) return;
|
|
222
|
+
const fresh = crypto.randomUUID();
|
|
223
|
+
handle.newTabId = fresh;
|
|
224
|
+
handle.hello.tabId = fresh;
|
|
225
|
+
}
|
|
226
|
+
function onHello(handle) {
|
|
227
|
+
switch (state.phase) {
|
|
228
|
+
case "init":
|
|
229
|
+
lockedConfig = {
|
|
230
|
+
apiUrl: handle.hello.apiUrl,
|
|
231
|
+
websocketUrl: handle.hello.websocketUrl,
|
|
232
|
+
clientSecret: handle.hello.clientSecret
|
|
233
|
+
};
|
|
234
|
+
log(`hello received apiUrl=${handle.hello.apiUrl} websocketUrl=${handle.hello.websocketUrl ?? "(derive)"} sessionIdHint=${handle.hello.sessionIdHint ?? "(none)"}`);
|
|
235
|
+
state = { phase: "initializing" };
|
|
236
|
+
initializeSession(handle.hello).then(flushPendingWelcomes);
|
|
237
|
+
return;
|
|
238
|
+
case "initializing": return;
|
|
239
|
+
case "active":
|
|
240
|
+
sendActiveWelcome(handle, state.sessionId, state.sessionToken);
|
|
241
|
+
return;
|
|
242
|
+
case "idle": {
|
|
243
|
+
const { client, sessionId, sessionToken } = state;
|
|
244
|
+
state = { phase: "initializing" };
|
|
245
|
+
resumeTransport(client, sessionId, sessionToken).then(flushPendingWelcomes);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
case "skipping":
|
|
249
|
+
if (handle.hello.forceRecord) {
|
|
250
|
+
log("forceRecord set; re-initializing from skipping state");
|
|
251
|
+
state = { phase: "initializing" };
|
|
252
|
+
initializeSession(handle.hello).then(flushPendingWelcomes);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
handle.port.postMessage({
|
|
256
|
+
type: "welcome",
|
|
257
|
+
result: { skipRecording: true }
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
case "dead":
|
|
261
|
+
handle.port.postMessage({
|
|
262
|
+
type: "dead",
|
|
263
|
+
reason: state.reason
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
default: break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function initializeSession(firstHello) {
|
|
270
|
+
const client = new CsrClient(firstHello.apiUrl, firstHello.clientSecret, firstHello.context, firstHello.websocketUrl, log, firstHello.forceRecord);
|
|
271
|
+
if (firstHello.sessionIdHint && firstHello.sessionTokenHint) {
|
|
272
|
+
log(`adopting sessionIdHint=${firstHello.sessionIdHint}`);
|
|
273
|
+
try {
|
|
274
|
+
const transport = await client.openTransport(firstHello.sessionTokenHint);
|
|
275
|
+
wireTransport(transport);
|
|
276
|
+
state = {
|
|
277
|
+
phase: "active",
|
|
278
|
+
client,
|
|
279
|
+
transport,
|
|
280
|
+
sessionId: firstHello.sessionIdHint,
|
|
281
|
+
sessionToken: firstHello.sessionTokenHint
|
|
282
|
+
};
|
|
283
|
+
log("hint adopted; transport open");
|
|
284
|
+
if (ports.length === 0) idleTimer = setTimeout(enterIdle, IDLE_GRACE_MS);
|
|
285
|
+
return;
|
|
286
|
+
} catch (err) {
|
|
287
|
+
log(`hint rejected (${String(err)}); falling back to fresh init`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
let result;
|
|
291
|
+
try {
|
|
292
|
+
result = await client.initSession();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
log(`init-session threw: ${String(err)}`);
|
|
295
|
+
transitionToDead(`init-session-failed: ${String(err)}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if ("skipRecording" in result) {
|
|
299
|
+
log("init-session: skipRecording");
|
|
300
|
+
state = { phase: "skipping" };
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
log(`init-session ok sessionId=${result.sessionId}`);
|
|
304
|
+
let transport;
|
|
305
|
+
try {
|
|
306
|
+
transport = await client.openTransport(result.sessionToken);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
log(`openTransport threw: ${String(err)}`);
|
|
309
|
+
transitionToDead(`open-transport-failed: ${String(err)}`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
wireTransport(transport);
|
|
313
|
+
log("transport open; session active");
|
|
314
|
+
state = {
|
|
315
|
+
phase: "active",
|
|
316
|
+
client,
|
|
317
|
+
transport,
|
|
318
|
+
sessionId: result.sessionId,
|
|
319
|
+
sessionToken: result.sessionToken
|
|
320
|
+
};
|
|
321
|
+
if (ports.length === 0) idleTimer = setTimeout(enterIdle, IDLE_GRACE_MS);
|
|
322
|
+
}
|
|
323
|
+
async function resumeTransport(client, sessionId, sessionToken) {
|
|
324
|
+
log("resuming transport from idle");
|
|
325
|
+
let transport;
|
|
326
|
+
try {
|
|
327
|
+
transport = await client.openTransport(sessionToken);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
log(`resume-transport threw: ${String(err)}`);
|
|
330
|
+
transitionToDead(`resume-transport-failed: ${String(err)}`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
wireTransport(transport);
|
|
334
|
+
log("transport resumed");
|
|
335
|
+
state = {
|
|
336
|
+
phase: "active",
|
|
337
|
+
client,
|
|
338
|
+
transport,
|
|
339
|
+
sessionId,
|
|
340
|
+
sessionToken
|
|
341
|
+
};
|
|
342
|
+
if (ports.length === 0) idleTimer = setTimeout(enterIdle, IDLE_GRACE_MS);
|
|
343
|
+
}
|
|
344
|
+
function wireTransport(transport) {
|
|
345
|
+
transport.onClose((info) => {
|
|
346
|
+
if (state.phase !== "active") return;
|
|
347
|
+
transitionToDead(info.reason);
|
|
348
|
+
});
|
|
349
|
+
transport.onStateChange((info) => {
|
|
350
|
+
if (state.phase !== "active") return;
|
|
351
|
+
for (const handle of ports) handle.port.postMessage({
|
|
352
|
+
type: "state",
|
|
353
|
+
connected: info.connected
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function transitionToDead(reason) {
|
|
358
|
+
state = {
|
|
359
|
+
phase: "dead",
|
|
360
|
+
reason
|
|
361
|
+
};
|
|
362
|
+
for (const handle of ports) handle.port.postMessage({
|
|
363
|
+
type: "dead",
|
|
364
|
+
reason
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function flushPendingWelcomes() {
|
|
368
|
+
for (const handle of ports) {
|
|
369
|
+
if (handle.hello === null) continue;
|
|
370
|
+
if (state.phase === "active") sendActiveWelcome(handle, state.sessionId, state.sessionToken);
|
|
371
|
+
else if (state.phase === "skipping") handle.port.postMessage({
|
|
372
|
+
type: "welcome",
|
|
373
|
+
result: { skipRecording: true }
|
|
374
|
+
});
|
|
375
|
+
else if (state.phase === "dead") handle.port.postMessage({
|
|
376
|
+
type: "dead",
|
|
377
|
+
reason: state.reason
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function sendActiveWelcome(handle, currentSessionId, currentSessionToken) {
|
|
382
|
+
const hint = handle.hello?.sessionIdHint;
|
|
383
|
+
const adopted = hint !== void 0 && hint !== currentSessionId;
|
|
384
|
+
const newTabId = handle.newTabId;
|
|
385
|
+
handle.port.postMessage({
|
|
386
|
+
type: "welcome",
|
|
387
|
+
result: {
|
|
388
|
+
sessionId: currentSessionId,
|
|
389
|
+
sessionToken: currentSessionToken
|
|
390
|
+
},
|
|
391
|
+
adoptedFromSessionId: adopted ? hint : void 0,
|
|
392
|
+
newTabId,
|
|
393
|
+
resetCounter: adopted || newTabId !== void 0
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function onFrame(frame) {
|
|
397
|
+
if (state.phase !== "active") return;
|
|
398
|
+
state.transport.send(frame);
|
|
399
|
+
}
|
|
400
|
+
function onBye(handle) {
|
|
401
|
+
const idx = ports.indexOf(handle);
|
|
402
|
+
if (idx >= 0) ports.splice(idx, 1);
|
|
403
|
+
if (ports.length === 0 && state.phase === "active") {
|
|
404
|
+
log(`last tab disconnected; closing transport in ${IDLE_GRACE_MS}ms`);
|
|
405
|
+
idleTimer = setTimeout(enterIdle, IDLE_GRACE_MS);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function enterIdle() {
|
|
409
|
+
idleTimer = null;
|
|
410
|
+
if (state.phase !== "active" || ports.length > 0) return;
|
|
411
|
+
log("idle timeout; closing transport");
|
|
412
|
+
state.transport.close("idle");
|
|
413
|
+
state = {
|
|
414
|
+
phase: "idle",
|
|
415
|
+
client: state.client,
|
|
416
|
+
sessionId: state.sessionId,
|
|
417
|
+
sessionToken: state.sessionToken
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/uploader/worker/entry.ts
|
|
422
|
+
const SharedWorkerScopeCtor = globalThis.SharedWorkerGlobalScope;
|
|
423
|
+
if (typeof SharedWorkerScopeCtor === "function" && self instanceof SharedWorkerScopeCtor) self.onconnect = (event) => {
|
|
424
|
+
const port = event.ports[0];
|
|
425
|
+
port.start();
|
|
426
|
+
registerPort(adaptMessagePort(port));
|
|
427
|
+
};
|
|
428
|
+
else registerPort(adaptDedicatedSelf());
|
|
429
|
+
function adaptMessagePort(port) {
|
|
430
|
+
return {
|
|
431
|
+
postMessage: (message) => port.postMessage(message),
|
|
432
|
+
onmessage: (cb) => {
|
|
433
|
+
port.onmessage = (e) => cb(e.data);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function adaptDedicatedSelf() {
|
|
438
|
+
const ws = self;
|
|
439
|
+
return {
|
|
440
|
+
postMessage: (message) => ws.postMessage(message),
|
|
441
|
+
onmessage: (cb) => {
|
|
442
|
+
ws.onmessage = (e) => cb(e.data);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
//#endregion
|
package/dist/index.d.cts
CHANGED
|
@@ -61,6 +61,39 @@ declare enum MouseInteractions {
|
|
|
61
61
|
TouchEnd = 9,
|
|
62
62
|
TouchCancel = 10
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Incremental mouse-interaction data emitted by rrweb.
|
|
66
|
+
*/
|
|
67
|
+
type MouseInteractionData = {
|
|
68
|
+
source: IncrementalSource.MouseInteraction;
|
|
69
|
+
type: MouseInteractions;
|
|
70
|
+
id: number;
|
|
71
|
+
x?: number;
|
|
72
|
+
y?: number;
|
|
73
|
+
pointerType?: number;
|
|
74
|
+
};
|
|
75
|
+
type SelectionRange = {
|
|
76
|
+
start: number;
|
|
77
|
+
startOffset: number;
|
|
78
|
+
end: number;
|
|
79
|
+
endOffset: number;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Incremental text-selection data emitted by rrweb.
|
|
83
|
+
*/
|
|
84
|
+
type SelectionData = {
|
|
85
|
+
source: IncrementalSource.Selection;
|
|
86
|
+
ranges: SelectionRange[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Incremental sources that csr-common does not model yet. The source remains
|
|
90
|
+
* discriminated so consumers can narrow the known mouse and selection shapes.
|
|
91
|
+
*/
|
|
92
|
+
type OpaqueIncrementalData = {
|
|
93
|
+
source: Exclude<IncrementalSource, IncrementalSource.MouseInteraction | IncrementalSource.Selection>;
|
|
94
|
+
[key: string]: unknown;
|
|
95
|
+
};
|
|
96
|
+
type IncrementalSnapshotData = MouseInteractionData | SelectionData | OpaqueIncrementalData;
|
|
64
97
|
type RageClickCustomData = {
|
|
65
98
|
tag: "csr:rageClick";
|
|
66
99
|
payload: {
|
|
@@ -177,6 +210,9 @@ type ConsoleLogPluginData = {
|
|
|
177
210
|
};
|
|
178
211
|
};
|
|
179
212
|
type NetworkRequestInitiator = "fetch" | "xhr";
|
|
213
|
+
type GraphQLRequestMetadata = {
|
|
214
|
+
operationName: string;
|
|
215
|
+
};
|
|
180
216
|
/**
|
|
181
217
|
* Plugin event data emitted by the recorder for network requests.
|
|
182
218
|
*/
|
|
@@ -190,6 +226,7 @@ type NetworkRequestPluginData = {
|
|
|
190
226
|
durationMs: number;
|
|
191
227
|
requestSize?: number;
|
|
192
228
|
responseSize?: number;
|
|
229
|
+
graphql?: GraphQLRequestMetadata;
|
|
193
230
|
};
|
|
194
231
|
};
|
|
195
232
|
type RouteChangePluginData = {
|
|
@@ -272,7 +309,11 @@ type RecordingEvent = {
|
|
|
272
309
|
timestamp: number;
|
|
273
310
|
data: CustomEventData;
|
|
274
311
|
} | {
|
|
275
|
-
type:
|
|
312
|
+
type: RecordingEventType.IncrementalSnapshot;
|
|
313
|
+
timestamp: number;
|
|
314
|
+
data: IncrementalSnapshotData;
|
|
315
|
+
} | {
|
|
316
|
+
type: Exclude<RecordingEventType, RecordingEventType.Custom | RecordingEventType.IncrementalSnapshot>;
|
|
276
317
|
timestamp: number;
|
|
277
318
|
data: unknown;
|
|
278
319
|
};
|
|
@@ -293,4 +334,4 @@ declare function validateKey(key: string): string | null;
|
|
|
293
334
|
declare function validateTagValue(value: string | undefined): string | null;
|
|
294
335
|
declare function validateMeasureValue(value: number | undefined): string | null;
|
|
295
336
|
//#endregion
|
|
296
|
-
export { type AwayGapCustomData, type ClickCustomData, type ClientContext, type ConsoleLogLevel, type ConsoleLogPluginData, type CustomEventData, type DeadClickCustomData, type DialogOpenedCustomData, type ElementDescriptor, type ErrorMessageCustomData, type FlagEvaluationPluginData, type FormFieldReEditCustomData, type Frame, type IdleGapCustomData, IncrementalSource, type InputCustomData, MAX_DISTINCT_KEYS, MAX_KEY_LENGTH, MAX_TAG_VALUE_LENGTH, MAX_VALUES_PER_KEY, type MeasurePluginData, MouseInteractions, type NetworkRequestInitiator, type NetworkRequestPluginData, type RageClickCustomData, type RecordingEvent, RecordingEventType, type RouteChangeCustomData, type RouteChangePayload, type RouteChangePluginData, type RouteChangeTrigger, type ScrollBackCustomData, SerializedNodeType, type TabRefocusCustomData, type TabUnfocusCustomData, type TabVisibilityPluginData, type TagPluginData, type UserAgentContext, stripUrl, validateKey, validateMeasureValue, validateTagValue };
|
|
337
|
+
export { type AwayGapCustomData, type ClickCustomData, type ClientContext, type ConsoleLogLevel, type ConsoleLogPluginData, type CustomEventData, type DeadClickCustomData, type DialogOpenedCustomData, type ElementDescriptor, type ErrorMessageCustomData, type FlagEvaluationPluginData, type FormFieldReEditCustomData, type Frame, type GraphQLRequestMetadata, type IdleGapCustomData, type IncrementalSnapshotData, IncrementalSource, type InputCustomData, MAX_DISTINCT_KEYS, MAX_KEY_LENGTH, MAX_TAG_VALUE_LENGTH, MAX_VALUES_PER_KEY, type MeasurePluginData, type MouseInteractionData, MouseInteractions, type NetworkRequestInitiator, type NetworkRequestPluginData, type OpaqueIncrementalData, type RageClickCustomData, type RecordingEvent, RecordingEventType, type RouteChangeCustomData, type RouteChangePayload, type RouteChangePluginData, type RouteChangeTrigger, type ScrollBackCustomData, type SelectionData, type SelectionRange, SerializedNodeType, type TabRefocusCustomData, type TabUnfocusCustomData, type TabVisibilityPluginData, type TagPluginData, type UserAgentContext, stripUrl, validateKey, validateMeasureValue, validateTagValue };
|
package/dist/index.d.ts
CHANGED
|
@@ -61,6 +61,39 @@ declare enum MouseInteractions {
|
|
|
61
61
|
TouchEnd = 9,
|
|
62
62
|
TouchCancel = 10
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Incremental mouse-interaction data emitted by rrweb.
|
|
66
|
+
*/
|
|
67
|
+
type MouseInteractionData = {
|
|
68
|
+
source: IncrementalSource.MouseInteraction;
|
|
69
|
+
type: MouseInteractions;
|
|
70
|
+
id: number;
|
|
71
|
+
x?: number;
|
|
72
|
+
y?: number;
|
|
73
|
+
pointerType?: number;
|
|
74
|
+
};
|
|
75
|
+
type SelectionRange = {
|
|
76
|
+
start: number;
|
|
77
|
+
startOffset: number;
|
|
78
|
+
end: number;
|
|
79
|
+
endOffset: number;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Incremental text-selection data emitted by rrweb.
|
|
83
|
+
*/
|
|
84
|
+
type SelectionData = {
|
|
85
|
+
source: IncrementalSource.Selection;
|
|
86
|
+
ranges: SelectionRange[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Incremental sources that csr-common does not model yet. The source remains
|
|
90
|
+
* discriminated so consumers can narrow the known mouse and selection shapes.
|
|
91
|
+
*/
|
|
92
|
+
type OpaqueIncrementalData = {
|
|
93
|
+
source: Exclude<IncrementalSource, IncrementalSource.MouseInteraction | IncrementalSource.Selection>;
|
|
94
|
+
[key: string]: unknown;
|
|
95
|
+
};
|
|
96
|
+
type IncrementalSnapshotData = MouseInteractionData | SelectionData | OpaqueIncrementalData;
|
|
64
97
|
type RageClickCustomData = {
|
|
65
98
|
tag: "csr:rageClick";
|
|
66
99
|
payload: {
|
|
@@ -177,6 +210,9 @@ type ConsoleLogPluginData = {
|
|
|
177
210
|
};
|
|
178
211
|
};
|
|
179
212
|
type NetworkRequestInitiator = "fetch" | "xhr";
|
|
213
|
+
type GraphQLRequestMetadata = {
|
|
214
|
+
operationName: string;
|
|
215
|
+
};
|
|
180
216
|
/**
|
|
181
217
|
* Plugin event data emitted by the recorder for network requests.
|
|
182
218
|
*/
|
|
@@ -190,6 +226,7 @@ type NetworkRequestPluginData = {
|
|
|
190
226
|
durationMs: number;
|
|
191
227
|
requestSize?: number;
|
|
192
228
|
responseSize?: number;
|
|
229
|
+
graphql?: GraphQLRequestMetadata;
|
|
193
230
|
};
|
|
194
231
|
};
|
|
195
232
|
type RouteChangePluginData = {
|
|
@@ -272,7 +309,11 @@ type RecordingEvent = {
|
|
|
272
309
|
timestamp: number;
|
|
273
310
|
data: CustomEventData;
|
|
274
311
|
} | {
|
|
275
|
-
type:
|
|
312
|
+
type: RecordingEventType.IncrementalSnapshot;
|
|
313
|
+
timestamp: number;
|
|
314
|
+
data: IncrementalSnapshotData;
|
|
315
|
+
} | {
|
|
316
|
+
type: Exclude<RecordingEventType, RecordingEventType.Custom | RecordingEventType.IncrementalSnapshot>;
|
|
276
317
|
timestamp: number;
|
|
277
318
|
data: unknown;
|
|
278
319
|
};
|
|
@@ -293,4 +334,4 @@ declare function validateKey(key: string): string | null;
|
|
|
293
334
|
declare function validateTagValue(value: string | undefined): string | null;
|
|
294
335
|
declare function validateMeasureValue(value: number | undefined): string | null;
|
|
295
336
|
//#endregion
|
|
296
|
-
export { type AwayGapCustomData, type ClickCustomData, type ClientContext, type ConsoleLogLevel, type ConsoleLogPluginData, type CustomEventData, type DeadClickCustomData, type DialogOpenedCustomData, type ElementDescriptor, type ErrorMessageCustomData, type FlagEvaluationPluginData, type FormFieldReEditCustomData, type Frame, type IdleGapCustomData, IncrementalSource, type InputCustomData, MAX_DISTINCT_KEYS, MAX_KEY_LENGTH, MAX_TAG_VALUE_LENGTH, MAX_VALUES_PER_KEY, type MeasurePluginData, MouseInteractions, type NetworkRequestInitiator, type NetworkRequestPluginData, type RageClickCustomData, type RecordingEvent, RecordingEventType, type RouteChangeCustomData, type RouteChangePayload, type RouteChangePluginData, type RouteChangeTrigger, type ScrollBackCustomData, SerializedNodeType, type TabRefocusCustomData, type TabUnfocusCustomData, type TabVisibilityPluginData, type TagPluginData, type UserAgentContext, stripUrl, validateKey, validateMeasureValue, validateTagValue };
|
|
337
|
+
export { type AwayGapCustomData, type ClickCustomData, type ClientContext, type ConsoleLogLevel, type ConsoleLogPluginData, type CustomEventData, type DeadClickCustomData, type DialogOpenedCustomData, type ElementDescriptor, type ErrorMessageCustomData, type FlagEvaluationPluginData, type FormFieldReEditCustomData, type Frame, type GraphQLRequestMetadata, type IdleGapCustomData, type IncrementalSnapshotData, IncrementalSource, type InputCustomData, MAX_DISTINCT_KEYS, MAX_KEY_LENGTH, MAX_TAG_VALUE_LENGTH, MAX_VALUES_PER_KEY, type MeasurePluginData, type MouseInteractionData, MouseInteractions, type NetworkRequestInitiator, type NetworkRequestPluginData, type OpaqueIncrementalData, type RageClickCustomData, type RecordingEvent, RecordingEventType, type RouteChangeCustomData, type RouteChangePayload, type RouteChangePluginData, type RouteChangeTrigger, type ScrollBackCustomData, type SelectionData, type SelectionRange, SerializedNodeType, type TabRefocusCustomData, type TabUnfocusCustomData, type TabVisibilityPluginData, type TagPluginData, type UserAgentContext, stripUrl, validateKey, validateMeasureValue, validateTagValue };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spotify-confidence/csr-common",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.6",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/spotify/confidence-sdk-js.git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
30
|
"prebuild": "node scripts/build-worker.mjs",
|
|
31
|
-
"build": "yarn run -T tsdown",
|
|
31
|
+
"build": "yarn run -T tsdown && node scripts/emit-worker-file.mjs",
|
|
32
32
|
"typecheck": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
package/src/events.ts
CHANGED
|
@@ -62,6 +62,44 @@ export enum MouseInteractions {
|
|
|
62
62
|
TouchCancel = 10,
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Incremental mouse-interaction data emitted by rrweb.
|
|
67
|
+
*/
|
|
68
|
+
export type MouseInteractionData = {
|
|
69
|
+
source: IncrementalSource.MouseInteraction;
|
|
70
|
+
type: MouseInteractions;
|
|
71
|
+
id: number;
|
|
72
|
+
x?: number;
|
|
73
|
+
y?: number;
|
|
74
|
+
pointerType?: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type SelectionRange = {
|
|
78
|
+
start: number;
|
|
79
|
+
startOffset: number;
|
|
80
|
+
end: number;
|
|
81
|
+
endOffset: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Incremental text-selection data emitted by rrweb.
|
|
86
|
+
*/
|
|
87
|
+
export type SelectionData = {
|
|
88
|
+
source: IncrementalSource.Selection;
|
|
89
|
+
ranges: SelectionRange[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Incremental sources that csr-common does not model yet. The source remains
|
|
94
|
+
* discriminated so consumers can narrow the known mouse and selection shapes.
|
|
95
|
+
*/
|
|
96
|
+
export type OpaqueIncrementalData = {
|
|
97
|
+
source: Exclude<IncrementalSource, IncrementalSource.MouseInteraction | IncrementalSource.Selection>;
|
|
98
|
+
[key: string]: unknown;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type IncrementalSnapshotData = MouseInteractionData | SelectionData | OpaqueIncrementalData;
|
|
102
|
+
|
|
65
103
|
export type RageClickCustomData = {
|
|
66
104
|
tag: 'csr:rageClick';
|
|
67
105
|
payload: {
|
|
@@ -192,6 +230,10 @@ export type ConsoleLogPluginData = {
|
|
|
192
230
|
|
|
193
231
|
export type NetworkRequestInitiator = 'fetch' | 'xhr';
|
|
194
232
|
|
|
233
|
+
export type GraphQLRequestMetadata = {
|
|
234
|
+
operationName: string;
|
|
235
|
+
};
|
|
236
|
+
|
|
195
237
|
/**
|
|
196
238
|
* Plugin event data emitted by the recorder for network requests.
|
|
197
239
|
*/
|
|
@@ -205,6 +247,7 @@ export type NetworkRequestPluginData = {
|
|
|
205
247
|
durationMs: number;
|
|
206
248
|
requestSize?: number;
|
|
207
249
|
responseSize?: number;
|
|
250
|
+
graphql?: GraphQLRequestMetadata;
|
|
208
251
|
};
|
|
209
252
|
};
|
|
210
253
|
|
|
@@ -312,7 +355,12 @@ export type RecordingEvent =
|
|
|
312
355
|
data: CustomEventData;
|
|
313
356
|
}
|
|
314
357
|
| {
|
|
315
|
-
type:
|
|
358
|
+
type: RecordingEventType.IncrementalSnapshot;
|
|
359
|
+
timestamp: number;
|
|
360
|
+
data: IncrementalSnapshotData;
|
|
361
|
+
}
|
|
362
|
+
| {
|
|
363
|
+
type: Exclude<RecordingEventType, RecordingEventType.Custom | RecordingEventType.IncrementalSnapshot>;
|
|
316
364
|
timestamp: number;
|
|
317
365
|
data: unknown;
|
|
318
366
|
};
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,11 @@ export {
|
|
|
4
4
|
IncrementalSource,
|
|
5
5
|
MouseInteractions,
|
|
6
6
|
type RecordingEvent,
|
|
7
|
+
type IncrementalSnapshotData,
|
|
8
|
+
type MouseInteractionData,
|
|
9
|
+
type OpaqueIncrementalData,
|
|
10
|
+
type SelectionData,
|
|
11
|
+
type SelectionRange,
|
|
7
12
|
type CustomEventData,
|
|
8
13
|
type ClickCustomData,
|
|
9
14
|
type InputCustomData,
|
|
@@ -18,6 +23,7 @@ export {
|
|
|
18
23
|
type ConsoleLogLevel,
|
|
19
24
|
type ConsoleLogPluginData,
|
|
20
25
|
type NetworkRequestInitiator,
|
|
26
|
+
type GraphQLRequestMetadata,
|
|
21
27
|
type NetworkRequestPluginData,
|
|
22
28
|
type RouteChangeTrigger,
|
|
23
29
|
type RouteChangePayload,
|