@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
@@ -0,0 +1,831 @@
1
+ import { resolveAuthoringBaseEndpoint, startBusinessEventAuthoringOverlay, } from "../authoring/overlay";
2
+ import { createAuthoringRecordingController } from "../authoring/recording";
3
+ import { readPersistedBusinessEventAuthoringToken } from "../authoring/session";
4
+ import { isWithinAuthoringSurface } from "../authoring/surface";
5
+ import { startActionCapture } from "../capture/action";
6
+ import { startDragCapture } from "../capture/drag";
7
+ import { startErrorCapture } from "../capture/error";
8
+ import { startFormCapture } from "../capture/form";
9
+ import { startFrustrationCapture } from "../capture/frustration";
10
+ import { startInputCapture } from "../capture/input";
11
+ import { startNavigationCapture } from "../capture/navigation";
12
+ import { startNetworkCapture } from "../capture/network";
13
+ import { startPageCapture } from "../capture/page";
14
+ import { startPerformanceCapture } from "../capture/performance";
15
+ import { AccountManager } from "../context/account";
16
+ import { EnvironmentManager } from "../context/environment";
17
+ import { IdentityManager } from "../context/identity";
18
+ import { SessionManager } from "../context/session";
19
+ import { TabManager } from "../context/tab";
20
+ import { TraceManager } from "../context/trace";
21
+ import { InternalMetricsStore } from "../internal/metrics";
22
+ import { buildCanonicalEvent, buildDegradedCanonicalCandidates, } from "../normalize/builders";
23
+ import { generateViewId } from "../normalize/event-id";
24
+ import { BatchTransport } from "../transport/batch";
25
+ import { IngestClient } from "../transport/client";
26
+ import { isConfigValid, resolveConfig, validateConfig } from "./config";
27
+ import { DEFAULT_CONSENT, DEFAULT_INTERACTION_PROPAGATION_WINDOW_MS, DEFAULT_MAX_BATCH_QUEUE_BYTES, SDK_VERSION, } from "./constants";
28
+ import { FRONTEND_EVENT_CATEGORY, FRONTEND_EVENT_NAME } from "./contracts";
29
+ import { DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK, hasUsageGovernorFeedbackChanged, isCostCapabilityDisabled, normalizeIngestUsageGovernorFeedback, } from "./usage-governor";
30
+ const TRACE_ROOT_EVENTS = new Set([
31
+ "page_view",
32
+ "route_change",
33
+ "element_clicked",
34
+ "form_submitted",
35
+ "input_committed",
36
+ "toggle_changed",
37
+ "selection_committed",
38
+ "file_selected",
39
+ "drag_drop_completed",
40
+ "custom_emitted",
41
+ ]);
42
+ const INTERACTION_ANCHOR_EVENTS = new Set([
43
+ "element_clicked",
44
+ "form_submitted",
45
+ "input_committed",
46
+ "toggle_changed",
47
+ "selection_committed",
48
+ "file_selected",
49
+ "drag_drop_completed",
50
+ "custom_emitted",
51
+ ]);
52
+ const OFFICIAL_CUSTOM_EVENT_NAME = {
53
+ accountAssociated: "account_associated",
54
+ identityIdentified: "identity_identified",
55
+ identityLoggedOut: "identity_logged_out",
56
+ sdkInitialized: "sdk_initialized",
57
+ };
58
+ const OFFICIAL_CUSTOM_EVENT_NAMES = new Set(Object.values(OFFICIAL_CUSTOM_EVENT_NAME));
59
+ const SUMMARY_EVENT_NAMES = new Set([
60
+ FRONTEND_EVENT_NAME.performanceSummaryObserved,
61
+ FRONTEND_EVENT_NAME.browserReportSummaryObserved,
62
+ FRONTEND_EVENT_NAME.uiLifecycleSummaryObserved,
63
+ FRONTEND_EVENT_NAME.streamLifecycleSummaryObserved,
64
+ ]);
65
+ const createNoopLogger = () => {
66
+ return {
67
+ debug: () => undefined,
68
+ warn: () => undefined,
69
+ error: () => undefined,
70
+ };
71
+ };
72
+ const hasBusinessEventAuthoringToken = () => {
73
+ if (typeof window === "undefined") {
74
+ return false;
75
+ }
76
+ return readPersistedBusinessEventAuthoringToken() !== null;
77
+ };
78
+ const resolveEffectiveBatchConfig = (config) => {
79
+ if (hasBusinessEventAuthoringToken()) {
80
+ return {
81
+ ...config.batch,
82
+ transportProfile: "authoring_realtime",
83
+ };
84
+ }
85
+ if (config.transportProfile === "authoring_realtime") {
86
+ return {
87
+ ...config.batch,
88
+ transportProfile: "authoring_realtime",
89
+ };
90
+ }
91
+ return {
92
+ ...config.batch,
93
+ transportProfile: "standard",
94
+ };
95
+ };
96
+ const toStringOrNull = (value) => {
97
+ return typeof value === "string" ? value : null;
98
+ };
99
+ const isOfficialCustomLifecycleEvent = (event) => {
100
+ if (event.type !== "custom_emitted") {
101
+ return false;
102
+ }
103
+ const customEventName = event.payload.eventName;
104
+ return (typeof customEventName === "string" &&
105
+ OFFICIAL_CUSTOM_EVENT_NAMES.has(customEventName));
106
+ };
107
+ const toSubjectProfile = (traits) => {
108
+ const displayName = toStringOrNull(traits.display_name ?? traits.displayName);
109
+ const avatarUrl = toStringOrNull(traits.avatar_url ?? traits.avatarUrl);
110
+ const email = toStringOrNull(traits.email ?? traits.email_address ?? traits.emailAddress);
111
+ const role = toStringOrNull(traits.role);
112
+ const plan = toStringOrNull(traits.plan);
113
+ if (displayName === null &&
114
+ avatarUrl === null &&
115
+ email === null &&
116
+ role === null &&
117
+ plan === null) {
118
+ return null;
119
+ }
120
+ return {
121
+ display_name: displayName,
122
+ avatar_url: avatarUrl,
123
+ email,
124
+ role,
125
+ plan,
126
+ };
127
+ };
128
+ class ClueBrowserSdkImpl {
129
+ constructor(logger = createNoopLogger()) {
130
+ this.config = null;
131
+ this.initialized = false;
132
+ this.initSignature = null;
133
+ this.consent = DEFAULT_CONSENT;
134
+ this.identityManager = null;
135
+ this.sessionManager = null;
136
+ this.tabManager = null;
137
+ this.accountManager = null;
138
+ this.environmentManager = null;
139
+ this.traceManager = null;
140
+ this.transport = null;
141
+ this.captureHandles = [];
142
+ this.authoringOverlayHandle = null;
143
+ this.authoringRecording = createAuthoringRecordingController();
144
+ this.unloadHandlers = [];
145
+ this.usageGovernorFeedback = DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK;
146
+ this.currentViewId = null;
147
+ this.sessionCaptureEligible = true;
148
+ this.logger = logger;
149
+ this.internalMetrics = new InternalMetricsStore();
150
+ }
151
+ getInternalMetrics() {
152
+ return this.internalMetrics.getSnapshot();
153
+ }
154
+ init(options) {
155
+ try {
156
+ const resolved = resolveConfig(options);
157
+ const issues = validateConfig(resolved);
158
+ if (!isConfigValid(resolved)) {
159
+ this.logger.warn("Clue Browser SDK init validation failed", {
160
+ issues,
161
+ });
162
+ return;
163
+ }
164
+ const nextSignature = JSON.stringify({
165
+ endpoint: resolved.endpoint,
166
+ originalEndpoint: resolved.originalEndpoint,
167
+ localSetupCheckEndpoint: resolved.localSetupCheckEndpoint,
168
+ projectKey: resolved.projectKey,
169
+ environment: resolved.environment,
170
+ serviceKey: resolved.serviceKey,
171
+ producerId: resolved.producerId,
172
+ browserTokenProvider: resolved.browserTokenProvider !== null,
173
+ transportProfile: resolved.transportProfile,
174
+ autocapture: resolved.autocapture,
175
+ captureNetwork: resolved.captureNetwork,
176
+ captureErrors: resolved.captureErrors,
177
+ captureFrustration: resolved.captureFrustration,
178
+ capturePerformance: resolved.capturePerformance,
179
+ network: resolved.network,
180
+ batch: resolved.batch,
181
+ privacy: resolved.privacy,
182
+ sampling: resolved.sampling,
183
+ collection: resolved.collection,
184
+ release: resolved.release,
185
+ });
186
+ if (this.initialized && this.initSignature === nextSignature) {
187
+ this.logger.debug("Clue Browser SDK init skipped", {
188
+ reason: "same_config",
189
+ });
190
+ return;
191
+ }
192
+ this.shutdown();
193
+ this.internalMetrics.reset();
194
+ this.config = resolved;
195
+ this.usageGovernorFeedback = DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK;
196
+ this.initSignature = nextSignature;
197
+ this.identityManager = new IdentityManager();
198
+ this.sessionManager = new SessionManager();
199
+ this.tabManager = new TabManager();
200
+ this.accountManager = new AccountManager();
201
+ this.traceManager = new TraceManager();
202
+ this.environmentManager = new EnvironmentManager({
203
+ environment: resolved.environment,
204
+ frontendRelease: resolved.release.frontendRelease,
205
+ featureFlagsProvider: resolved.featureFlagsProvider,
206
+ experimentVariantProvider: resolved.experimentVariantProvider,
207
+ });
208
+ this.currentViewId = generateViewId();
209
+ const effectiveBatch = resolveEffectiveBatchConfig(resolved);
210
+ const ingestClient = new IngestClient({
211
+ endpoint: resolved.endpoint,
212
+ projectKey: resolved.projectKey,
213
+ environment: resolved.environment,
214
+ serviceKey: resolved.serviceKey,
215
+ producerId: resolved.producerId,
216
+ browserTokenProvider: resolved.browserTokenProvider ?? undefined,
217
+ sdkType: "browser",
218
+ sdkVersion: SDK_VERSION,
219
+ schemaVersion: 1,
220
+ metrics: this.internalMetrics,
221
+ });
222
+ this.transport = new BatchTransport({
223
+ client: ingestClient,
224
+ enabled: effectiveBatch.enabled,
225
+ flushIntervalMs: effectiveBatch.flushIntervalMs,
226
+ maxPayloadBytes: effectiveBatch.maxPayloadBytes,
227
+ queueMaxBytes: DEFAULT_MAX_BATCH_QUEUE_BYTES,
228
+ metrics: this.internalMetrics,
229
+ onGovernorFeedback: (feedback) => {
230
+ this.applyUsageGovernorFeedback(feedback);
231
+ },
232
+ });
233
+ this.transport.start();
234
+ this.installUnloadHandlers();
235
+ this.initialized = true;
236
+ this.refreshSessionCaptureEligibility();
237
+ const sessionLifecycle = this.sessionManager.ensureActiveSession(Date.now());
238
+ if (sessionLifecycle.started) {
239
+ this.enqueueRawEvent({
240
+ type: "session_started",
241
+ occurredAtMs: Date.now(),
242
+ payload: {
243
+ entryUrl: typeof window !== "undefined" ? window.location.href : null,
244
+ },
245
+ }, true);
246
+ }
247
+ if (effectiveBatch.transportProfile !== "authoring_realtime") {
248
+ this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.sdkInitialized, {
249
+ sdk_action: "init",
250
+ sdk_scope: "lifecycle",
251
+ }, true);
252
+ }
253
+ if (resolved.autocapture && this.consent !== "denied") {
254
+ this.startCaptureModules();
255
+ }
256
+ this.authoringOverlayHandle = startBusinessEventAuthoringOverlay({
257
+ ingestEndpoint: resolved.endpoint,
258
+ recording: this.authoringRecording,
259
+ onExit: () => {
260
+ this.authoringRecording.reset();
261
+ },
262
+ });
263
+ }
264
+ catch (error) {
265
+ this.logger.error("Clue Browser SDK init failed", {
266
+ error: String(error),
267
+ });
268
+ }
269
+ }
270
+ identify(userId, traits) {
271
+ if (!this.identityManager) {
272
+ return;
273
+ }
274
+ try {
275
+ this.identityManager.identify(userId, traits);
276
+ this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.identityIdentified, {
277
+ identity_action: "identify",
278
+ profile_keys: Object.keys(traits ?? {}).sort(),
279
+ });
280
+ }
281
+ catch (error) {
282
+ this.logger.error("identify failed", {
283
+ error: String(error),
284
+ });
285
+ }
286
+ }
287
+ setAccount(accountId, traits) {
288
+ if (!this.accountManager) {
289
+ return;
290
+ }
291
+ try {
292
+ this.accountManager.setAccount(accountId, traits);
293
+ this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.accountAssociated, {
294
+ account_action: "associate",
295
+ profile_keys: Object.keys(traits ?? {}).sort(),
296
+ });
297
+ }
298
+ catch (error) {
299
+ this.logger.error("setAccount failed", {
300
+ error: String(error),
301
+ });
302
+ }
303
+ }
304
+ reset() {
305
+ if (!this.initialized) {
306
+ return;
307
+ }
308
+ try {
309
+ const ended = this.sessionManager?.endSession(Date.now());
310
+ if (ended) {
311
+ this.enqueueRawEvent({
312
+ type: "session_ended",
313
+ occurredAtMs: Date.now(),
314
+ payload: {
315
+ sessionId: ended.sessionId,
316
+ exitUrl: typeof window !== "undefined" ? window.location.href : null,
317
+ durationMs: ended.durationMs,
318
+ },
319
+ }, true);
320
+ }
321
+ this.identityManager?.reset();
322
+ this.accountManager?.reset();
323
+ this.traceManager?.reset();
324
+ this.sessionManager?.reset(Date.now());
325
+ this.currentViewId = generateViewId();
326
+ this.enqueueRawEvent({
327
+ type: "session_started",
328
+ occurredAtMs: Date.now(),
329
+ payload: {
330
+ entryUrl: typeof window !== "undefined" ? window.location.href : null,
331
+ },
332
+ }, true);
333
+ }
334
+ catch (error) {
335
+ this.logger.error("reset failed", {
336
+ error: String(error),
337
+ });
338
+ }
339
+ }
340
+ logout() {
341
+ this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.identityLoggedOut, {
342
+ identity_action: "logout",
343
+ });
344
+ this.reset();
345
+ }
346
+ track(eventName, properties, metrics) {
347
+ this.enqueueRawEvent({
348
+ type: "custom_emitted",
349
+ occurredAtMs: Date.now(),
350
+ payload: {
351
+ eventName,
352
+ properties: properties ?? {},
353
+ metrics: metrics ?? {},
354
+ },
355
+ });
356
+ }
357
+ setConsent(consent) {
358
+ this.consent = consent;
359
+ if (!this.initialized) {
360
+ return;
361
+ }
362
+ if (consent === "denied") {
363
+ this.stopCaptureModules();
364
+ this.transport?.clear();
365
+ return;
366
+ }
367
+ if (this.config?.autocapture && this.captureHandles.length === 0) {
368
+ this.startCaptureModules();
369
+ }
370
+ }
371
+ enqueueOfficialCustomEvent(eventName, properties, skipSessionTouch = false) {
372
+ this.enqueueRawEvent({
373
+ type: "custom_emitted",
374
+ occurredAtMs: Date.now(),
375
+ payload: {
376
+ eventName,
377
+ metrics: {
378
+ count: 1,
379
+ },
380
+ properties,
381
+ },
382
+ }, skipSessionTouch);
383
+ }
384
+ enqueueRawEvent(event, skipSessionTouch = false) {
385
+ if (!this.initialized || !this.config || !this.transport) {
386
+ return;
387
+ }
388
+ if (this.consent === "denied") {
389
+ return;
390
+ }
391
+ if (!skipSessionTouch &&
392
+ event.type !== "session_started" &&
393
+ event.type !== "session_ended") {
394
+ this.handleSessionLifecycle(Date.now());
395
+ }
396
+ const eventWithInteraction = this.attachInteractionContext(event);
397
+ if (eventWithInteraction.type === "page_view") {
398
+ const nextViewId = toStringOrNull(eventWithInteraction.payload.viewId);
399
+ if (nextViewId) {
400
+ this.currentViewId = nextViewId;
401
+ }
402
+ }
403
+ try {
404
+ const context = this.buildContext(eventWithInteraction.occurredAtMs);
405
+ if (SUMMARY_EVENT_NAMES.has(eventWithInteraction.type)) {
406
+ const summaryCount = eventWithInteraction.payload.summaryCount;
407
+ const droppedCount = eventWithInteraction.payload.droppedCount;
408
+ if (typeof summaryCount === "number" && summaryCount > 0) {
409
+ this.internalMetrics.increment("events_summarized_count", summaryCount);
410
+ }
411
+ if (typeof droppedCount === "number" && droppedCount > 0) {
412
+ this.internalMetrics.increment("sampling_dropped_count", droppedCount);
413
+ }
414
+ }
415
+ if (isCostCapabilityDisabled(this.usageGovernorFeedback, "event_collection")) {
416
+ this.internalMetrics.increment("dropped_count");
417
+ return;
418
+ }
419
+ const canonical = buildCanonicalEvent(eventWithInteraction, {
420
+ context,
421
+ deniedKeys: this.config.privacy.deniedKeys,
422
+ allowedValuePaths: this.config.privacy.allowedValuePaths,
423
+ onMaskingError: () => {
424
+ this.internalMetrics.increment("masking_error_count");
425
+ },
426
+ });
427
+ if (!canonical) {
428
+ return;
429
+ }
430
+ if (!this.shouldCaptureEvent(eventWithInteraction, canonical)) {
431
+ this.internalMetrics.increment("dropped_count");
432
+ return;
433
+ }
434
+ let canonicalEvent = canonical;
435
+ let eventBytes = this.measureSerializedBytes(canonicalEvent);
436
+ if (eventBytes > this.config.batch.maxEventBytes) {
437
+ let degradedStage = null;
438
+ for (const candidate of buildDegradedCanonicalCandidates(canonicalEvent)) {
439
+ const candidateBytes = this.measureSerializedBytes(candidate.event);
440
+ if (candidateBytes <= this.config.batch.maxEventBytes) {
441
+ canonicalEvent = candidate.event;
442
+ eventBytes = candidateBytes;
443
+ degradedStage = candidate.stage;
444
+ break;
445
+ }
446
+ }
447
+ if (degradedStage) {
448
+ this.internalMetrics.increment("snapshot_truncated_count");
449
+ this.logger.warn("browser sdk degraded oversized event", {
450
+ eventType: eventWithInteraction.type,
451
+ eventBytes,
452
+ maxEventBytes: this.config.batch.maxEventBytes,
453
+ degradedStage,
454
+ });
455
+ }
456
+ }
457
+ if (eventBytes > this.config.batch.maxEventBytes) {
458
+ this.internalMetrics.increment("dropped_count");
459
+ this.internalMetrics.increment("oversized_event_count");
460
+ this.internalMetrics.increment("oversized_event_dropped_count");
461
+ if (canonical.event_category === FRONTEND_EVENT_CATEGORY.request) {
462
+ this.internalMetrics.increment("parameter_snapshot_oversized_dropped");
463
+ }
464
+ this.logger.warn("browser sdk dropped oversized event", {
465
+ eventType: eventWithInteraction.type,
466
+ eventBytes,
467
+ maxEventBytes: this.config.batch.maxEventBytes,
468
+ });
469
+ return;
470
+ }
471
+ this.updateActiveTraceSpan(eventWithInteraction, canonicalEvent.event_id);
472
+ this.transport.enqueue(canonicalEvent);
473
+ }
474
+ catch (error) {
475
+ this.logger.error("enqueueRawEvent failed", {
476
+ eventType: eventWithInteraction.type,
477
+ error: String(error),
478
+ });
479
+ }
480
+ }
481
+ attachInteractionContext(event) {
482
+ if (isOfficialCustomLifecycleEvent(event)) {
483
+ return event;
484
+ }
485
+ const nowMs = event.occurredAtMs ?? Date.now();
486
+ const payloadInteractionId = toStringOrNull(event.payload.interactionId);
487
+ if (INTERACTION_ANCHOR_EVENTS.has(event.type)) {
488
+ const interactionId = this.traceManager?.startInteraction({
489
+ interactionId: payloadInteractionId,
490
+ nowMs,
491
+ propagationWindowMs: this.config?.network.interactionPropagationWindowMs ??
492
+ DEFAULT_INTERACTION_PROPAGATION_WINDOW_MS,
493
+ }) ?? payloadInteractionId;
494
+ if (interactionId && interactionId !== payloadInteractionId) {
495
+ return {
496
+ ...event,
497
+ payload: {
498
+ ...event.payload,
499
+ interactionId,
500
+ },
501
+ };
502
+ }
503
+ return event;
504
+ }
505
+ const activeInteractionId = payloadInteractionId ??
506
+ this.traceManager?.getActiveInteractionId(nowMs) ??
507
+ null;
508
+ if (!activeInteractionId || activeInteractionId === payloadInteractionId) {
509
+ return event;
510
+ }
511
+ return {
512
+ ...event,
513
+ payload: {
514
+ ...event.payload,
515
+ interactionId: activeInteractionId,
516
+ },
517
+ };
518
+ }
519
+ buildCaptureContext() {
520
+ return {
521
+ onEvent: (event) => {
522
+ this.enqueueRawEvent(event);
523
+ },
524
+ onInternalError: (source, error) => {
525
+ this.internalMetrics.increment("capture_error_count");
526
+ this.logger.error("capture module error", {
527
+ source,
528
+ error: String(error),
529
+ });
530
+ },
531
+ getViewId: () => {
532
+ if (!this.currentViewId) {
533
+ this.currentViewId = generateViewId();
534
+ }
535
+ return this.currentViewId;
536
+ },
537
+ getConsent: () => this.consent,
538
+ getObservationContext: () => {
539
+ const identity = this.identityManager?.getContext();
540
+ const account = this.accountManager?.getContext();
541
+ const session = this.sessionManager?.getContext();
542
+ return {
543
+ anonymousId: identity?.anonymousId ?? null,
544
+ userId: identity?.userId ?? null,
545
+ accountId: account?.accountId ?? null,
546
+ organizationId: account?.organizationId ?? null,
547
+ sessionId: session?.sessionId ?? null,
548
+ tabId: this.tabManager?.getTabId() ?? null,
549
+ };
550
+ },
551
+ shouldIgnoreElement: (element) => {
552
+ return isWithinAuthoringSurface(element);
553
+ },
554
+ };
555
+ }
556
+ startCaptureModules() {
557
+ if (!this.config) {
558
+ return;
559
+ }
560
+ const captureContext = this.buildCaptureContext();
561
+ const handles = [];
562
+ handles.push(startPageCapture(captureContext));
563
+ handles.push(startNavigationCapture(captureContext));
564
+ handles.push(startActionCapture(captureContext));
565
+ handles.push(startFormCapture(captureContext));
566
+ handles.push(startInputCapture(captureContext));
567
+ handles.push(startDragCapture(captureContext));
568
+ if (this.config.captureNetwork) {
569
+ handles.push(startNetworkCapture(captureContext, {
570
+ ignoreUrlPrefixes: [
571
+ this.config.endpoint,
572
+ resolveAuthoringBaseEndpoint(this.config.endpoint),
573
+ ],
574
+ tracePropagationOrigins: this.config.network.tracePropagationOrigins,
575
+ important4xxPathPrefixes: this.config.network.important4xxPathPrefixes,
576
+ createRequestTraceContext: () => {
577
+ if (this.traceManager) {
578
+ return this.traceManager.createRequestContext();
579
+ }
580
+ return new TraceManager().createRequestContext();
581
+ },
582
+ }));
583
+ }
584
+ if (this.config.capturePerformance) {
585
+ handles.push(startPerformanceCapture(captureContext));
586
+ }
587
+ if (this.config.captureErrors) {
588
+ handles.push(startErrorCapture(captureContext));
589
+ }
590
+ if (this.config.captureFrustration) {
591
+ handles.push(startFrustrationCapture(captureContext));
592
+ }
593
+ this.captureHandles = handles;
594
+ }
595
+ stopCaptureModules() {
596
+ for (const handle of this.captureHandles) {
597
+ try {
598
+ handle.stop();
599
+ }
600
+ catch (error) {
601
+ this.logger.warn("failed to stop capture module", {
602
+ error: String(error),
603
+ });
604
+ }
605
+ }
606
+ this.captureHandles = [];
607
+ }
608
+ applyUsageGovernorFeedback(feedback) {
609
+ const nextFeedback = normalizeIngestUsageGovernorFeedback(feedback);
610
+ if (!hasUsageGovernorFeedbackChanged(this.usageGovernorFeedback, nextFeedback)) {
611
+ return;
612
+ }
613
+ this.usageGovernorFeedback = nextFeedback;
614
+ }
615
+ handleSessionLifecycle(nowMs) {
616
+ if (!this.sessionManager) {
617
+ return;
618
+ }
619
+ const lifecycle = this.sessionManager.ensureActiveSession(nowMs);
620
+ if (lifecycle.ended) {
621
+ this.enqueueRawEvent({
622
+ type: "session_ended",
623
+ occurredAtMs: nowMs,
624
+ payload: {
625
+ sessionId: lifecycle.ended.sessionId,
626
+ exitUrl: typeof window !== "undefined" ? window.location.href : null,
627
+ durationMs: lifecycle.ended.durationMs,
628
+ },
629
+ }, true);
630
+ }
631
+ if (lifecycle.started) {
632
+ this.traceManager?.reset();
633
+ this.currentViewId = generateViewId();
634
+ this.refreshSessionCaptureEligibility();
635
+ this.enqueueRawEvent({
636
+ type: "session_started",
637
+ occurredAtMs: nowMs,
638
+ payload: {
639
+ entryUrl: typeof window !== "undefined" ? window.location.href : null,
640
+ },
641
+ }, true);
642
+ }
643
+ }
644
+ buildContext(occurredAtMs) {
645
+ if (!this.config ||
646
+ !this.identityManager ||
647
+ !this.sessionManager ||
648
+ !this.tabManager ||
649
+ !this.accountManager ||
650
+ !this.environmentManager) {
651
+ throw new Error("SDK is not initialized");
652
+ }
653
+ const identity = this.identityManager.getContext();
654
+ const session = this.sessionManager.getContext();
655
+ const account = this.accountManager.getContext();
656
+ const environment = this.environmentManager.getContext();
657
+ const trace = this.traceManager?.getSnapshot();
658
+ const plan = toStringOrNull(identity.traits.plan);
659
+ const role = toStringOrNull(identity.traits.role);
660
+ const accountStatus = toStringOrNull(account.traits.account_status ?? account.traits.accountStatus);
661
+ const billingStatus = toStringOrNull(account.traits.billing_status ?? account.traits.billingStatus);
662
+ const customerLifecycleStage = toStringOrNull(account.traits.customer_lifecycle_stage ??
663
+ account.traits.customerLifecycleStage);
664
+ const organizationSizeBucket = toStringOrNull(account.traits.organization_size_bucket ??
665
+ account.traits.organizationSizeBucket);
666
+ const permissionGroup = toStringOrNull(identity.traits.permission_group ??
667
+ identity.traits.permissionGroup ??
668
+ account.traits.permission_group ??
669
+ account.traits.permissionGroup);
670
+ const onboardingState = toStringOrNull(identity.traits.onboarding_state ??
671
+ identity.traits.onboardingState ??
672
+ account.traits.onboarding_state ??
673
+ account.traits.onboardingState);
674
+ const trialDaysRemainingBucket = toStringOrNull(account.traits.trial_days_remaining_bucket ??
675
+ account.traits.trialDaysRemainingBucket);
676
+ const customerHealthBucket = toStringOrNull(account.traits.customer_health_bucket ??
677
+ account.traits.customerHealthBucket);
678
+ return {
679
+ producer_id: this.config.producerId,
680
+ anonymous_id: identity.anonymousId,
681
+ user_id: identity.userId,
682
+ account_id: account.accountId,
683
+ organization_id: account.organizationId,
684
+ user_profile: toSubjectProfile(identity.traits),
685
+ account_profile: toSubjectProfile(account.traits),
686
+ workspace_id: account.workspaceId,
687
+ session_id: session.sessionId,
688
+ tab_id: this.tabManager.getTabId(),
689
+ trace_id: trace?.traceId ?? null,
690
+ request_id: null,
691
+ request_span_id: null,
692
+ interaction_id: this.traceManager?.getActiveInteractionId(occurredAtMs ?? Date.now()) ??
693
+ null,
694
+ correlation_id: null,
695
+ environment: environment.environment,
696
+ sdk_collection_mode: this.config.collection.mode,
697
+ frontend_release: environment.frontendRelease,
698
+ feature_flags: environment.featureFlags,
699
+ experiment_variant: environment.experimentVariant,
700
+ plan,
701
+ role,
702
+ locale: environment.locale,
703
+ country: environment.country,
704
+ device_type: environment.deviceType,
705
+ browser: environment.browser,
706
+ os: environment.os,
707
+ referrer: environment.referrer,
708
+ browser_family: environment.browserFamily,
709
+ browser_major_version: environment.browserMajorVersion,
710
+ browser_engine: environment.browserEngine,
711
+ os_family: environment.osFamily,
712
+ os_major_version: environment.osMajorVersion,
713
+ timezone: environment.timezone,
714
+ pointer_type_candidate: environment.pointerTypeCandidate,
715
+ touch_capable: environment.touchCapable,
716
+ cookies_enabled: environment.cookiesEnabled,
717
+ local_storage_available: environment.localStorageAvailable,
718
+ session_storage_available: environment.sessionStorageAvailable,
719
+ service_worker_supported: environment.serviceWorkerSupported,
720
+ webgl_supported: environment.webglSupported,
721
+ reduced_motion: environment.reducedMotion,
722
+ color_scheme: environment.colorScheme,
723
+ account_status: accountStatus,
724
+ plan_tier: plan,
725
+ billing_status: billingStatus,
726
+ customer_lifecycle_stage: customerLifecycleStage,
727
+ organization_size_bucket: organizationSizeBucket,
728
+ user_role: role,
729
+ permission_group: permissionGroup,
730
+ onboarding_state: onboardingState,
731
+ trial_days_remaining_bucket: trialDaysRemainingBucket,
732
+ customer_health_bucket: customerHealthBucket,
733
+ };
734
+ }
735
+ installUnloadHandlers() {
736
+ if (typeof window === "undefined") {
737
+ return;
738
+ }
739
+ let handled = false;
740
+ const handler = () => {
741
+ if (handled) {
742
+ return;
743
+ }
744
+ handled = true;
745
+ const ended = this.sessionManager?.endSession(Date.now());
746
+ if (ended) {
747
+ this.enqueueRawEvent({
748
+ type: "session_ended",
749
+ occurredAtMs: Date.now(),
750
+ payload: {
751
+ sessionId: ended.sessionId,
752
+ exitUrl: typeof window !== "undefined" ? window.location.href : null,
753
+ durationMs: ended.durationMs,
754
+ },
755
+ }, true);
756
+ }
757
+ this.transport?.flushOnUnload();
758
+ };
759
+ const visibilityHandler = () => {
760
+ if (document.visibilityState !== "hidden") {
761
+ return;
762
+ }
763
+ this.transport?.flushOnUnload();
764
+ };
765
+ window.addEventListener("pagehide", handler, { capture: true });
766
+ window.addEventListener("beforeunload", handler, { capture: true });
767
+ document.addEventListener("visibilitychange", visibilityHandler, {
768
+ capture: true,
769
+ });
770
+ this.unloadHandlers.push(() => {
771
+ window.removeEventListener("pagehide", handler, true);
772
+ window.removeEventListener("beforeunload", handler, true);
773
+ document.removeEventListener("visibilitychange", visibilityHandler, true);
774
+ });
775
+ }
776
+ refreshSessionCaptureEligibility() {
777
+ if (!this.config) {
778
+ this.sessionCaptureEligible = true;
779
+ return;
780
+ }
781
+ const rate = this.config.sampling.sessionSampleRate;
782
+ this.sessionCaptureEligible = rate >= 1 || Math.random() <= rate;
783
+ }
784
+ shouldCaptureEvent(event, _canonical) {
785
+ if (this.sessionCaptureEligible) {
786
+ return true;
787
+ }
788
+ if (event.type === "session_started" || event.type === "session_ended") {
789
+ return true;
790
+ }
791
+ this.internalMetrics.increment("sampling_dropped_count");
792
+ return false;
793
+ }
794
+ measureSerializedBytes(event) {
795
+ return new TextEncoder().encode(JSON.stringify(event)).byteLength;
796
+ }
797
+ shutdown() {
798
+ this.stopCaptureModules();
799
+ for (const dispose of this.unloadHandlers) {
800
+ try {
801
+ dispose();
802
+ }
803
+ catch {
804
+ // Ignore.
805
+ }
806
+ }
807
+ this.unloadHandlers = [];
808
+ this.authoringOverlayHandle?.stop();
809
+ this.authoringOverlayHandle = null;
810
+ this.authoringRecording.reset();
811
+ if (this.transport) {
812
+ this.transport.stop();
813
+ this.transport.flushOnUnload();
814
+ this.transport = null;
815
+ }
816
+ this.traceManager = null;
817
+ this.initialized = false;
818
+ this.initSignature = null;
819
+ }
820
+ updateActiveTraceSpan(event, eventId) {
821
+ if (!this.traceManager ||
822
+ isOfficialCustomLifecycleEvent(event) ||
823
+ !TRACE_ROOT_EVENTS.has(event.type)) {
824
+ return;
825
+ }
826
+ this.traceManager.setActiveSpan(eventId);
827
+ }
828
+ }
829
+ export const createClueBrowserSdk = () => {
830
+ return new ClueBrowserSdkImpl();
831
+ };