@spotify-confidence/csr-common 0.18.5 → 0.18.7

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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.18.7](https://github.com/spotify/confidence-sdk-js/compare/csr-common-v0.18.6...csr-common-v0.18.7) (2026-08-18)
4
+
5
+
6
+ ### ✨ New Features
7
+
8
+ * **csr-recorder:** capture click modifier keys ([#435](https://github.com/spotify/confidence-sdk-js/issues/435)) ([a1e7a96](https://github.com/spotify/confidence-sdk-js/commit/a1e7a96996cd2f361f3b44cc924d6b4d5ff0b003))
9
+
10
+ ## [0.18.6](https://github.com/spotify/confidence-sdk-js/compare/csr-common-v0.18.5...csr-common-v0.18.6) (2026-08-18)
11
+
12
+
13
+ ### ✨ New Features
14
+
15
+ * **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))
16
+ * **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))
17
+
3
18
  ## [0.18.5](https://github.com/spotify/confidence-sdk-js/compare/csr-common-v0.18.4...csr-common-v0.18.5) (2026-08-12)
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
@@ -70,7 +70,12 @@ type MouseInteractionData = {
70
70
  id: number;
71
71
  x?: number;
72
72
  y?: number;
73
- pointerType?: number;
73
+ pointerType?: number; /** Mouse button and modifier keys present on click events. */
74
+ button?: number;
75
+ altKey?: boolean;
76
+ ctrlKey?: boolean;
77
+ metaKey?: boolean;
78
+ shiftKey?: boolean;
74
79
  };
75
80
  type SelectionRange = {
76
81
  start: number;
@@ -138,6 +143,11 @@ type ClickCustomData = {
138
143
  targetId: number;
139
144
  element?: ElementDescriptor;
140
145
  pathname?: string;
146
+ button?: number;
147
+ altKey?: boolean;
148
+ ctrlKey?: boolean;
149
+ metaKey?: boolean;
150
+ shiftKey?: boolean;
141
151
  };
142
152
  };
143
153
  type InputCustomData = {
@@ -210,6 +220,9 @@ type ConsoleLogPluginData = {
210
220
  };
211
221
  };
212
222
  type NetworkRequestInitiator = "fetch" | "xhr";
223
+ type GraphQLRequestMetadata = {
224
+ operationName: string;
225
+ };
213
226
  /**
214
227
  * Plugin event data emitted by the recorder for network requests.
215
228
  */
@@ -223,6 +236,7 @@ type NetworkRequestPluginData = {
223
236
  durationMs: number;
224
237
  requestSize?: number;
225
238
  responseSize?: number;
239
+ graphql?: GraphQLRequestMetadata;
226
240
  };
227
241
  };
228
242
  type RouteChangePluginData = {
@@ -330,4 +344,4 @@ declare function validateKey(key: string): string | null;
330
344
  declare function validateTagValue(value: string | undefined): string | null;
331
345
  declare function validateMeasureValue(value: number | undefined): string | null;
332
346
  //#endregion
333
- 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, 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 };
347
+ 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
@@ -70,7 +70,12 @@ type MouseInteractionData = {
70
70
  id: number;
71
71
  x?: number;
72
72
  y?: number;
73
- pointerType?: number;
73
+ pointerType?: number; /** Mouse button and modifier keys present on click events. */
74
+ button?: number;
75
+ altKey?: boolean;
76
+ ctrlKey?: boolean;
77
+ metaKey?: boolean;
78
+ shiftKey?: boolean;
74
79
  };
75
80
  type SelectionRange = {
76
81
  start: number;
@@ -138,6 +143,11 @@ type ClickCustomData = {
138
143
  targetId: number;
139
144
  element?: ElementDescriptor;
140
145
  pathname?: string;
146
+ button?: number;
147
+ altKey?: boolean;
148
+ ctrlKey?: boolean;
149
+ metaKey?: boolean;
150
+ shiftKey?: boolean;
141
151
  };
142
152
  };
143
153
  type InputCustomData = {
@@ -210,6 +220,9 @@ type ConsoleLogPluginData = {
210
220
  };
211
221
  };
212
222
  type NetworkRequestInitiator = "fetch" | "xhr";
223
+ type GraphQLRequestMetadata = {
224
+ operationName: string;
225
+ };
213
226
  /**
214
227
  * Plugin event data emitted by the recorder for network requests.
215
228
  */
@@ -223,6 +236,7 @@ type NetworkRequestPluginData = {
223
236
  durationMs: number;
224
237
  requestSize?: number;
225
238
  responseSize?: number;
239
+ graphql?: GraphQLRequestMetadata;
226
240
  };
227
241
  };
228
242
  type RouteChangePluginData = {
@@ -330,4 +344,4 @@ declare function validateKey(key: string): string | null;
330
344
  declare function validateTagValue(value: string | undefined): string | null;
331
345
  declare function validateMeasureValue(value: number | undefined): string | null;
332
346
  //#endregion
333
- 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, 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 };
347
+ 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.5",
4
+ "version": "0.18.7",
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
@@ -72,6 +72,12 @@ export type MouseInteractionData = {
72
72
  x?: number;
73
73
  y?: number;
74
74
  pointerType?: number;
75
+ /** Mouse button and modifier keys present on click events. */
76
+ button?: number;
77
+ altKey?: boolean;
78
+ ctrlKey?: boolean;
79
+ metaKey?: boolean;
80
+ shiftKey?: boolean;
75
81
  };
76
82
 
77
83
  export type SelectionRange = {
@@ -148,6 +154,11 @@ export type ClickCustomData = {
148
154
  targetId: number;
149
155
  element?: ElementDescriptor;
150
156
  pathname?: string;
157
+ button?: number;
158
+ altKey?: boolean;
159
+ ctrlKey?: boolean;
160
+ metaKey?: boolean;
161
+ shiftKey?: boolean;
151
162
  };
152
163
  };
153
164
 
@@ -230,6 +241,10 @@ export type ConsoleLogPluginData = {
230
241
 
231
242
  export type NetworkRequestInitiator = 'fetch' | 'xhr';
232
243
 
244
+ export type GraphQLRequestMetadata = {
245
+ operationName: string;
246
+ };
247
+
233
248
  /**
234
249
  * Plugin event data emitted by the recorder for network requests.
235
250
  */
@@ -243,6 +258,7 @@ export type NetworkRequestPluginData = {
243
258
  durationMs: number;
244
259
  requestSize?: number;
245
260
  responseSize?: number;
261
+ graphql?: GraphQLRequestMetadata;
246
262
  };
247
263
  };
248
264
 
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export {
23
23
  type ConsoleLogLevel,
24
24
  type ConsoleLogPluginData,
25
25
  type NetworkRequestInitiator,
26
+ type GraphQLRequestMetadata,
26
27
  type NetworkRequestPluginData,
27
28
  type RouteChangeTrigger,
28
29
  type RouteChangePayload,