@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.
- package/README.md +100 -0
- package/dist/authoring/overlay.d.ts +12 -0
- package/dist/authoring/overlay.js +468 -0
- package/dist/authoring/recording.d.ts +125 -0
- package/dist/authoring/recording.js +481 -0
- package/dist/authoring/service-logo.d.ts +1 -0
- package/dist/authoring/service-logo.generated.d.ts +1 -0
- package/dist/authoring/service-logo.generated.js +3 -0
- package/dist/authoring/service-logo.js +1 -0
- package/dist/authoring/session.d.ts +23 -0
- package/dist/authoring/session.js +127 -0
- package/dist/authoring/surface.d.ts +11 -0
- package/dist/authoring/surface.js +63 -0
- package/dist/authoring/toolbar-constants.d.ts +23 -0
- package/dist/authoring/toolbar-constants.js +42 -0
- package/dist/authoring/toolbar-drag.d.ts +29 -0
- package/dist/authoring/toolbar-drag.js +270 -0
- package/dist/authoring/toolbar-view.d.ts +21 -0
- package/dist/authoring/toolbar-view.js +2584 -0
- package/dist/capture/action.d.ts +2 -0
- package/dist/capture/action.js +62 -0
- package/dist/capture/dom.d.ts +23 -0
- package/dist/capture/dom.js +329 -0
- package/dist/capture/drag.d.ts +2 -0
- package/dist/capture/drag.js +75 -0
- package/dist/capture/error.d.ts +2 -0
- package/dist/capture/error.js +193 -0
- package/dist/capture/form.d.ts +2 -0
- package/dist/capture/form.js +137 -0
- package/dist/capture/frustration.d.ts +2 -0
- package/dist/capture/frustration.js +171 -0
- package/dist/capture/input.d.ts +2 -0
- package/dist/capture/input.js +109 -0
- package/dist/capture/location.d.ts +10 -0
- package/dist/capture/location.js +42 -0
- package/dist/capture/navigation.d.ts +2 -0
- package/dist/capture/navigation.js +100 -0
- package/dist/capture/network.d.ts +13 -0
- package/dist/capture/network.js +903 -0
- package/dist/capture/page.d.ts +2 -0
- package/dist/capture/page.js +78 -0
- package/dist/capture/performance.d.ts +2 -0
- package/dist/capture/performance.js +268 -0
- package/dist/context/account.d.ts +12 -0
- package/dist/context/account.js +129 -0
- package/dist/context/environment.d.ts +42 -0
- package/dist/context/environment.js +208 -0
- package/dist/context/identity.d.ts +14 -0
- package/dist/context/identity.js +123 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +155 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +142 -0
- package/dist/context/trace.d.ts +32 -0
- package/dist/context/trace.js +65 -0
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +199 -0
- package/dist/core/constants.d.ts +43 -0
- package/dist/core/constants.js +109 -0
- package/dist/core/contracts.d.ts +58 -0
- package/dist/core/contracts.js +53 -0
- package/dist/core/sdk.d.ts +2 -0
- package/dist/core/sdk.js +831 -0
- package/dist/core/types.d.ts +413 -0
- package/dist/core/types.js +1 -0
- package/dist/core/usage-governor.d.ts +7 -0
- package/dist/core/usage-governor.js +127 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +36 -0
- package/dist/integrations/next-router.d.ts +16 -0
- package/dist/integrations/next-router.js +18 -0
- package/dist/integrations/react-router.d.ts +7 -0
- package/dist/integrations/react-router.js +37 -0
- package/dist/internal/metrics.d.ts +9 -0
- package/dist/internal/metrics.js +38 -0
- package/dist/normalize/builders.d.ts +15 -0
- package/dist/normalize/builders.js +786 -0
- package/dist/normalize/canonical.d.ts +13 -0
- package/dist/normalize/canonical.js +77 -0
- package/dist/normalize/event-id.d.ts +8 -0
- package/dist/normalize/event-id.js +39 -0
- package/dist/normalize/path-template.d.ts +1 -0
- package/dist/normalize/path-template.js +33 -0
- package/dist/privacy/local-minimization.d.ts +29 -0
- package/dist/privacy/local-minimization.js +88 -0
- package/dist/privacy/mask.d.ts +7 -0
- package/dist/privacy/mask.js +60 -0
- package/dist/privacy/parameter-snapshot.d.ts +14 -0
- package/dist/privacy/parameter-snapshot.js +206 -0
- package/dist/privacy/sanitize.d.ts +11 -0
- package/dist/privacy/sanitize.js +145 -0
- package/dist/privacy/schema-evidence.d.ts +20 -0
- package/dist/privacy/schema-evidence.js +238 -0
- package/dist/transport/batch.d.ts +37 -0
- package/dist/transport/batch.js +182 -0
- package/dist/transport/client.d.ts +61 -0
- package/dist/transport/client.js +267 -0
- package/dist/transport/queue.d.ts +22 -0
- package/dist/transport/queue.js +56 -0
- package/dist/transport/retry.d.ts +14 -0
- package/dist/transport/retry.js +46 -0
- 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;
|