@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
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
const MAX_RECORDED_ACTION_UNITS = 200;
|
|
2
|
+
const EVENT_TITLE_LABELS = {
|
|
3
|
+
page_view: "ページ表示",
|
|
4
|
+
page_leave: "ページ離脱",
|
|
5
|
+
route_change: "画面遷移",
|
|
6
|
+
element_clicked: "要素クリック",
|
|
7
|
+
input_committed: "入力確定",
|
|
8
|
+
form_submitted: "フォーム送信",
|
|
9
|
+
toggle_changed: "切替変更",
|
|
10
|
+
selection_committed: "選択確定",
|
|
11
|
+
file_selected: "ファイル選択",
|
|
12
|
+
drag_drop_completed: "ドラッグ&ドロップ",
|
|
13
|
+
request_started: "リクエスト開始",
|
|
14
|
+
request_finished: "リクエスト完了",
|
|
15
|
+
request_failed: "リクエスト失敗",
|
|
16
|
+
error_raised: "エラー発生",
|
|
17
|
+
session_started: "セッション開始",
|
|
18
|
+
session_ended: "セッション終了",
|
|
19
|
+
flow_started: "フロー開始",
|
|
20
|
+
flow_finished: "フロー完了",
|
|
21
|
+
flow_failed: "フロー失敗",
|
|
22
|
+
dead_click_detected: "無効クリック",
|
|
23
|
+
error_click_detected: "エラークリック",
|
|
24
|
+
rage_click_detected: "連続クリック",
|
|
25
|
+
custom_emitted: "カスタムイベント",
|
|
26
|
+
unknown_observed: "未分類イベント",
|
|
27
|
+
};
|
|
28
|
+
const EVENT_CATEGORY_LABELS = {
|
|
29
|
+
session: "セッション",
|
|
30
|
+
navigation: "画面",
|
|
31
|
+
action: "操作",
|
|
32
|
+
request: "リクエスト",
|
|
33
|
+
error: "エラー",
|
|
34
|
+
flow: "フロー",
|
|
35
|
+
support: "補助",
|
|
36
|
+
custom: "カスタム",
|
|
37
|
+
};
|
|
38
|
+
const ACTION_UNIT_TYPE_LABELS = {
|
|
39
|
+
user_action: "ユーザー操作",
|
|
40
|
+
custom_action: "カスタム操作",
|
|
41
|
+
};
|
|
42
|
+
const ACTION_UNIT_STATUS_LABELS = {
|
|
43
|
+
completed: "完了",
|
|
44
|
+
failed: "失敗",
|
|
45
|
+
abandoned: "中断",
|
|
46
|
+
};
|
|
47
|
+
const ACTION_UNIT_ROLE_LABELS = {
|
|
48
|
+
action: "起点",
|
|
49
|
+
outcome: "結果",
|
|
50
|
+
context: "文脈",
|
|
51
|
+
support: "補助",
|
|
52
|
+
anchor: "起点",
|
|
53
|
+
continuation: "継続",
|
|
54
|
+
secondary: "補助",
|
|
55
|
+
noise: "ノイズ",
|
|
56
|
+
};
|
|
57
|
+
const REQUEST_STATUS_LABELS = {
|
|
58
|
+
finished: "成功",
|
|
59
|
+
failed: "失敗",
|
|
60
|
+
success: "成功",
|
|
61
|
+
failure: "失敗",
|
|
62
|
+
canceled: "中断",
|
|
63
|
+
};
|
|
64
|
+
const toTitleCase = (value) => {
|
|
65
|
+
return value
|
|
66
|
+
.split(/[\s._:/-]+/)
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.map((segment) => {
|
|
69
|
+
if (segment.length <= 2 && segment === segment.toUpperCase()) {
|
|
70
|
+
return segment;
|
|
71
|
+
}
|
|
72
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase();
|
|
73
|
+
})
|
|
74
|
+
.join(" ");
|
|
75
|
+
};
|
|
76
|
+
const HUMAN_TOKEN_MAP = {
|
|
77
|
+
api: "API",
|
|
78
|
+
drafts: "Drafts",
|
|
79
|
+
quicklink: "Quicklink",
|
|
80
|
+
quicklinks: "Quicklinks",
|
|
81
|
+
url: "URL",
|
|
82
|
+
ui: "UI",
|
|
83
|
+
};
|
|
84
|
+
const normalizeTechnicalLabel = (value) => {
|
|
85
|
+
const trimmed = value.trim();
|
|
86
|
+
if (!trimmed) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const stripped = trimmed
|
|
90
|
+
.replace(/^(?:id|name|aria|dtid|dqa|fp|struct):/i, "")
|
|
91
|
+
.replace(/^[^:]+:/, (prefix) => /^fp:/i.test(prefix) ? "" : prefix.length <= 6 ? "" : prefix);
|
|
92
|
+
if (!stripped || /^([a-f0-9]{12,}|p\d+-t\d+)$/i.test(stripped)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const tokens = stripped
|
|
96
|
+
.split(/[\s/_:.-]+/)
|
|
97
|
+
.flatMap((token) => token.split(/(?=[A-Z])/))
|
|
98
|
+
.map((token) => token.trim())
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.filter((token) => !/^[a-f0-9]{12,}$/i.test(token))
|
|
101
|
+
.filter((token) => !/^(?:headlessui|radix|react|button|input|div|span|struct|none|na)$/i.test(token));
|
|
102
|
+
if (tokens.length === 0) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return tokens
|
|
106
|
+
.map((token) => HUMAN_TOKEN_MAP[token.toLowerCase()] ?? toTitleCase(token))
|
|
107
|
+
.filter((token, index, all) => token.length > 0 && all.indexOf(token) === index)
|
|
108
|
+
.join(" ");
|
|
109
|
+
};
|
|
110
|
+
const readRawPropertyString = (rawProperties, keys) => {
|
|
111
|
+
for (const key of keys) {
|
|
112
|
+
const value = rawProperties?.[key];
|
|
113
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
114
|
+
return value.trim();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
};
|
|
119
|
+
const buildRequestLabelFromEndpoint = (endpoint) => {
|
|
120
|
+
const pathname = endpoint
|
|
121
|
+
.split("?")[0]
|
|
122
|
+
?.split("/")
|
|
123
|
+
.map((segment) => segment.trim())
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.filter((segment) => segment !== "api")
|
|
126
|
+
.filter((segment) => !segment.startsWith(":"))
|
|
127
|
+
.filter((segment) => !/^[a-f0-9-]{8,}$/i.test(segment));
|
|
128
|
+
const lastSegment = pathname && pathname.length > 0 ? pathname[pathname.length - 1] ?? null : null;
|
|
129
|
+
const normalized = lastSegment ? normalizeTechnicalLabel(lastSegment) : null;
|
|
130
|
+
return normalized ? `${normalized} API` : "API リクエスト";
|
|
131
|
+
};
|
|
132
|
+
const buildAtomDetailLabel = (atom, requestSpans) => {
|
|
133
|
+
const explicitLabel = atom.element_label_candidate ??
|
|
134
|
+
atom.aria_label_candidate ??
|
|
135
|
+
atom.field_name_candidate ??
|
|
136
|
+
atom.placeholder_candidate ??
|
|
137
|
+
readRawPropertyString(atom.raw_properties_json, [
|
|
138
|
+
"target_label",
|
|
139
|
+
"ui_target_display_name",
|
|
140
|
+
"field_label",
|
|
141
|
+
"button_text",
|
|
142
|
+
"visible_text",
|
|
143
|
+
"accessibility_label",
|
|
144
|
+
"placeholder",
|
|
145
|
+
"placeholder_text",
|
|
146
|
+
]) ??
|
|
147
|
+
normalizeTechnicalLabel(atom.ui_target_id ?? "") ??
|
|
148
|
+
normalizeTechnicalLabel(atom.stable_key ?? "") ??
|
|
149
|
+
normalizeTechnicalLabel(atom.form_key ?? "");
|
|
150
|
+
if (atom.event_name === "request_finished" ||
|
|
151
|
+
atom.event_name === "request_failed") {
|
|
152
|
+
const requestSummary = requestSpans.find((requestSpan) => typeof requestSpan.summary_title === "string" &&
|
|
153
|
+
requestSpan.summary_title.trim().length > 0)?.summary_title ?? null;
|
|
154
|
+
return requestSummary ?? buildRequestLabelFromEndpoint(requestSpans[0]?.endpoint_canonical ?? atom.backend_scope ?? atom.service_key ?? "");
|
|
155
|
+
}
|
|
156
|
+
if (explicitLabel) {
|
|
157
|
+
const descriptor = atom.event_name === "input_committed" || atom.field_type
|
|
158
|
+
? "入力欄"
|
|
159
|
+
: atom.event_name === "element_clicked"
|
|
160
|
+
? "ボタン"
|
|
161
|
+
: null;
|
|
162
|
+
return descriptor && !explicitLabel.endsWith(descriptor)
|
|
163
|
+
? `${explicitLabel} ${descriptor}`
|
|
164
|
+
: explicitLabel;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
};
|
|
168
|
+
const formatEventTitle = (eventName) => {
|
|
169
|
+
return EVENT_TITLE_LABELS[eventName] ?? toTitleCase(eventName);
|
|
170
|
+
};
|
|
171
|
+
const formatEventCategoryLabel = (eventCategory) => {
|
|
172
|
+
if (!eventCategory) {
|
|
173
|
+
return "イベント";
|
|
174
|
+
}
|
|
175
|
+
const normalized = eventCategory.trim().toLowerCase();
|
|
176
|
+
return (EVENT_CATEGORY_LABELS[normalized] ??
|
|
177
|
+
EVENT_CATEGORY_LABELS[eventCategory] ??
|
|
178
|
+
toTitleCase(eventCategory));
|
|
179
|
+
};
|
|
180
|
+
const formatActionUnitTypeLabel = (unitType) => {
|
|
181
|
+
if (!unitType || unitType.trim().length === 0) {
|
|
182
|
+
return "操作";
|
|
183
|
+
}
|
|
184
|
+
const normalized = unitType.trim().toLowerCase();
|
|
185
|
+
return (ACTION_UNIT_TYPE_LABELS[normalized] ??
|
|
186
|
+
ACTION_UNIT_TYPE_LABELS[unitType] ??
|
|
187
|
+
toTitleCase(unitType));
|
|
188
|
+
};
|
|
189
|
+
const formatActionUnitStatusLabel = (status) => {
|
|
190
|
+
if (!status || status.trim().length === 0) {
|
|
191
|
+
return "完了";
|
|
192
|
+
}
|
|
193
|
+
const normalized = status.trim().toLowerCase();
|
|
194
|
+
return (ACTION_UNIT_STATUS_LABELS[normalized] ??
|
|
195
|
+
ACTION_UNIT_STATUS_LABELS[status] ??
|
|
196
|
+
toTitleCase(status));
|
|
197
|
+
};
|
|
198
|
+
const formatActionUnitRoleLabel = (role) => {
|
|
199
|
+
if (!role || role.trim().length === 0) {
|
|
200
|
+
return "補助";
|
|
201
|
+
}
|
|
202
|
+
const normalized = role.trim().toLowerCase();
|
|
203
|
+
return (ACTION_UNIT_ROLE_LABELS[normalized] ??
|
|
204
|
+
ACTION_UNIT_ROLE_LABELS[role] ??
|
|
205
|
+
toTitleCase(role));
|
|
206
|
+
};
|
|
207
|
+
const formatRequestStatusLabel = (status) => {
|
|
208
|
+
if (!status || status.trim().length === 0) {
|
|
209
|
+
return "結果不明";
|
|
210
|
+
}
|
|
211
|
+
const normalized = status.trim().toLowerCase();
|
|
212
|
+
return (REQUEST_STATUS_LABELS[normalized] ??
|
|
213
|
+
REQUEST_STATUS_LABELS[status] ??
|
|
214
|
+
toTitleCase(status));
|
|
215
|
+
};
|
|
216
|
+
const formatOccurredAtLabel = (occurredAt) => {
|
|
217
|
+
const parsed = new Date(occurredAt);
|
|
218
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
219
|
+
return "";
|
|
220
|
+
}
|
|
221
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
222
|
+
hour: "2-digit",
|
|
223
|
+
minute: "2-digit",
|
|
224
|
+
second: "2-digit",
|
|
225
|
+
}).format(parsed);
|
|
226
|
+
};
|
|
227
|
+
const formatDurationLabel = (durationMs) => {
|
|
228
|
+
if (durationMs < 1000) {
|
|
229
|
+
return `${durationMs}ms`;
|
|
230
|
+
}
|
|
231
|
+
return `${(durationMs / 1000).toFixed(1)}s`;
|
|
232
|
+
};
|
|
233
|
+
const formatScreenLabelFromScreenKey = (screenKey) => {
|
|
234
|
+
if (!screenKey) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const segments = screenKey
|
|
238
|
+
.split("/")
|
|
239
|
+
.map((segment) => segment.trim())
|
|
240
|
+
.filter((segment) => segment.length > 0)
|
|
241
|
+
.filter((segment) => !segment.startsWith("{") &&
|
|
242
|
+
!segment.startsWith(":") &&
|
|
243
|
+
!segment.startsWith("<"));
|
|
244
|
+
if (segments.length <= 1) {
|
|
245
|
+
return "ホーム画面";
|
|
246
|
+
}
|
|
247
|
+
const lastSegment = segments[segments.length - 1];
|
|
248
|
+
if (!lastSegment) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const label = toTitleCase(lastSegment);
|
|
252
|
+
return `${label}画面`;
|
|
253
|
+
};
|
|
254
|
+
const formatActionUnitSubtitle = (actionUnit) => {
|
|
255
|
+
const humanLabel = actionUnit.primary_target_label ?? actionUnit.primary_screen_label ?? null;
|
|
256
|
+
if (humanLabel && humanLabel.length > 0) {
|
|
257
|
+
return humanLabel;
|
|
258
|
+
}
|
|
259
|
+
return `${actionUnit.atom_count} atoms / ${actionUnit.request_span_count} request spans / ${formatActionUnitStatusLabel(actionUnit.status)}`;
|
|
260
|
+
};
|
|
261
|
+
const toRecordedAtom = (atom, requestSpans) => {
|
|
262
|
+
const roleLabel = formatActionUnitRoleLabel(atom.event_role);
|
|
263
|
+
return {
|
|
264
|
+
id: atom.atom_id,
|
|
265
|
+
title: `【${roleLabel}】${formatEventTitle(atom.event_name)}`,
|
|
266
|
+
subtitle: null,
|
|
267
|
+
detailLabel: buildAtomDetailLabel(atom, requestSpans),
|
|
268
|
+
occurredAtLabel: formatOccurredAtLabel(atom.occurred_at),
|
|
269
|
+
badgeLabel: formatEventCategoryLabel(atom.event_category),
|
|
270
|
+
roleLabel,
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
const toRecordedRequestSpan = (requestSpan) => ({
|
|
274
|
+
id: requestSpan.request_span_id,
|
|
275
|
+
title: requestSpan.summary_title,
|
|
276
|
+
subtitle: requestSpan.endpoint_canonical,
|
|
277
|
+
statusLabel: formatRequestStatusLabel(requestSpan.status_kind),
|
|
278
|
+
durationLabel: formatDurationLabel(requestSpan.duration_ms),
|
|
279
|
+
});
|
|
280
|
+
const toRecordedEvent = (actionUnit) => ({
|
|
281
|
+
id: actionUnit.action_unit_id,
|
|
282
|
+
actionUnit,
|
|
283
|
+
occurredAtLabel: formatOccurredAtLabel(actionUnit.start_at),
|
|
284
|
+
title: actionUnit.summary_label,
|
|
285
|
+
subtitle: formatActionUnitSubtitle(actionUnit),
|
|
286
|
+
screenLabel: formatActionUnitSectionTitle(actionUnit),
|
|
287
|
+
badgeLabel: formatActionUnitTypeLabel(actionUnit.unit_type),
|
|
288
|
+
atoms: actionUnit.atoms.map((atom) => toRecordedAtom(atom, actionUnit.request_spans)),
|
|
289
|
+
requestSpans: actionUnit.request_spans.map(toRecordedRequestSpan),
|
|
290
|
+
});
|
|
291
|
+
export const formatActionUnitSectionTitle = (actionUnit) => {
|
|
292
|
+
const startScreenKey = actionUnit.atoms.find((atom) => atom.screen_key && atom.screen_key.length > 0)
|
|
293
|
+
?.screen_key ?? null;
|
|
294
|
+
return (formatScreenLabelFromScreenKey(startScreenKey) ??
|
|
295
|
+
actionUnit.primary_screen_label ??
|
|
296
|
+
"イベント");
|
|
297
|
+
};
|
|
298
|
+
const isWithinAnyWindow = (occurredAtMs, windows) => {
|
|
299
|
+
return windows.some((window) => {
|
|
300
|
+
if (occurredAtMs < window.startMs) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
if (window.endMs !== null && occurredAtMs > window.endMs) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
export const createAuthoringRecordingController = () => {
|
|
310
|
+
let isRecording = false;
|
|
311
|
+
let windows = [];
|
|
312
|
+
let entries = [];
|
|
313
|
+
const selectedEventIds = new Set();
|
|
314
|
+
const pendingRemovalEventIds = new Set();
|
|
315
|
+
const highlightTraces = new Map();
|
|
316
|
+
const listeners = new Set();
|
|
317
|
+
const buildSnapshot = () => ({
|
|
318
|
+
isRecording,
|
|
319
|
+
hasRecordedWindow: windows.length > 0,
|
|
320
|
+
entries: entries.map((entry) => ({
|
|
321
|
+
...entry,
|
|
322
|
+
selected: selectedEventIds.has(entry.id) && !pendingRemovalEventIds.has(entry.id),
|
|
323
|
+
pendingRemoval: pendingRemovalEventIds.has(entry.id),
|
|
324
|
+
highlightTrace: highlightTraces.get(entry.id) ?? null,
|
|
325
|
+
})),
|
|
326
|
+
selectedCount: [...selectedEventIds].filter((eventId) => !pendingRemovalEventIds.has(eventId)).length,
|
|
327
|
+
saveTargetCount: entries.filter((entry) => !pendingRemovalEventIds.has(entry.id)).length,
|
|
328
|
+
});
|
|
329
|
+
const emit = () => {
|
|
330
|
+
const snapshot = buildSnapshot();
|
|
331
|
+
for (const listener of listeners) {
|
|
332
|
+
listener(snapshot);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
getSnapshot() {
|
|
337
|
+
return buildSnapshot();
|
|
338
|
+
},
|
|
339
|
+
subscribe(listener) {
|
|
340
|
+
listeners.add(listener);
|
|
341
|
+
listener(buildSnapshot());
|
|
342
|
+
return () => {
|
|
343
|
+
listeners.delete(listener);
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
syncSession(session) {
|
|
347
|
+
isRecording = session.status === "recording";
|
|
348
|
+
windows = session.recording_windows
|
|
349
|
+
.map((window) => ({
|
|
350
|
+
startMs: Date.parse(window.start_at),
|
|
351
|
+
endMs: window.end_at ? Date.parse(window.end_at) : null,
|
|
352
|
+
}))
|
|
353
|
+
.filter((window) => Number.isFinite(window.startMs) &&
|
|
354
|
+
(window.endMs === null || Number.isFinite(window.endMs)));
|
|
355
|
+
emit();
|
|
356
|
+
},
|
|
357
|
+
toggleSelected(eventId) {
|
|
358
|
+
const hasEntry = entries.some((entry) => entry.id === eventId);
|
|
359
|
+
if (!hasEntry || pendingRemovalEventIds.has(eventId)) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (selectedEventIds.has(eventId)) {
|
|
363
|
+
selectedEventIds.delete(eventId);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
selectedEventIds.clear();
|
|
367
|
+
selectedEventIds.add(eventId);
|
|
368
|
+
}
|
|
369
|
+
emit();
|
|
370
|
+
},
|
|
371
|
+
markPendingRemoval(eventId) {
|
|
372
|
+
const hasEntry = entries.some((entry) => entry.id === eventId);
|
|
373
|
+
if (!hasEntry) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
pendingRemovalEventIds.add(eventId);
|
|
377
|
+
selectedEventIds.delete(eventId);
|
|
378
|
+
emit();
|
|
379
|
+
},
|
|
380
|
+
restorePendingRemoval(eventId) {
|
|
381
|
+
if (!pendingRemovalEventIds.has(eventId)) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
pendingRemovalEventIds.delete(eventId);
|
|
385
|
+
emit();
|
|
386
|
+
},
|
|
387
|
+
setHighlightTrace(eventId, trace) {
|
|
388
|
+
const hasEntry = entries.some((entry) => entry.id === eventId);
|
|
389
|
+
if (!hasEntry) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (trace) {
|
|
393
|
+
highlightTraces.set(eventId, trace);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
highlightTraces.delete(eventId);
|
|
397
|
+
}
|
|
398
|
+
emit();
|
|
399
|
+
},
|
|
400
|
+
replaceActionUnits(actionUnits) {
|
|
401
|
+
if (windows.length === 0) {
|
|
402
|
+
if (entries.length > 0) {
|
|
403
|
+
entries = [];
|
|
404
|
+
selectedEventIds.clear();
|
|
405
|
+
emit();
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const nextEntries = [...actionUnits]
|
|
410
|
+
.filter((actionUnit) => {
|
|
411
|
+
const occurredAtMs = Date.parse(actionUnit.start_at);
|
|
412
|
+
return (!Number.isNaN(occurredAtMs) &&
|
|
413
|
+
isWithinAnyWindow(occurredAtMs, windows));
|
|
414
|
+
})
|
|
415
|
+
.sort((left, right) => {
|
|
416
|
+
const occurredDelta = Date.parse(left.start_at) - Date.parse(right.start_at);
|
|
417
|
+
if (occurredDelta !== 0) {
|
|
418
|
+
return occurredDelta;
|
|
419
|
+
}
|
|
420
|
+
return left.action_unit_id.localeCompare(right.action_unit_id);
|
|
421
|
+
})
|
|
422
|
+
.slice(-MAX_RECORDED_ACTION_UNITS)
|
|
423
|
+
.map(toRecordedEvent);
|
|
424
|
+
const nextEntryIds = new Set(nextEntries.map((entry) => entry.id));
|
|
425
|
+
for (const selectedId of selectedEventIds) {
|
|
426
|
+
if (!nextEntryIds.has(selectedId)) {
|
|
427
|
+
selectedEventIds.delete(selectedId);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
for (const removedId of pendingRemovalEventIds) {
|
|
431
|
+
if (!nextEntryIds.has(removedId)) {
|
|
432
|
+
pendingRemovalEventIds.delete(removedId);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const eventId of highlightTraces.keys()) {
|
|
436
|
+
if (!nextEntryIds.has(eventId)) {
|
|
437
|
+
highlightTraces.delete(eventId);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
entries = nextEntries;
|
|
441
|
+
emit();
|
|
442
|
+
},
|
|
443
|
+
upsertActionUnit(actionUnit) {
|
|
444
|
+
if (windows.length === 0) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const occurredAtMs = Date.parse(actionUnit.start_at);
|
|
448
|
+
if (Number.isNaN(occurredAtMs) ||
|
|
449
|
+
!isWithinAnyWindow(occurredAtMs, windows)) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const nextEntry = toRecordedEvent(actionUnit);
|
|
453
|
+
const currentEntries = entries.filter((entry) => entry.id !== actionUnit.action_unit_id);
|
|
454
|
+
currentEntries.push(nextEntry);
|
|
455
|
+
currentEntries.sort((left, right) => {
|
|
456
|
+
const occurredDelta = Date.parse(left.actionUnit.start_at) -
|
|
457
|
+
Date.parse(right.actionUnit.start_at);
|
|
458
|
+
if (occurredDelta !== 0) {
|
|
459
|
+
return occurredDelta;
|
|
460
|
+
}
|
|
461
|
+
return left.id.localeCompare(right.id);
|
|
462
|
+
});
|
|
463
|
+
entries = currentEntries.slice(-MAX_RECORDED_ACTION_UNITS);
|
|
464
|
+
for (const eventId of highlightTraces.keys()) {
|
|
465
|
+
if (!entries.some((entry) => entry.id === eventId)) {
|
|
466
|
+
highlightTraces.delete(eventId);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
emit();
|
|
470
|
+
},
|
|
471
|
+
reset() {
|
|
472
|
+
isRecording = false;
|
|
473
|
+
windows = [];
|
|
474
|
+
entries = [];
|
|
475
|
+
selectedEventIds.clear();
|
|
476
|
+
pendingRemovalEventIds.clear();
|
|
477
|
+
highlightTraces.clear();
|
|
478
|
+
emit();
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SERVICE_LOGO_URL } from "./service-logo.generated";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SERVICE_LOGO_URL = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTMwIiBoZWlnaHQ9IjEzMCIgdmlld0JveD0iMCAwIDEzMCAxMzAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8yNzBfMTIxMikiPgo8cGF0aCBkPSJNMjkuNzY1MiA1Mi42MDI3QzMxLjIxOTUgNDkuMTcwNCAzNS41NTU5IDQ4LjA4NjUgMzguNDU0MiA1MC40MzA3TDExNS42NjggMTEyLjg4M0MxMTkuNzc3IDExNi4yMDcgMTE3LjQyNyAxMjIuODUgMTEyLjE0MiAxMjIuODVIOC40NjU2NUM0LjQ1MDA2IDEyMi44NSAxLjczNjE2IDExOC43NTIgMy4zMDI4MiAxMTUuMDU1TDI5Ljc2NTIgNTIuNjAyN1oiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik05Ny4wNTEyIDczLjc0NTlDOTUuNTk2OSA3Ny4xNzgyIDkxLjI2MDUgNzguMjYyMiA4OC4zNjIyIDc1LjkxOEwxMS4xNDg2IDEzLjQ2NTdDNy4wMzkyMSAxMC4xNDIgOS4zODk0NiAzLjQ5ODkyIDE0LjY3NDggMy40OTg5MkwxMTguMzUxIDMuNDk4OTNDMTIyLjM2NiAzLjQ5ODkzIDEyNS4wOCA3LjU5NjM0IDEyMy41MTQgMTEuMjkzN0w5Ny4wNTEyIDczLjc0NTlaIiBmaWxsPSJibGFjayIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzI3MF8xMjEyIj4KPHJlY3Qgd2lkdGg9IjEzMCIgaGVpZ2h0PSIxMzAiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// This file is generated from src/authoring/service-logo.svg.
|
|
2
|
+
// Run `pnpm --filter @clue-ai/browser-sdk build` after updating the SVG.
|
|
3
|
+
export const SERVICE_LOGO_URL = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTMwIiBoZWlnaHQ9IjEzMCIgdmlld0JveD0iMCAwIDEzMCAxMzAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8yNzBfMTIxMikiPgo8cGF0aCBkPSJNMjkuNzY1MiA1Mi42MDI3QzMxLjIxOTUgNDkuMTcwNCAzNS41NTU5IDQ4LjA4NjUgMzguNDU0MiA1MC40MzA3TDExNS42NjggMTEyLjg4M0MxMTkuNzc3IDExNi4yMDcgMTE3LjQyNyAxMjIuODUgMTEyLjE0MiAxMjIuODVIOC40NjU2NUM0LjQ1MDA2IDEyMi44NSAxLjczNjE2IDExOC43NTIgMy4zMDI4MiAxMTUuMDU1TDI5Ljc2NTIgNTIuNjAyN1oiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik05Ny4wNTEyIDczLjc0NTlDOTUuNTk2OSA3Ny4xNzgyIDkxLjI2MDUgNzguMjYyMiA4OC4zNjIyIDc1LjkxOEwxMS4xNDg2IDEzLjQ2NTdDNy4wMzkyMSAxMC4xNDIgOS4zODk0NiAzLjQ5ODkyIDE0LjY3NDggMy40OTg5MkwxMTguMzUxIDMuNDk4OTNDMTIyLjM2NiAzLjQ5ODkzIDEyNS4wOCA3LjU5NjM0IDEyMy41MTQgMTEuMjkzN0w5Ny4wNTEyIDczLjc0NTlaIiBmaWxsPSJibGFjayIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzI3MF8xMjEyIj4KPHJlY3Qgd2lkdGg9IjEzMCIgaGVpZ2h0PSIxMzAiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SERVICE_LOGO_URL } from "./service-logo.generated";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const BUSINESS_EVENT_AUTHORING_WINDOW_NAME_PREFIX = "__clue_business_event_authoring__:";
|
|
2
|
+
export type BusinessEventAuthoringSession = {
|
|
3
|
+
type: "clue-business-event-authoring-session";
|
|
4
|
+
session_id: string;
|
|
5
|
+
issued_at: string;
|
|
6
|
+
expires_at: string;
|
|
7
|
+
clue_origin: string;
|
|
8
|
+
project_id: string;
|
|
9
|
+
project_key: string;
|
|
10
|
+
target_origin: string;
|
|
11
|
+
target_url: string;
|
|
12
|
+
status: "paused" | "recording" | "ended";
|
|
13
|
+
recording_windows: Array<{
|
|
14
|
+
start_at: string;
|
|
15
|
+
end_at: string | null;
|
|
16
|
+
}>;
|
|
17
|
+
algorithm_version: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function parseBusinessEventAuthoringToken(windowName: string): string | null;
|
|
20
|
+
export declare function persistBusinessEventAuthoringToken(token: string): void;
|
|
21
|
+
export declare function readPersistedBusinessEventAuthoringToken(): string | null;
|
|
22
|
+
export declare function clearPersistedBusinessEventAuthoringToken(): void;
|
|
23
|
+
export declare function isBusinessEventAuthoringSession(value: unknown): value is BusinessEventAuthoringSession;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export const BUSINESS_EVENT_AUTHORING_WINDOW_NAME_PREFIX = "__clue_business_event_authoring__:";
|
|
2
|
+
const BUSINESS_EVENT_AUTHORING_STORAGE_KEY = "__clue_business_event_authoring_token__";
|
|
3
|
+
function isHttpUrl(value) {
|
|
4
|
+
try {
|
|
5
|
+
const parsed = new URL(value);
|
|
6
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function parseBusinessEventAuthoringToken(windowName) {
|
|
13
|
+
if (!windowName.startsWith(BUSINESS_EVENT_AUTHORING_WINDOW_NAME_PREFIX)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const token = windowName
|
|
17
|
+
.slice(BUSINESS_EVENT_AUTHORING_WINDOW_NAME_PREFIX.length)
|
|
18
|
+
.trim();
|
|
19
|
+
return token.length > 0 ? token : null;
|
|
20
|
+
}
|
|
21
|
+
function getAuthoringStorage() {
|
|
22
|
+
try {
|
|
23
|
+
return window.localStorage;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function decodeBusinessEventAuthoringToken(token) {
|
|
30
|
+
const [payloadSegment] = token.split(".", 1);
|
|
31
|
+
if (!payloadSegment) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const normalizedSegment = payloadSegment.replace(/-/g, "+").replace(/_/g, "/");
|
|
36
|
+
const padding = normalizedSegment.length % 4 === 0
|
|
37
|
+
? ""
|
|
38
|
+
: "=".repeat(4 - (normalizedSegment.length % 4));
|
|
39
|
+
const decoded = JSON.parse(atob(`${normalizedSegment}${padding}`));
|
|
40
|
+
if (decoded &&
|
|
41
|
+
typeof decoded === "object" &&
|
|
42
|
+
typeof decoded.expires_at === "string") {
|
|
43
|
+
return decoded;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isTokenExpired(token) {
|
|
52
|
+
const payload = decodeBusinessEventAuthoringToken(token);
|
|
53
|
+
if (!payload || typeof payload.expires_at !== "string") {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const expiresAtMs = Date.parse(payload.expires_at);
|
|
57
|
+
return Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now();
|
|
58
|
+
}
|
|
59
|
+
export function persistBusinessEventAuthoringToken(token) {
|
|
60
|
+
const normalizedToken = token.trim();
|
|
61
|
+
if (normalizedToken.length === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
window.name = `${BUSINESS_EVENT_AUTHORING_WINDOW_NAME_PREFIX}${normalizedToken}`;
|
|
65
|
+
getAuthoringStorage()?.setItem(BUSINESS_EVENT_AUTHORING_STORAGE_KEY, normalizedToken);
|
|
66
|
+
}
|
|
67
|
+
export function readPersistedBusinessEventAuthoringToken() {
|
|
68
|
+
const windowToken = parseBusinessEventAuthoringToken(window.name);
|
|
69
|
+
if (windowToken) {
|
|
70
|
+
if (isTokenExpired(windowToken)) {
|
|
71
|
+
clearPersistedBusinessEventAuthoringToken();
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
getAuthoringStorage()?.setItem(BUSINESS_EVENT_AUTHORING_STORAGE_KEY, windowToken);
|
|
75
|
+
return windowToken;
|
|
76
|
+
}
|
|
77
|
+
const storageToken = getAuthoringStorage()?.getItem(BUSINESS_EVENT_AUTHORING_STORAGE_KEY);
|
|
78
|
+
if (!storageToken) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const normalizedToken = storageToken.trim();
|
|
82
|
+
if (normalizedToken.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (isTokenExpired(normalizedToken)) {
|
|
86
|
+
clearPersistedBusinessEventAuthoringToken();
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
window.name = `${BUSINESS_EVENT_AUTHORING_WINDOW_NAME_PREFIX}${normalizedToken}`;
|
|
90
|
+
return normalizedToken;
|
|
91
|
+
}
|
|
92
|
+
export function clearPersistedBusinessEventAuthoringToken() {
|
|
93
|
+
if (window.name.startsWith(BUSINESS_EVENT_AUTHORING_WINDOW_NAME_PREFIX)) {
|
|
94
|
+
window.name = "";
|
|
95
|
+
}
|
|
96
|
+
getAuthoringStorage()?.removeItem(BUSINESS_EVENT_AUTHORING_STORAGE_KEY);
|
|
97
|
+
}
|
|
98
|
+
export function isBusinessEventAuthoringSession(value) {
|
|
99
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const session = value;
|
|
103
|
+
const recordingWindows = session.recording_windows;
|
|
104
|
+
return (session.type === "clue-business-event-authoring-session" &&
|
|
105
|
+
typeof session.session_id === "string" &&
|
|
106
|
+
typeof session.issued_at === "string" &&
|
|
107
|
+
typeof session.expires_at === "string" &&
|
|
108
|
+
typeof session.project_id === "string" &&
|
|
109
|
+
typeof session.project_key === "string" &&
|
|
110
|
+
typeof session.target_url === "string" &&
|
|
111
|
+
typeof session.target_origin === "string" &&
|
|
112
|
+
typeof session.clue_origin === "string" &&
|
|
113
|
+
(session.status === "paused" ||
|
|
114
|
+
session.status === "recording" ||
|
|
115
|
+
session.status === "ended") &&
|
|
116
|
+
Array.isArray(recordingWindows) &&
|
|
117
|
+
recordingWindows.every((window) => window &&
|
|
118
|
+
typeof window === "object" &&
|
|
119
|
+
!Array.isArray(window) &&
|
|
120
|
+
typeof window.start_at === "string" &&
|
|
121
|
+
(window.end_at === null ||
|
|
122
|
+
typeof window.end_at === "string")) &&
|
|
123
|
+
typeof session.algorithm_version === "number" &&
|
|
124
|
+
isHttpUrl(session.target_origin) &&
|
|
125
|
+
isHttpUrl(session.clue_origin) &&
|
|
126
|
+
isHttpUrl(session.target_url));
|
|
127
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const AUTHORING_ROOT_ATTRIBUTE = "data-clue-authoring-root";
|
|
2
|
+
export declare const AUTHORING_ROOT_ATTRIBUTE_VALUE = "true";
|
|
3
|
+
export declare const AUTHORING_INTERNAL_REQUEST_HEADER = "x-clue-authoring-request";
|
|
4
|
+
export declare const AUTHORING_INTERNAL_REQUEST_HEADER_VALUE = "true";
|
|
5
|
+
export declare const AUTHORING_INTERNAL_REQUEST_PATH_SEGMENT = "/business-events/authoring";
|
|
6
|
+
export declare function markAuthoringRoot(element: HTMLElement): void;
|
|
7
|
+
export declare function resolveAuthoringSurfaceRoot(target: EventTarget | Element | null | undefined): HTMLElement | null;
|
|
8
|
+
export declare function isWithinAuthoringSurface(target: EventTarget | Element | null | undefined): boolean;
|
|
9
|
+
export declare function hasAuthoringInternalRequestHeader(headers: Record<string, string>): boolean;
|
|
10
|
+
export declare function isAuthoringInternalRequestUrl(rawUrl: string): boolean;
|
|
11
|
+
export declare function logAuthoringRequestExclusion(matchedRequestRule: "request_header" | "path_segment" | "ignore_url_prefix"): void;
|