@clue-ai/browser-sdk 0.0.1

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.
Files changed (102) hide show
  1. package/README.md +100 -0
  2. package/dist/authoring/overlay.d.ts +12 -0
  3. package/dist/authoring/overlay.js +468 -0
  4. package/dist/authoring/recording.d.ts +125 -0
  5. package/dist/authoring/recording.js +481 -0
  6. package/dist/authoring/service-logo.d.ts +1 -0
  7. package/dist/authoring/service-logo.generated.d.ts +1 -0
  8. package/dist/authoring/service-logo.generated.js +3 -0
  9. package/dist/authoring/service-logo.js +1 -0
  10. package/dist/authoring/session.d.ts +23 -0
  11. package/dist/authoring/session.js +127 -0
  12. package/dist/authoring/surface.d.ts +11 -0
  13. package/dist/authoring/surface.js +63 -0
  14. package/dist/authoring/toolbar-constants.d.ts +23 -0
  15. package/dist/authoring/toolbar-constants.js +42 -0
  16. package/dist/authoring/toolbar-drag.d.ts +29 -0
  17. package/dist/authoring/toolbar-drag.js +270 -0
  18. package/dist/authoring/toolbar-view.d.ts +21 -0
  19. package/dist/authoring/toolbar-view.js +2584 -0
  20. package/dist/capture/action.d.ts +2 -0
  21. package/dist/capture/action.js +62 -0
  22. package/dist/capture/dom.d.ts +23 -0
  23. package/dist/capture/dom.js +329 -0
  24. package/dist/capture/drag.d.ts +2 -0
  25. package/dist/capture/drag.js +75 -0
  26. package/dist/capture/error.d.ts +2 -0
  27. package/dist/capture/error.js +193 -0
  28. package/dist/capture/form.d.ts +2 -0
  29. package/dist/capture/form.js +137 -0
  30. package/dist/capture/frustration.d.ts +2 -0
  31. package/dist/capture/frustration.js +171 -0
  32. package/dist/capture/input.d.ts +2 -0
  33. package/dist/capture/input.js +109 -0
  34. package/dist/capture/location.d.ts +10 -0
  35. package/dist/capture/location.js +42 -0
  36. package/dist/capture/navigation.d.ts +2 -0
  37. package/dist/capture/navigation.js +100 -0
  38. package/dist/capture/network.d.ts +13 -0
  39. package/dist/capture/network.js +903 -0
  40. package/dist/capture/page.d.ts +2 -0
  41. package/dist/capture/page.js +78 -0
  42. package/dist/capture/performance.d.ts +2 -0
  43. package/dist/capture/performance.js +268 -0
  44. package/dist/context/account.d.ts +12 -0
  45. package/dist/context/account.js +129 -0
  46. package/dist/context/environment.d.ts +42 -0
  47. package/dist/context/environment.js +208 -0
  48. package/dist/context/identity.d.ts +14 -0
  49. package/dist/context/identity.js +123 -0
  50. package/dist/context/session.d.ts +28 -0
  51. package/dist/context/session.js +155 -0
  52. package/dist/context/tab.d.ts +22 -0
  53. package/dist/context/tab.js +142 -0
  54. package/dist/context/trace.d.ts +32 -0
  55. package/dist/context/trace.js +65 -0
  56. package/dist/core/config.d.ts +4 -0
  57. package/dist/core/config.js +199 -0
  58. package/dist/core/constants.d.ts +43 -0
  59. package/dist/core/constants.js +109 -0
  60. package/dist/core/contracts.d.ts +58 -0
  61. package/dist/core/contracts.js +53 -0
  62. package/dist/core/sdk.d.ts +2 -0
  63. package/dist/core/sdk.js +831 -0
  64. package/dist/core/types.d.ts +413 -0
  65. package/dist/core/types.js +1 -0
  66. package/dist/core/usage-governor.d.ts +7 -0
  67. package/dist/core/usage-governor.js +127 -0
  68. package/dist/index.d.ts +17 -0
  69. package/dist/index.js +36 -0
  70. package/dist/integrations/next-router.d.ts +16 -0
  71. package/dist/integrations/next-router.js +18 -0
  72. package/dist/integrations/react-router.d.ts +7 -0
  73. package/dist/integrations/react-router.js +37 -0
  74. package/dist/internal/metrics.d.ts +9 -0
  75. package/dist/internal/metrics.js +38 -0
  76. package/dist/normalize/builders.d.ts +15 -0
  77. package/dist/normalize/builders.js +786 -0
  78. package/dist/normalize/canonical.d.ts +13 -0
  79. package/dist/normalize/canonical.js +77 -0
  80. package/dist/normalize/event-id.d.ts +8 -0
  81. package/dist/normalize/event-id.js +39 -0
  82. package/dist/normalize/path-template.d.ts +1 -0
  83. package/dist/normalize/path-template.js +33 -0
  84. package/dist/privacy/local-minimization.d.ts +29 -0
  85. package/dist/privacy/local-minimization.js +88 -0
  86. package/dist/privacy/mask.d.ts +7 -0
  87. package/dist/privacy/mask.js +60 -0
  88. package/dist/privacy/parameter-snapshot.d.ts +14 -0
  89. package/dist/privacy/parameter-snapshot.js +206 -0
  90. package/dist/privacy/sanitize.d.ts +11 -0
  91. package/dist/privacy/sanitize.js +145 -0
  92. package/dist/privacy/schema-evidence.d.ts +20 -0
  93. package/dist/privacy/schema-evidence.js +238 -0
  94. package/dist/transport/batch.d.ts +37 -0
  95. package/dist/transport/batch.js +182 -0
  96. package/dist/transport/client.d.ts +61 -0
  97. package/dist/transport/client.js +267 -0
  98. package/dist/transport/queue.d.ts +22 -0
  99. package/dist/transport/queue.js +56 -0
  100. package/dist/transport/retry.d.ts +14 -0
  101. package/dist/transport/retry.js +46 -0
  102. package/package.json +38 -0
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @clue-ai/browser-sdk
2
+
3
+ Clue browser ingest SDK.
4
+
5
+ ## Minimal integration
6
+
7
+ After `pnpm add @clue-ai/browser-sdk`, the intended integration is:
8
+
9
+ ```ts
10
+ import { ClueInit } from "@clue-ai/browser-sdk";
11
+
12
+ ClueInit({
13
+ endpoint: process.env.NEXT_PUBLIC_CLUE_INGEST_ENDPOINT!,
14
+ projectKey: "...",
15
+ environment: "production",
16
+ });
17
+ ```
18
+
19
+ `projectKey` is the public ingest key. The API resolves authoritative
20
+ `tenantId`, `projectId`, and `environmentId` from that key. `endpoint` must be
21
+ injected from the host app and passed to `ClueInit`.
22
+
23
+ ## Responsibility boundary
24
+
25
+ - SDK captures browser signals and applies local minimization where the signal is obviously high-risk.
26
+ - API ingest privacy gate is authoritative for final allow/deny/unmask policy.
27
+ - Worker-side sanitize is defense-in-depth only.
28
+ - SDK sends `projectKey`, `environment`, `schemaVersion`, `sdkType`, `sdkVersion`, stable keys, and minimized capture payloads.
29
+ - SDK does **not** authoritatively stamp `tenant_id`, `project_id`, `environment_id`, or final archive-safe request/response payloads.
30
+
31
+ The browser SDK does **not** own final archive-safe allowlist decisions.
32
+
33
+ ## Default privacy/minimization behavior
34
+
35
+ The browser SDK avoids shipping obvious high-risk plaintext and keeps the MVP
36
+ default event surface intentionally narrow.
37
+
38
+ - `input_change.value` -> local value metadata only
39
+ - `selection_text` -> metadata only
40
+ - raw selector/path -> never serialized; stable key only
41
+
42
+ Current MVP defaults:
43
+
44
+ - successful network emits one `request_finished`
45
+ - failed network emits one `request_failed`
46
+ - standard action capture emits `element_clicked`, `form_submitted`,
47
+ `input_committed`, `toggle_changed`, `selection_committed`,
48
+ `file_selected`, and `drag_drop_completed`
49
+ - standard support capture emits only frustration signals
50
+ (`dead_click_detected`, `error_click_detected`, `rage_click_detected`)
51
+ - normal batching flushes when the queued payload reaches the 48KB send
52
+ threshold, when no new event is added for 3 minutes, or when the page is
53
+ leaving
54
+ - event type does not change the delivery rule in MVP
55
+
56
+ Stable key precedence:
57
+
58
+ 1. `data-testid`
59
+ 2. `data-qa`
60
+ 3. safe `name`
61
+ 4. safe `aria-label`
62
+ 5. structural fallback
63
+
64
+ `data-clue-id` and `data-clue-key` are not part of the MVP stable-key
65
+ contract. Captured stable keys are best-effort evidence, not authoritative
66
+ business meaning.
67
+
68
+ ## Sampling and cost guards
69
+
70
+ - `sampling.sessionSampleRate` controls whether a session emits normal capture
71
+ - oversized events degrade in stages before final drop:
72
+ - payload body removed
73
+ - metadata/schema only
74
+ - shell only
75
+ - oversized batches stay below the configured payload hard max
76
+
77
+ ## Flush reliability
78
+
79
+ The SDK flushes on:
80
+
81
+ - `visibilitychange` -> `hidden`
82
+ - `pagehide`
83
+ - `beforeunload`
84
+
85
+ The page-leave flush is best effort. The SDK does not persist unsent events in
86
+ `localStorage` or `IndexedDB` in MVP.
87
+
88
+ ## Beacon contract
89
+
90
+ - unload / hidden beacon sends only the request body
91
+ - ingest must be able to resolve `projectKey` and `environment` from the body
92
+ - custom auth headers cannot be assumed on the beacon path
93
+
94
+ ## Degraded event contract
95
+
96
+ Downstream ingest must accept these as normal input:
97
+
98
+ - no raw payload body
99
+ - metadata + schema only
100
+ - shell-only event
@@ -0,0 +1,12 @@
1
+ import type { AuthoringRecordingController } from "./recording";
2
+ declare function resolveAuthoringBaseEndpoint(ingestEndpoint: string): string;
3
+ declare function resolveAuthoringActionUnitsEndpoint(ingestEndpoint: string): string;
4
+ export { resolveAuthoringBaseEndpoint, resolveAuthoringActionUnitsEndpoint };
5
+ export declare function startBusinessEventAuthoringOverlay(options: {
6
+ ingestEndpoint: string;
7
+ recording: AuthoringRecordingController;
8
+ onExit?: () => void;
9
+ }): {
10
+ stop: () => void;
11
+ exit: () => void;
12
+ } | null;
@@ -0,0 +1,468 @@
1
+ import { clearPersistedBusinessEventAuthoringToken, isBusinessEventAuthoringSession, persistBusinessEventAuthoringToken, readPersistedBusinessEventAuthoringToken, } from "./session";
2
+ import { AUTHORING_INTERNAL_REQUEST_HEADER, AUTHORING_INTERNAL_REQUEST_HEADER_VALUE, } from "./surface";
3
+ import { mountToolbar } from "./toolbar-view";
4
+ function notifyOpener(session) {
5
+ if (!window.opener) {
6
+ return;
7
+ }
8
+ window.opener.postMessage({
9
+ type: "clue-business-event-authoring-ready",
10
+ sessionId: session.session_id,
11
+ targetOrigin: window.location.origin,
12
+ href: window.location.href,
13
+ title: document.title,
14
+ }, session.clue_origin);
15
+ }
16
+ function reportOverlayError(phase, error) {
17
+ if (typeof console === "undefined") {
18
+ return;
19
+ }
20
+ console.error("[clue] business event authoring overlay failed", {
21
+ phase,
22
+ error,
23
+ });
24
+ }
25
+ function buildLocalPausedSession(session, nowIso) {
26
+ return {
27
+ ...session,
28
+ status: "paused",
29
+ recording_windows: session.recording_windows.map((window, index, windows) => {
30
+ if (index !== windows.length - 1 || window.end_at !== null) {
31
+ return window;
32
+ }
33
+ return {
34
+ start_at: window.start_at,
35
+ end_at: nowIso,
36
+ };
37
+ }),
38
+ };
39
+ }
40
+ function shouldKeepAuthoringStreamOpen(session) {
41
+ return (session !== null &&
42
+ session.status !== "ended" &&
43
+ session.recording_windows.length > 0);
44
+ }
45
+ function resolveAuthoringBaseEndpoint(ingestEndpoint) {
46
+ const url = new URL(ingestEndpoint);
47
+ if (url.pathname.endsWith("/ingest/browser")) {
48
+ url.pathname = url.pathname.replace(/\/ingest\/browser$/, "/business-events/authoring");
49
+ return url.toString().replace(/\/$/, "");
50
+ }
51
+ url.pathname = "/api/v1/business-events/authoring";
52
+ url.search = "";
53
+ url.hash = "";
54
+ return url.toString().replace(/\/$/, "");
55
+ }
56
+ function resolveBootstrapEndpoint(ingestEndpoint) {
57
+ return `${resolveAuthoringBaseEndpoint(ingestEndpoint)}/bootstrap`;
58
+ }
59
+ function resolveAuthoringActionUnitsEndpoint(ingestEndpoint) {
60
+ return `${resolveAuthoringBaseEndpoint(ingestEndpoint)}/action-units`;
61
+ }
62
+ function resolveAuthoringActionUnitsSnapshotEndpoint(ingestEndpoint) {
63
+ return `${resolveAuthoringBaseEndpoint(ingestEndpoint)}/action-units/snapshot`;
64
+ }
65
+ function resolveAuthoringActionUnitsStreamEndpoint(ingestEndpoint) {
66
+ return `${resolveAuthoringBaseEndpoint(ingestEndpoint)}/action-units/stream`;
67
+ }
68
+ function resolveAuthoringRecordingControlEndpoint(ingestEndpoint, action) {
69
+ return `${resolveAuthoringBaseEndpoint(ingestEndpoint)}/recording/${action}`;
70
+ }
71
+ function resolveAuthoringSaveEndpoint(ingestEndpoint) {
72
+ return `${resolveAuthoringBaseEndpoint(ingestEndpoint)}/save`;
73
+ }
74
+ function isAuthoringActionUnit(value) {
75
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
76
+ return false;
77
+ }
78
+ const actionUnit = value;
79
+ return (typeof actionUnit.action_unit_id === "string" &&
80
+ typeof actionUnit.start_at === "string" &&
81
+ typeof actionUnit.end_at === "string" &&
82
+ typeof actionUnit.summary_label === "string" &&
83
+ typeof actionUnit.status === "string" &&
84
+ Array.isArray(actionUnit.atoms) &&
85
+ Array.isArray(actionUnit.request_spans));
86
+ }
87
+ function isAuthoringStreamEvent(value) {
88
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
89
+ return false;
90
+ }
91
+ const event = value;
92
+ if (event.type === "heartbeat") {
93
+ return typeof event.session_id === "string";
94
+ }
95
+ if (event.type === "recording_state_changed") {
96
+ return (typeof event.session_id === "string" &&
97
+ typeof event.sequence === "number" &&
98
+ isBusinessEventAuthoringSession(event.session));
99
+ }
100
+ if (event.type === "action_unit_upserted" ||
101
+ event.type === "action_unit_closed") {
102
+ return (typeof event.session_id === "string" &&
103
+ typeof event.sequence === "number" &&
104
+ isAuthoringActionUnit(event.action_unit));
105
+ }
106
+ return false;
107
+ }
108
+ async function readJsonResponse(response) {
109
+ return response.json().catch(() => null);
110
+ }
111
+ function buildAuthoringRequestHeaders() {
112
+ return {
113
+ "content-type": "application/json",
114
+ [AUTHORING_INTERNAL_REQUEST_HEADER]: AUTHORING_INTERNAL_REQUEST_HEADER_VALUE,
115
+ };
116
+ }
117
+ async function bootstrapAuthoringSession(bootstrapEndpoint, token) {
118
+ const response = await fetch(bootstrapEndpoint, {
119
+ method: "POST",
120
+ headers: buildAuthoringRequestHeaders(),
121
+ body: JSON.stringify({ token }),
122
+ credentials: "omit",
123
+ });
124
+ if (!response.ok) {
125
+ throw new Error(`authoring bootstrap failed: ${response.status}`);
126
+ }
127
+ const payload = await readJsonResponse(response);
128
+ const session = payload && typeof payload === "object" ? payload.session : null;
129
+ if (!isBusinessEventAuthoringSession(session)) {
130
+ throw new Error("authoring bootstrap returned an invalid session");
131
+ }
132
+ return session;
133
+ }
134
+ async function fetchAuthoringActionUnitSnapshot(params) {
135
+ const response = await fetch(params.snapshotEndpoint, {
136
+ method: "POST",
137
+ headers: buildAuthoringRequestHeaders(),
138
+ body: JSON.stringify({
139
+ token: params.token,
140
+ limit: 200,
141
+ }),
142
+ credentials: "omit",
143
+ });
144
+ if (!response.ok) {
145
+ throw new Error(`authoring snapshot failed: ${response.status}`);
146
+ }
147
+ const payload = await readJsonResponse(response);
148
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
149
+ throw new Error("authoring snapshot returned an invalid payload");
150
+ }
151
+ const session = payload.session;
152
+ const actionUnits = payload.action_units;
153
+ if (!isBusinessEventAuthoringSession(session) || !Array.isArray(actionUnits)) {
154
+ throw new Error("authoring snapshot returned an invalid payload");
155
+ }
156
+ return {
157
+ session,
158
+ action_units: actionUnits.filter(isAuthoringActionUnit),
159
+ };
160
+ }
161
+ async function controlAuthoringRecording(params) {
162
+ const response = await fetch(params.endpoint, {
163
+ method: "POST",
164
+ headers: buildAuthoringRequestHeaders(),
165
+ body: JSON.stringify({
166
+ token: params.token,
167
+ }),
168
+ credentials: "omit",
169
+ });
170
+ if (!response.ok) {
171
+ throw new Error(`authoring recording control failed: ${response.status}`);
172
+ }
173
+ const payload = await readJsonResponse(response);
174
+ const session = payload && typeof payload === "object" ? payload.session : null;
175
+ if (!isBusinessEventAuthoringSession(session)) {
176
+ throw new Error("authoring recording control returned an invalid session");
177
+ }
178
+ return session;
179
+ }
180
+ async function saveAuthoringSelection(params) {
181
+ const response = await fetch(params.endpoint, {
182
+ method: "POST",
183
+ headers: buildAuthoringRequestHeaders(),
184
+ body: JSON.stringify({
185
+ token: params.token,
186
+ title: params.title,
187
+ description: params.description,
188
+ save_mode: params.saveMode,
189
+ selected_action_unit_ids: params.selectedActionUnitIds,
190
+ }),
191
+ credentials: "omit",
192
+ });
193
+ if (!response.ok) {
194
+ throw new Error(`authoring save failed: ${response.status}`);
195
+ }
196
+ return readJsonResponse(response);
197
+ }
198
+ export { resolveAuthoringBaseEndpoint, resolveAuthoringActionUnitsEndpoint };
199
+ export function startBusinessEventAuthoringOverlay(options) {
200
+ if (typeof window === "undefined" || typeof document === "undefined") {
201
+ return null;
202
+ }
203
+ const token = readPersistedBusinessEventAuthoringToken();
204
+ if (!token) {
205
+ return null;
206
+ }
207
+ persistBusinessEventAuthoringToken(token);
208
+ let mountedToolbar = null;
209
+ let streamSource = null;
210
+ let streamRetryTimer = null;
211
+ let isStopped = false;
212
+ let isExited = false;
213
+ let currentSession = null;
214
+ const exitAuthoring = () => {
215
+ if (isExited) {
216
+ return;
217
+ }
218
+ isExited = true;
219
+ options.recording.reset();
220
+ options.onExit?.();
221
+ };
222
+ const stopOverlay = (params) => {
223
+ if (isStopped) {
224
+ return;
225
+ }
226
+ isStopped = true;
227
+ if (streamRetryTimer !== null) {
228
+ window.clearTimeout(streamRetryTimer);
229
+ streamRetryTimer = null;
230
+ }
231
+ streamSource?.close();
232
+ streamSource = null;
233
+ mountedToolbar?.dispose();
234
+ mountedToolbar?.root.remove();
235
+ delete document.documentElement.dataset.clueBusinessEventAuthoring;
236
+ if (params?.clearSessionToken) {
237
+ clearPersistedBusinessEventAuthoringToken();
238
+ }
239
+ exitAuthoring();
240
+ };
241
+ const syncSession = (session) => {
242
+ currentSession = session;
243
+ options.recording.syncSession({
244
+ status: session.status,
245
+ recording_windows: session.recording_windows,
246
+ });
247
+ if (!shouldKeepAuthoringStreamOpen(session)) {
248
+ if (streamRetryTimer !== null) {
249
+ window.clearTimeout(streamRetryTimer);
250
+ streamRetryTimer = null;
251
+ }
252
+ streamSource?.close();
253
+ streamSource = null;
254
+ return;
255
+ }
256
+ ensureStream();
257
+ };
258
+ const syncSnapshot = async () => {
259
+ const snapshot = await fetchAuthoringActionUnitSnapshot({
260
+ snapshotEndpoint: resolveAuthoringActionUnitsSnapshotEndpoint(options.ingestEndpoint),
261
+ token,
262
+ });
263
+ if (isStopped) {
264
+ return;
265
+ }
266
+ syncSession(snapshot.session);
267
+ options.recording.replaceActionUnits(snapshot.action_units);
268
+ };
269
+ const ensureStream = () => {
270
+ if (isStopped || !shouldKeepAuthoringStreamOpen(currentSession)) {
271
+ if (streamRetryTimer !== null) {
272
+ window.clearTimeout(streamRetryTimer);
273
+ streamRetryTimer = null;
274
+ }
275
+ streamSource?.close();
276
+ streamSource = null;
277
+ return;
278
+ }
279
+ if (streamSource) {
280
+ return;
281
+ }
282
+ if (streamRetryTimer !== null) {
283
+ window.clearTimeout(streamRetryTimer);
284
+ streamRetryTimer = null;
285
+ }
286
+ const streamUrl = new URL(resolveAuthoringActionUnitsStreamEndpoint(options.ingestEndpoint));
287
+ streamUrl.searchParams.set("token", token);
288
+ const source = new EventSource(streamUrl.toString());
289
+ streamSource = source;
290
+ source.onmessage = (message) => {
291
+ if (isStopped) {
292
+ return;
293
+ }
294
+ try {
295
+ const parsed = JSON.parse(message.data);
296
+ if (!isAuthoringStreamEvent(parsed)) {
297
+ return;
298
+ }
299
+ if (parsed.type === "heartbeat") {
300
+ return;
301
+ }
302
+ if (parsed.type === "recording_state_changed") {
303
+ syncSession(parsed.session);
304
+ return;
305
+ }
306
+ options.recording.upsertActionUnit(parsed.action_unit);
307
+ }
308
+ catch (error) {
309
+ reportOverlayError("stream", error);
310
+ }
311
+ };
312
+ source.onerror = () => {
313
+ if (isStopped) {
314
+ return;
315
+ }
316
+ source.close();
317
+ if (streamSource === source) {
318
+ streamSource = null;
319
+ }
320
+ if (!shouldKeepAuthoringStreamOpen(currentSession) ||
321
+ streamRetryTimer !== null) {
322
+ return;
323
+ }
324
+ streamRetryTimer = window.setTimeout(() => {
325
+ streamRetryTimer = null;
326
+ ensureStream();
327
+ }, 3000);
328
+ };
329
+ };
330
+ const setRecording = async (shouldRecord) => {
331
+ const session = currentSession;
332
+ if (!session) {
333
+ return;
334
+ }
335
+ const nextAction = shouldRecord
336
+ ? session.recording_windows.length === 0
337
+ ? "start"
338
+ : "resume"
339
+ : "pause";
340
+ try {
341
+ const nextSession = await controlAuthoringRecording({
342
+ endpoint: resolveAuthoringRecordingControlEndpoint(options.ingestEndpoint, nextAction),
343
+ token,
344
+ });
345
+ if (isStopped) {
346
+ return;
347
+ }
348
+ syncSession(nextSession);
349
+ if (nextSession.recording_windows.length === 0) {
350
+ options.recording.replaceActionUnits([]);
351
+ return;
352
+ }
353
+ await syncSnapshot();
354
+ }
355
+ catch (error) {
356
+ if (!shouldRecord) {
357
+ syncSession(buildLocalPausedSession(session, new Date().toISOString()));
358
+ }
359
+ reportOverlayError("recording", error);
360
+ }
361
+ };
362
+ const resetRecording = async () => {
363
+ const nextSession = await controlAuthoringRecording({
364
+ endpoint: resolveAuthoringRecordingControlEndpoint(options.ingestEndpoint, "reset"),
365
+ token,
366
+ });
367
+ if (isStopped) {
368
+ return;
369
+ }
370
+ syncSession(nextSession);
371
+ options.recording.replaceActionUnits([]);
372
+ };
373
+ const endSessionAndStop = () => {
374
+ void (async () => {
375
+ try {
376
+ await controlAuthoringRecording({
377
+ endpoint: resolveAuthoringRecordingControlEndpoint(options.ingestEndpoint, "end"),
378
+ token,
379
+ });
380
+ }
381
+ catch (error) {
382
+ reportOverlayError("end", error);
383
+ }
384
+ finally {
385
+ stopOverlay({ clearSessionToken: true });
386
+ }
387
+ })();
388
+ };
389
+ void bootstrapAuthoringSession(resolveBootstrapEndpoint(options.ingestEndpoint), token)
390
+ .then((session) => {
391
+ if (isStopped) {
392
+ return;
393
+ }
394
+ if (session.target_origin !== window.location.origin) {
395
+ stopOverlay();
396
+ return;
397
+ }
398
+ persistBusinessEventAuthoringToken(token);
399
+ syncSession(session);
400
+ const mount = () => {
401
+ if (isStopped) {
402
+ return;
403
+ }
404
+ void mountToolbar(session, {
405
+ onClose: endSessionAndStop,
406
+ onReady: () => {
407
+ notifyOpener(session);
408
+ },
409
+ recording: options.recording,
410
+ onSetRecording: setRecording,
411
+ onResetRecording: resetRecording,
412
+ onSave: ({ title, description, saveMode, selectedActionUnitIds }) => saveAuthoringSelection({
413
+ endpoint: resolveAuthoringSaveEndpoint(options.ingestEndpoint),
414
+ token,
415
+ title,
416
+ description,
417
+ saveMode,
418
+ selectedActionUnitIds,
419
+ }),
420
+ })
421
+ .then(async (toolbar) => {
422
+ if (isStopped) {
423
+ toolbar.dispose();
424
+ toolbar.root.remove();
425
+ return;
426
+ }
427
+ mountedToolbar = toolbar;
428
+ if (currentSession?.recording_windows.length) {
429
+ try {
430
+ await syncSnapshot();
431
+ }
432
+ catch (error) {
433
+ reportOverlayError("snapshot", error);
434
+ }
435
+ }
436
+ })
437
+ .catch((error) => {
438
+ reportOverlayError("mount", error);
439
+ });
440
+ };
441
+ let didMount = false;
442
+ const mountOnce = () => {
443
+ if (didMount) {
444
+ return;
445
+ }
446
+ didMount = true;
447
+ mount();
448
+ };
449
+ if (document.readyState === "loading") {
450
+ document.addEventListener("DOMContentLoaded", mountOnce, { once: true });
451
+ window.setTimeout(mountOnce, 0);
452
+ return;
453
+ }
454
+ mountOnce();
455
+ })
456
+ .catch((error) => {
457
+ reportOverlayError("bootstrap", error);
458
+ stopOverlay();
459
+ });
460
+ return {
461
+ stop: () => {
462
+ stopOverlay();
463
+ },
464
+ exit: () => {
465
+ endSessionAndStop();
466
+ },
467
+ };
468
+ }
@@ -0,0 +1,125 @@
1
+ import type { BusinessEventAuthoringSession } from "./session";
2
+ export type AuthoringActionUnitAtom = {
3
+ atom_id: string;
4
+ occurred_at: string;
5
+ received_at: string;
6
+ event_category: string | null;
7
+ event_name: string;
8
+ event_role: string;
9
+ session_id: string | null;
10
+ stable_key: string | null;
11
+ ui_target_id?: string | null;
12
+ url_canonical?: string | null;
13
+ form_key: string | null;
14
+ service_key: string | null;
15
+ backend_scope: string | null;
16
+ operation_key: string | null;
17
+ screen_key: string | null;
18
+ request_kind: string | null;
19
+ result_kind: string | null;
20
+ error_kind: string | null;
21
+ route_template: string | null;
22
+ code_path_key: string | null;
23
+ container_key: string | null;
24
+ raw_properties_json?: Record<string, unknown> | null;
25
+ element_label_candidate?: string | null;
26
+ aria_label_candidate?: string | null;
27
+ placeholder_candidate?: string | null;
28
+ field_name_candidate?: string | null;
29
+ field_type?: string | null;
30
+ section_heading?: string | null;
31
+ };
32
+ export type AuthoringActionUnitRequestSpan = {
33
+ request_span_id: string;
34
+ summary_title: string;
35
+ endpoint_canonical: string;
36
+ status_kind: string;
37
+ duration_ms: number;
38
+ resource_kind: string | null;
39
+ };
40
+ export type AuthoringActionUnit = {
41
+ action_unit_id: string;
42
+ start_at: string;
43
+ end_at: string;
44
+ status: string;
45
+ unit_type: string;
46
+ primary_ui_target_id?: string | null;
47
+ route_from?: string | null;
48
+ route_to?: string | null;
49
+ summary_label: string;
50
+ primary_screen_label?: string | null;
51
+ primary_target_label?: string | null;
52
+ close_reason: string;
53
+ atom_count: number;
54
+ request_span_count: number;
55
+ algorithm_version: number;
56
+ atoms: readonly AuthoringActionUnitAtom[];
57
+ request_spans: readonly AuthoringActionUnitRequestSpan[];
58
+ };
59
+ export type AuthoringRecordedAtom = {
60
+ id: string;
61
+ title: string;
62
+ subtitle: string | null;
63
+ detailLabel: string | null;
64
+ occurredAtLabel: string;
65
+ badgeLabel: string;
66
+ roleLabel: string;
67
+ };
68
+ export type AuthoringRecordedRequestSpan = {
69
+ id: string;
70
+ title: string;
71
+ subtitle: string;
72
+ statusLabel: string;
73
+ durationLabel: string;
74
+ };
75
+ export type AuthoringHighlightTrace = {
76
+ highlightSurfaceContextKind: string;
77
+ highlightSurfaceRootId: string | null;
78
+ highlightSurfaceTitle: string | null;
79
+ candidateCountTotal: number;
80
+ candidateCountAfterContextFilter: number;
81
+ highlightMatchStage: string;
82
+ highlightRenderMode: "overlay_box" | "inline_outline" | "none";
83
+ highlightRejectReason: string | null;
84
+ top1Score: number | null;
85
+ top2Score: number | null;
86
+ usedSameSurfaceFilter: boolean;
87
+ overlayZIndex: number | null;
88
+ targetEffectiveZIndex: number | null;
89
+ surfaceEffectiveZIndex: number | null;
90
+ };
91
+ export type AuthoringRecordedEvent = {
92
+ id: string;
93
+ actionUnit: AuthoringActionUnit;
94
+ occurredAtLabel: string;
95
+ title: string;
96
+ subtitle: string | null;
97
+ screenLabel: string | null;
98
+ badgeLabel: string;
99
+ selected: boolean;
100
+ pendingRemoval: boolean;
101
+ highlightTrace: AuthoringHighlightTrace | null;
102
+ atoms: readonly AuthoringRecordedAtom[];
103
+ requestSpans: readonly AuthoringRecordedRequestSpan[];
104
+ };
105
+ export type AuthoringRecordingSnapshot = {
106
+ isRecording: boolean;
107
+ hasRecordedWindow: boolean;
108
+ entries: readonly AuthoringRecordedEvent[];
109
+ selectedCount: number;
110
+ saveTargetCount: number;
111
+ };
112
+ export type AuthoringRecordingController = {
113
+ getSnapshot: () => AuthoringRecordingSnapshot;
114
+ subscribe: (listener: (snapshot: AuthoringRecordingSnapshot) => void) => () => void;
115
+ syncSession: (session: Pick<BusinessEventAuthoringSession, "status" | "recording_windows">) => void;
116
+ toggleSelected: (eventId: string) => void;
117
+ markPendingRemoval: (eventId: string) => void;
118
+ restorePendingRemoval: (eventId: string) => void;
119
+ setHighlightTrace: (eventId: string, trace: AuthoringHighlightTrace | null) => void;
120
+ replaceActionUnits: (actionUnits: readonly AuthoringActionUnit[]) => void;
121
+ upsertActionUnit: (actionUnit: AuthoringActionUnit) => void;
122
+ reset: () => void;
123
+ };
124
+ export declare const formatActionUnitSectionTitle: (actionUnit: AuthoringActionUnit) => string;
125
+ export declare const createAuthoringRecordingController: () => AuthoringRecordingController;