@genn-inc/cluebase-frontend-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 (140) hide show
  1. package/README.md +222 -0
  2. package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
  3. package/dist/_generated/correlation-attribute-keys.js +33 -0
  4. package/dist/_generated/identity-pii-policy.d.ts +38 -0
  5. package/dist/_generated/identity-pii-policy.js +103 -0
  6. package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
  7. package/dist/_generated/ingest-privacy-tables.js +890 -0
  8. package/dist/_generated/otel-resource-keys.d.ts +21 -0
  9. package/dist/_generated/otel-resource-keys.js +72 -0
  10. package/dist/_generated/otel-span-conversion.d.ts +48 -0
  11. package/dist/_generated/otel-span-conversion.js +69 -0
  12. package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
  13. package/dist/_generated/parameter-snapshot-leaf.js +37 -0
  14. package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
  15. package/dist/_generated/sdk-runtime-policy.js +14 -0
  16. package/dist/_generated/usage-governor-tables.d.ts +15 -0
  17. package/dist/_generated/usage-governor-tables.js +44 -0
  18. package/dist/context/browser-storage.d.ts +15 -0
  19. package/dist/context/browser-storage.js +38 -0
  20. package/dist/context/environment.d.ts +37 -0
  21. package/dist/context/environment.js +229 -0
  22. package/dist/context/identity.d.ts +16 -0
  23. package/dist/context/identity.js +139 -0
  24. package/dist/context/organization.d.ts +16 -0
  25. package/dist/context/organization.js +60 -0
  26. package/dist/context/session.d.ts +28 -0
  27. package/dist/context/session.js +130 -0
  28. package/dist/context/tab.d.ts +22 -0
  29. package/dist/context/tab.js +117 -0
  30. package/dist/context/trace.d.ts +76 -0
  31. package/dist/context/trace.js +163 -0
  32. package/dist/exporter/batch.d.ts +94 -0
  33. package/dist/exporter/batch.js +485 -0
  34. package/dist/exporter/browser-outbox-records.d.ts +159 -0
  35. package/dist/exporter/browser-outbox-records.js +341 -0
  36. package/dist/exporter/browser-outbox.d.ts +112 -0
  37. package/dist/exporter/browser-outbox.js +876 -0
  38. package/dist/exporter/client.d.ts +92 -0
  39. package/dist/exporter/client.js +541 -0
  40. package/dist/exporter/delivery-response-contract.d.ts +103 -0
  41. package/dist/exporter/delivery-response-contract.js +147 -0
  42. package/dist/exporter/delivery-response.d.ts +37 -0
  43. package/dist/exporter/delivery-response.js +151 -0
  44. package/dist/exporter/diagnostics.d.ts +42 -0
  45. package/dist/exporter/diagnostics.js +148 -0
  46. package/dist/exporter/event-id.d.ts +10 -0
  47. package/dist/exporter/event-id.js +75 -0
  48. package/dist/exporter/frontend-resource.d.ts +20 -0
  49. package/dist/exporter/frontend-resource.js +52 -0
  50. package/dist/exporter/local-minimization.d.ts +29 -0
  51. package/dist/exporter/local-minimization.js +87 -0
  52. package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
  53. package/dist/exporter/observation-source-event-exporter.js +154 -0
  54. package/dist/exporter/parameter-snapshot.d.ts +14 -0
  55. package/dist/exporter/parameter-snapshot.js +215 -0
  56. package/dist/exporter/path-template.d.ts +1 -0
  57. package/dist/exporter/path-template.js +67 -0
  58. package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
  59. package/dist/exporter/raw-event-to-source-signal.js +297 -0
  60. package/dist/exporter/retry.d.ts +8 -0
  61. package/dist/exporter/retry.js +8 -0
  62. package/dist/exporter/sanitize.d.ts +30 -0
  63. package/dist/exporter/sanitize.js +177 -0
  64. package/dist/exporter/schema-evidence.d.ts +20 -0
  65. package/dist/exporter/schema-evidence.js +232 -0
  66. package/dist/exporter/source-event.d.ts +99 -0
  67. package/dist/exporter/source-event.js +13 -0
  68. package/dist/exporter/span-to-source-signal.d.ts +8 -0
  69. package/dist/exporter/span-to-source-signal.js +174 -0
  70. package/dist/exporter/text-fingerprint.d.ts +9 -0
  71. package/dist/exporter/text-fingerprint.js +16 -0
  72. package/dist/index.d.ts +25 -0
  73. package/dist/index.js +158 -0
  74. package/dist/instrumentation/action.d.ts +2 -0
  75. package/dist/instrumentation/action.js +171 -0
  76. package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
  77. package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
  78. package/dist/instrumentation/common.d.ts +73 -0
  79. package/dist/instrumentation/common.js +90 -0
  80. package/dist/instrumentation/dom.d.ts +63 -0
  81. package/dist/instrumentation/dom.js +640 -0
  82. package/dist/instrumentation/drag.d.ts +2 -0
  83. package/dist/instrumentation/drag.js +82 -0
  84. package/dist/instrumentation/error.d.ts +2 -0
  85. package/dist/instrumentation/error.js +318 -0
  86. package/dist/instrumentation/form.d.ts +2 -0
  87. package/dist/instrumentation/form.js +132 -0
  88. package/dist/instrumentation/input.d.ts +2 -0
  89. package/dist/instrumentation/input.js +180 -0
  90. package/dist/instrumentation/location.d.ts +11 -0
  91. package/dist/instrumentation/location.js +69 -0
  92. package/dist/instrumentation/naming-evidence.d.ts +20 -0
  93. package/dist/instrumentation/naming-evidence.js +11 -0
  94. package/dist/instrumentation/navigation.d.ts +2 -0
  95. package/dist/instrumentation/navigation.js +144 -0
  96. package/dist/instrumentation/network-request.d.ts +103 -0
  97. package/dist/instrumentation/network-request.js +285 -0
  98. package/dist/instrumentation/network-span-attributes.d.ts +25 -0
  99. package/dist/instrumentation/network-span-attributes.js +153 -0
  100. package/dist/instrumentation/network.d.ts +16 -0
  101. package/dist/instrumentation/network.js +502 -0
  102. package/dist/instrumentation/page.d.ts +2 -0
  103. package/dist/instrumentation/page.js +56 -0
  104. package/dist/instrumentation/performance.d.ts +2 -0
  105. package/dist/instrumentation/performance.js +365 -0
  106. package/dist/integrations/next-router.d.ts +16 -0
  107. package/dist/integrations/next-router.js +18 -0
  108. package/dist/integrations/react-router.d.ts +7 -0
  109. package/dist/integrations/react-router.js +61 -0
  110. package/dist/internal/internal-log.d.ts +23 -0
  111. package/dist/internal/internal-log.js +47 -0
  112. package/dist/internal/metrics.d.ts +10 -0
  113. package/dist/internal/metrics.js +50 -0
  114. package/dist/provider/_version.d.ts +1 -0
  115. package/dist/provider/_version.js +3 -0
  116. package/dist/provider/config.d.ts +12 -0
  117. package/dist/provider/config.js +177 -0
  118. package/dist/provider/constants.d.ts +62 -0
  119. package/dist/provider/constants.js +118 -0
  120. package/dist/provider/contracts.d.ts +51 -0
  121. package/dist/provider/contracts.js +46 -0
  122. package/dist/provider/privacy-policy-store.d.ts +20 -0
  123. package/dist/provider/privacy-policy-store.js +56 -0
  124. package/dist/provider/sampling.d.ts +2 -0
  125. package/dist/provider/sampling.js +17 -0
  126. package/dist/provider/sdk-event-pipeline.d.ts +60 -0
  127. package/dist/provider/sdk-event-pipeline.js +478 -0
  128. package/dist/provider/sdk-input-normalization.d.ts +7 -0
  129. package/dist/provider/sdk-input-normalization.js +113 -0
  130. package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
  131. package/dist/provider/sdk-otel-pipeline.js +148 -0
  132. package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
  133. package/dist/provider/sdk-runtime-surfaces.js +216 -0
  134. package/dist/provider/sdk.d.ts +2 -0
  135. package/dist/provider/sdk.js +614 -0
  136. package/dist/provider/types.d.ts +323 -0
  137. package/dist/provider/types.js +1 -0
  138. package/dist/provider/usage-governor.d.ts +7 -0
  139. package/dist/provider/usage-governor.js +116 -0
  140. package/package.json +59 -0
@@ -0,0 +1,478 @@
1
+ import { TraceManager } from "../context/trace.js";
2
+ import { generateViewId } from "../exporter/event-id.js";
3
+ import { rawEventToSourceSignalEvent } from "../exporter/raw-event-to-source-signal.js";
4
+ import { createFrontendResourceAttributes } from "../exporter/frontend-resource.js";
5
+ import { sanitizeObject, } from "../exporter/sanitize.js";
6
+ import { DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK, hasUsageGovernorFeedbackChanged, isCostCapabilityDisabled, normalizeIngestUsageGovernorFeedback, } from "./usage-governor.js";
7
+ import { DEFAULT_LIFECYCLE_INTERACTION_GRACE_WINDOW_MS, } from "./constants.js";
8
+ import { FRONTEND_EVENT_NAME } from "./contracts.js";
9
+ import { shouldSampleSession } from "./sampling.js";
10
+ import { stripQueryAndHashFromUrl, toStringOrNull, toSubjectProfile, } from "./sdk-input-normalization.js";
11
+ const TRACE_ROOT_EVENTS = new Set([
12
+ "page_view",
13
+ "route_change",
14
+ "element_clicked",
15
+ "form_submitted",
16
+ "input_committed",
17
+ "toggle_changed",
18
+ "selection_committed",
19
+ "file_selected",
20
+ "drag_drop_completed",
21
+ "custom_emitted",
22
+ ]);
23
+ const SUBJECT_PROFILE_TRAIT_KEYS = new Set([
24
+ "name",
25
+ "displayName",
26
+ "email",
27
+ "emailAddress",
28
+ "avatarUrl",
29
+ ]);
30
+ export const OFFICIAL_CUSTOM_EVENT_NAME = {
31
+ identityIdentified: "identity_identified",
32
+ identityReset: "identity_reset",
33
+ organizationAssociated: "organization_associated",
34
+ sdkInitialized: "sdk_initialized",
35
+ };
36
+ const OFFICIAL_CUSTOM_EVENT_NAMES = new Set(Object.values(OFFICIAL_CUSTOM_EVENT_NAME));
37
+ const SUMMARY_EVENT_NAMES = new Set([
38
+ FRONTEND_EVENT_NAME.performanceSummaryObserved,
39
+ ]);
40
+ const INTERACTION_METADATA_BY_EVENT = {
41
+ element_clicked: { initiator: "click", startEventKind: "click" },
42
+ form_submitted: { initiator: "submit", startEventKind: "submit" },
43
+ input_committed: { initiator: "input_commit", startEventKind: "change" },
44
+ toggle_changed: { initiator: "toggle", startEventKind: "change" },
45
+ selection_committed: { initiator: "selection", startEventKind: "change" },
46
+ file_selected: { initiator: "file", startEventKind: "change" },
47
+ drag_drop_completed: { initiator: "drag_drop", startEventKind: "drop" },
48
+ custom_emitted: { initiator: "custom", startEventKind: "custom" },
49
+ };
50
+ export const readCurrentLifecycleUrl = () => {
51
+ if (typeof window === "undefined") {
52
+ return null;
53
+ }
54
+ return stripQueryAndHashFromUrl(window.location.href);
55
+ };
56
+ const readCurrentPath = () => {
57
+ if (typeof window === "undefined") {
58
+ return null;
59
+ }
60
+ return window.location.pathname || "/";
61
+ };
62
+ const isOfficialCustomLifecycleEvent = (event) => {
63
+ if (event.type !== "custom_emitted") {
64
+ return false;
65
+ }
66
+ const customEventName = event.payload.eventName;
67
+ const customEventOrigin = event.payload.customEventOrigin;
68
+ return (customEventOrigin === "official_sdk_lifecycle" &&
69
+ typeof customEventName === "string" &&
70
+ OFFICIAL_CUSTOM_EVENT_NAMES.has(customEventName));
71
+ };
72
+ const sessionCaptureEligibilityByTransport = new WeakMap();
73
+ const registerSessionCaptureEligibility = (transport, getEligibility) => {
74
+ sessionCaptureEligibilityByTransport.set(transport, getEligibility);
75
+ return () => {
76
+ if (sessionCaptureEligibilityByTransport.get(transport) === getEligibility) {
77
+ sessionCaptureEligibilityByTransport.delete(transport);
78
+ }
79
+ };
80
+ };
81
+ export const getSessionCaptureEligibility = (transport) => sessionCaptureEligibilityByTransport.get(transport)?.();
82
+ export class ProviderEventPipeline {
83
+ constructor() {
84
+ this.runtime = null;
85
+ this.currentViewId = null;
86
+ this.usageGovernorFeedback = DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK;
87
+ this.sessionCaptureEligible = true;
88
+ this.unregisterSessionCaptureEligibility = null;
89
+ }
90
+ configure(runtime) {
91
+ this.unregisterSessionCaptureEligibility?.();
92
+ this.runtime = runtime;
93
+ this.unregisterSessionCaptureEligibility =
94
+ registerSessionCaptureEligibility(runtime.transport, () => this.sessionCaptureEligible);
95
+ this.currentViewId = generateViewId();
96
+ this.usageGovernorFeedback = DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK;
97
+ this.refreshSessionCaptureEligibility();
98
+ }
99
+ clearRuntime() {
100
+ this.unregisterSessionCaptureEligibility?.();
101
+ this.unregisterSessionCaptureEligibility = null;
102
+ this.runtime = null;
103
+ this.usageGovernorFeedback = DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK;
104
+ this.sessionCaptureEligible = true;
105
+ }
106
+ resetViewId() {
107
+ this.currentViewId = generateViewId();
108
+ }
109
+ getViewId() {
110
+ if (!this.currentViewId) {
111
+ this.currentViewId = generateViewId();
112
+ }
113
+ return this.currentViewId;
114
+ }
115
+ getObservationContext() {
116
+ const identity = this.runtime?.identityManager.getContext();
117
+ const organization = this.runtime?.organizationManager.getContext();
118
+ const session = this.runtime?.sessionManager.getContext();
119
+ return {
120
+ anonymousId: identity?.anonymousId ?? null,
121
+ userId: identity?.userId ?? null,
122
+ organizationId: organization?.organizationId ?? null,
123
+ sessionId: session?.sessionId ?? null,
124
+ tabId: this.runtime?.tabManager.getTabId() ?? null,
125
+ };
126
+ }
127
+ enqueueRawEvent(event, skipSessionTouch = false) {
128
+ const runtime = this.runtime;
129
+ if (!runtime) {
130
+ return;
131
+ }
132
+ if (runtime.getConsent() === "denied") {
133
+ return;
134
+ }
135
+ if (!skipSessionTouch &&
136
+ event.type !== "session_started" &&
137
+ event.type !== "session_ended") {
138
+ this.handleSessionLifecycle(Date.now());
139
+ }
140
+ const eventWithInteraction = this.attachInteractionContext(event);
141
+ const eventWithLocation = this.attachCurrentLocation(eventWithInteraction);
142
+ if (eventWithLocation.type === "page_view") {
143
+ const nextViewId = toStringOrNull(eventWithLocation.payload.viewId);
144
+ if (nextViewId) {
145
+ this.currentViewId = nextViewId;
146
+ }
147
+ }
148
+ try {
149
+ const context = this.buildContext();
150
+ if (SUMMARY_EVENT_NAMES.has(eventWithLocation.type)) {
151
+ const summaryCount = eventWithLocation.payload.summaryCount;
152
+ const droppedCount = eventWithLocation.payload.droppedCount;
153
+ if (typeof summaryCount === "number" && summaryCount > 0) {
154
+ runtime.internalMetrics.increment("events_summarized_count", summaryCount);
155
+ }
156
+ if (typeof droppedCount === "number" && droppedCount > 0) {
157
+ runtime.internalMetrics.increment("sampling_dropped_count", droppedCount);
158
+ }
159
+ }
160
+ if (isCostCapabilityDisabled(this.usageGovernorFeedback, "event_collection")) {
161
+ runtime.internalMetrics.increment("dropped_count");
162
+ return;
163
+ }
164
+ const sourceSignal = rawEventToSourceSignalEvent(eventWithLocation, {
165
+ context,
166
+ resource: createFrontendResourceAttributes(runtime.config),
167
+ deniedKeys: runtime.config.privacy.deniedKeys,
168
+ onMaskingError: () => {
169
+ runtime.internalMetrics.increment("masking_error_count");
170
+ },
171
+ });
172
+ if (!this.shouldCaptureEvent(eventWithLocation)) {
173
+ runtime.internalMetrics.increment("dropped_count");
174
+ return;
175
+ }
176
+ const eventBytes = this.measureSerializedBytes(sourceSignal);
177
+ if (eventBytes > runtime.config.batch.maxEventBytes) {
178
+ runtime.internalMetrics.increment("dropped_count");
179
+ runtime.internalMetrics.increment("oversized_event_count");
180
+ runtime.internalMetrics.increment("oversized_event_dropped_count");
181
+ runtime.logger.warn("browser sdk dropped oversized event", {
182
+ eventType: eventWithInteraction.type,
183
+ eventBytes,
184
+ maxEventBytes: runtime.config.batch.maxEventBytes,
185
+ });
186
+ return;
187
+ }
188
+ this.updateActiveTraceSpan(eventWithLocation, sourceSignal.event_id);
189
+ runtime.transport.enqueue(sourceSignal);
190
+ }
191
+ catch {
192
+ runtime.logger.error("enqueueRawEvent failed", {
193
+ eventType: eventWithLocation.type,
194
+ error_code: "sdk_event_enqueue_failed",
195
+ });
196
+ }
197
+ }
198
+ applyUsageGovernorFeedback(feedback) {
199
+ const nextFeedback = normalizeIngestUsageGovernorFeedback(feedback);
200
+ if (!hasUsageGovernorFeedbackChanged(this.usageGovernorFeedback, nextFeedback)) {
201
+ return;
202
+ }
203
+ this.usageGovernorFeedback = nextFeedback;
204
+ }
205
+ refreshSessionCaptureEligibility() {
206
+ const config = this.runtime?.config;
207
+ if (!config) {
208
+ this.sessionCaptureEligible = true;
209
+ return;
210
+ }
211
+ const rate = config.sampling.sessionSampleRate;
212
+ this.sessionCaptureEligible = shouldSampleSession(rate);
213
+ }
214
+ buildContext() {
215
+ const runtime = this.runtime;
216
+ if (!runtime) {
217
+ throw new Error("SDK is not initialized");
218
+ }
219
+ const identity = runtime.identityManager.getContext();
220
+ const session = runtime.sessionManager.getContext();
221
+ const organization = runtime.organizationManager.getContext();
222
+ const environment = runtime.environmentManager.getContext();
223
+ const trace = runtime.traceManager.getSnapshot();
224
+ const requestContext = runtime.traceManager.createRequestContext() ??
225
+ new TraceManager().createRequestContext();
226
+ const privacySummary = {
227
+ maskedValueCount: 0,
228
+ droppedFieldCount: 0,
229
+ maskedFields: new Set(),
230
+ };
231
+ const subjectTraits = sanitizeContextTraits(identity.traits, runtime.config.privacy.deniedKeys, SUBJECT_PROFILE_TRAIT_KEYS, privacySummary, "subject.trait");
232
+ const organizationTraits = sanitizeContextTraits(organization.traits, runtime.config.privacy.deniedKeys, new Set(), privacySummary, "organization.trait");
233
+ const contextTraits = sanitizeContextTraits(environment.contextTraits, runtime.config.privacy.deniedKeys, new Set(), privacySummary, "context.trait");
234
+ return {
235
+ producer_id: runtime.config.producerId,
236
+ anonymous_id: identity.anonymousId,
237
+ user_id: identity.userId,
238
+ organization_id: organization.organizationId,
239
+ user_profile: toSubjectProfile(identity.traits),
240
+ session_id: session.sessionId,
241
+ tab_id: runtime.tabManager.getTabId(),
242
+ trace_id: trace.traceId ?? requestContext.traceId,
243
+ request_id: requestContext.requestId,
244
+ request_span_id: requestContext.requestSpanId,
245
+ interaction_id: runtime.traceManager.getActiveInteractionId() ??
246
+ requestContext.interactionId,
247
+ correlation_id: null,
248
+ sdk_collection_mode: runtime.config.collection.mode,
249
+ frontend_release: environment.frontendRelease,
250
+ locale: environment.locale,
251
+ country: environment.country,
252
+ device_type: environment.deviceType,
253
+ browser: environment.browser,
254
+ os: environment.os,
255
+ referrer: environment.referrer,
256
+ browser_family: environment.browserFamily,
257
+ browser_major_version: environment.browserMajorVersion,
258
+ browser_engine: environment.browserEngine,
259
+ os_family: environment.osFamily,
260
+ os_major_version: environment.osMajorVersion,
261
+ timezone: environment.timezone,
262
+ pointer_type_candidate: environment.pointerTypeCandidate,
263
+ touch_capable: environment.touchCapable,
264
+ cookies_enabled: environment.cookiesEnabled,
265
+ local_storage_available: environment.localStorageAvailable,
266
+ session_storage_available: environment.sessionStorageAvailable,
267
+ service_worker_supported: environment.serviceWorkerSupported,
268
+ webgl_supported: environment.webglSupported,
269
+ reduced_motion: environment.reducedMotion,
270
+ color_scheme: environment.colorScheme,
271
+ subject_traits: subjectTraits,
272
+ organization_traits: organizationTraits,
273
+ context_traits: contextTraits,
274
+ privacy_summary: {
275
+ level: "masked",
276
+ masked_value_count: privacySummary.maskedValueCount,
277
+ dropped_field_count: privacySummary.droppedFieldCount,
278
+ masked_fields: [...privacySummary.maskedFields].sort(),
279
+ rule_set_version: null,
280
+ },
281
+ };
282
+ }
283
+ attachInteractionContext(event) {
284
+ const traceManager = this.runtime?.traceManager;
285
+ const nowMs = event.occurredAtMs ?? Date.now();
286
+ const payloadInteractionId = toStringOrNull(event.payload.interactionId);
287
+ const occurrenceKind = "frontend_action";
288
+ if (event.type === "page_view") {
289
+ const interactionId = traceManager?.startFrontendAction({
290
+ interactionId: payloadInteractionId,
291
+ interactionInitiator: toStringOrNull(event.payload.interactionInitiator) ?? "page_visit",
292
+ interactionStartEventKind: toStringOrNull(event.payload.interactionStartEventKind) ??
293
+ "page_view",
294
+ viewId: toStringOrNull(event.payload.viewId),
295
+ nowMs,
296
+ }) ?? payloadInteractionId;
297
+ if (!interactionId)
298
+ return event;
299
+ return {
300
+ ...event,
301
+ payload: {
302
+ ...event.payload,
303
+ interactionId,
304
+ interactionInitiator: toStringOrNull(event.payload.interactionInitiator) ?? "page_visit",
305
+ interactionStartEventKind: toStringOrNull(event.payload.interactionStartEventKind) ??
306
+ "page_view",
307
+ occurrenceKind,
308
+ },
309
+ };
310
+ }
311
+ if (event.type === "page_leave") {
312
+ const interactionId = payloadInteractionId ??
313
+ traceManager?.getActiveInteractionId() ??
314
+ null;
315
+ if (!interactionId)
316
+ return event;
317
+ traceManager?.endFrontendAction(interactionId);
318
+ return {
319
+ ...event,
320
+ payload: {
321
+ ...event.payload,
322
+ interactionId,
323
+ occurrenceKind,
324
+ },
325
+ };
326
+ }
327
+ if (isOfficialCustomLifecycleEvent(event)) {
328
+ const lifecycleInteraction = traceManager?.getRecentInteractionSnapshot(nowMs, DEFAULT_LIFECYCLE_INTERACTION_GRACE_WINDOW_MS) ?? null;
329
+ const interactionId = payloadInteractionId ?? lifecycleInteraction?.interactionId ?? null;
330
+ if (!interactionId)
331
+ return event;
332
+ return {
333
+ ...event,
334
+ payload: {
335
+ ...event.payload,
336
+ interactionId,
337
+ occurrenceKind,
338
+ interactionInitiator: toStringOrNull(event.payload.interactionInitiator) ??
339
+ lifecycleInteraction?.interactionInitiator ??
340
+ null,
341
+ interactionStartEventKind: toStringOrNull(event.payload.interactionStartEventKind) ??
342
+ lifecycleInteraction?.interactionStartEventKind ??
343
+ null,
344
+ },
345
+ };
346
+ }
347
+ const interactionMetadata = INTERACTION_METADATA_BY_EVENT[event.type];
348
+ if (interactionMetadata || event.type === "custom_emitted") {
349
+ const interactionId = traceManager?.joinOrStartInteraction({
350
+ interactionId: payloadInteractionId,
351
+ interactionInitiator: toStringOrNull(event.payload.interactionInitiator) ??
352
+ interactionMetadata?.initiator,
353
+ interactionStartEventKind: toStringOrNull(event.payload.interactionStartEventKind) ??
354
+ interactionMetadata?.startEventKind,
355
+ targetStableKey: toStringOrNull(event.payload.stableKey),
356
+ formKey: toStringOrNull(event.payload.formKey),
357
+ interactionScopeKind: toStringOrNull(event.payload.interactionScopeKind),
358
+ interactionScopeKey: toStringOrNull(event.payload.interactionScopeKey),
359
+ nowMs,
360
+ }) ?? payloadInteractionId;
361
+ if (!interactionId)
362
+ return event;
363
+ return {
364
+ ...event,
365
+ payload: {
366
+ ...event.payload,
367
+ interactionId,
368
+ occurrenceKind,
369
+ interactionInitiator: toStringOrNull(event.payload.interactionInitiator) ??
370
+ interactionMetadata?.initiator,
371
+ interactionStartEventKind: toStringOrNull(event.payload.interactionStartEventKind) ??
372
+ interactionMetadata?.startEventKind,
373
+ },
374
+ };
375
+ }
376
+ const activeInteractionId = payloadInteractionId ??
377
+ traceManager?.getActiveInteractionId() ??
378
+ null;
379
+ if (!activeInteractionId || activeInteractionId === payloadInteractionId) {
380
+ return event;
381
+ }
382
+ return {
383
+ ...event,
384
+ payload: {
385
+ ...event.payload,
386
+ interactionId: activeInteractionId,
387
+ occurrenceKind,
388
+ },
389
+ };
390
+ }
391
+ attachCurrentLocation(event) {
392
+ const payload = event.payload;
393
+ if (payload.url !== undefined ||
394
+ payload.currentUrl !== undefined ||
395
+ payload.pageUrl !== undefined ||
396
+ payload.entryUrl !== undefined ||
397
+ payload.exitUrl !== undefined ||
398
+ payload.toUrl !== undefined) {
399
+ return event;
400
+ }
401
+ const currentUrl = readCurrentLifecycleUrl();
402
+ const path = payload.path ?? payload.toPath ?? readCurrentPath();
403
+ if (!currentUrl && !path) {
404
+ return event;
405
+ }
406
+ return {
407
+ ...event,
408
+ payload: {
409
+ ...payload,
410
+ ...(currentUrl ? { currentUrl } : {}),
411
+ ...(path ? { path } : {}),
412
+ },
413
+ };
414
+ }
415
+ handleSessionLifecycle(nowMs) {
416
+ const runtime = this.runtime;
417
+ if (!runtime) {
418
+ return;
419
+ }
420
+ const lifecycle = runtime.sessionManager.ensureActiveSession(nowMs);
421
+ if (lifecycle.ended) {
422
+ this.enqueueRawEvent({
423
+ type: "session_ended",
424
+ occurredAtMs: nowMs,
425
+ payload: {
426
+ sessionId: lifecycle.ended.sessionId,
427
+ exitUrl: readCurrentLifecycleUrl(),
428
+ durationMs: lifecycle.ended.durationMs,
429
+ },
430
+ }, true);
431
+ }
432
+ if (lifecycle.started) {
433
+ runtime.traceManager.reset();
434
+ this.currentViewId = generateViewId();
435
+ this.refreshSessionCaptureEligibility();
436
+ this.enqueueRawEvent({
437
+ type: "session_started",
438
+ occurredAtMs: nowMs,
439
+ payload: {
440
+ entryUrl: readCurrentLifecycleUrl(),
441
+ },
442
+ }, true);
443
+ }
444
+ }
445
+ shouldCaptureEvent(event) {
446
+ const runtime = this.runtime;
447
+ if (!runtime) {
448
+ return false;
449
+ }
450
+ if (this.sessionCaptureEligible) {
451
+ return true;
452
+ }
453
+ if (event.type === "session_started" || event.type === "session_ended") {
454
+ return true;
455
+ }
456
+ runtime.internalMetrics.increment("sampling_dropped_count");
457
+ return false;
458
+ }
459
+ measureSerializedBytes(event) {
460
+ return new TextEncoder().encode(JSON.stringify(event)).byteLength;
461
+ }
462
+ updateActiveTraceSpan(event, eventId) {
463
+ const traceManager = this.runtime?.traceManager;
464
+ if (!traceManager ||
465
+ isOfficialCustomLifecycleEvent(event) ||
466
+ !TRACE_ROOT_EVENTS.has(event.type)) {
467
+ return;
468
+ }
469
+ traceManager.setActiveSpan(eventId);
470
+ }
471
+ }
472
+ function sanitizeContextTraits(traits, deniedKeys, excludedKeys = new Set(), summary, pathPrefix = "") {
473
+ const sanitized = sanitizeObject(traits, deniedKeys, summary, pathPrefix);
474
+ if (typeof sanitized !== "object" || sanitized === null || Array.isArray(sanitized)) {
475
+ return {};
476
+ }
477
+ return Object.fromEntries(Object.entries(sanitized).filter(([key]) => !excludedKeys.has(key)));
478
+ }
@@ -0,0 +1,7 @@
1
+ import type { CluebaseSubjectProfile, CluebaseSubjectTraits } from "./types.js";
2
+ export declare const toStringOrNull: (value: unknown) => string | null;
3
+ export declare const normalizePublicIdentityId: (value: unknown) => string | null;
4
+ export declare const isValidPublicCustomEventName: (value: unknown) => value is string;
5
+ export declare const stripQueryAndHashFromUrl: (value: unknown) => string | null;
6
+ export declare const normalizeProfileAvatarUrl: (raw: unknown) => string | null;
7
+ export declare const toSubjectProfile: (traits: CluebaseSubjectTraits) => CluebaseSubjectProfile | null;
@@ -0,0 +1,113 @@
1
+ import { detectIdentityPII, hashIdentityValue } from "../exporter/sanitize.js";
2
+ const PUBLIC_CUSTOM_EVENT_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
3
+ const SAFE_DATA_IMAGE_MIME_TYPES = new Set([
4
+ "image/png",
5
+ "image/jpeg",
6
+ "image/jpg",
7
+ "image/gif",
8
+ "image/webp",
9
+ "image/avif",
10
+ ]);
11
+ export const toStringOrNull = (value) => {
12
+ return typeof value === "string" ? value : null;
13
+ };
14
+ export const normalizePublicIdentityId = (value) => {
15
+ if (typeof value !== "string") {
16
+ return null;
17
+ }
18
+ const trimmed = value.trim();
19
+ return trimmed.length > 0 ? trimmed : null;
20
+ };
21
+ export const isValidPublicCustomEventName = (value) => {
22
+ return (typeof value === "string" && PUBLIC_CUSTOM_EVENT_NAME_PATTERN.test(value));
23
+ };
24
+ export const stripQueryAndHashFromUrl = (value) => {
25
+ if (typeof value !== "string") {
26
+ return null;
27
+ }
28
+ const trimmed = value.trim();
29
+ if (!trimmed) {
30
+ return null;
31
+ }
32
+ try {
33
+ const parsed = new URL(trimmed);
34
+ parsed.search = "";
35
+ parsed.hash = "";
36
+ return parsed.toString();
37
+ }
38
+ catch {
39
+ const withoutHash = trimmed.split("#", 1)[0] ?? "";
40
+ const withoutQuery = withoutHash.split("?", 1)[0] ?? "";
41
+ return withoutQuery || null;
42
+ }
43
+ };
44
+ const isSafeDataImageUrl = (value) => {
45
+ const commaIndex = value.indexOf(",");
46
+ if (commaIndex <= "data:".length) {
47
+ return false;
48
+ }
49
+ const metadata = value.slice("data:".length, commaIndex).toLowerCase();
50
+ const mimeType = metadata.split(";", 1)[0] ?? "";
51
+ return SAFE_DATA_IMAGE_MIME_TYPES.has(mimeType);
52
+ };
53
+ export const normalizeProfileAvatarUrl = (raw) => {
54
+ const value = toStringOrNull(raw);
55
+ if (value === null) {
56
+ return null;
57
+ }
58
+ try {
59
+ const parsed = new URL(value);
60
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") {
61
+ if (!parsed.hostname || parsed.username || parsed.password) {
62
+ return null;
63
+ }
64
+ parsed.search = "";
65
+ parsed.hash = "";
66
+ return parsed.toString();
67
+ }
68
+ if (parsed.protocol === "data:" && isSafeDataImageUrl(value)) {
69
+ return value;
70
+ }
71
+ return null;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ };
77
+ const normalizeEmailTrait = (raw) => {
78
+ const value = toStringOrNull(raw);
79
+ if (value === null) {
80
+ return null;
81
+ }
82
+ const normalized = value.trim().toLowerCase();
83
+ return detectIdentityPII(normalized) === "email" ? normalized : null;
84
+ };
85
+ const emailDomain = (email) => {
86
+ if (email === null) {
87
+ return null;
88
+ }
89
+ const atIndex = email.lastIndexOf("@");
90
+ if (atIndex < 0 || atIndex === email.length - 1) {
91
+ return null;
92
+ }
93
+ return email.slice(atIndex + 1);
94
+ };
95
+ export const toSubjectProfile = (traits) => {
96
+ const displayName = toStringOrNull(traits.name ?? traits.displayName);
97
+ const avatarUrl = normalizeProfileAvatarUrl(traits.avatarUrl);
98
+ const contactEmail = normalizeEmailTrait(traits.email ?? traits.emailAddress);
99
+ const contactIdentityHash = contactEmail === null ? null : hashIdentityValue(contactEmail);
100
+ const contactDomain = emailDomain(contactEmail);
101
+ if (displayName === null &&
102
+ avatarUrl === null &&
103
+ contactIdentityHash === null &&
104
+ contactDomain === null) {
105
+ return null;
106
+ }
107
+ return {
108
+ display_name: displayName,
109
+ avatar_url: avatarUrl,
110
+ contact_identity_hash: contactIdentityHash,
111
+ contact_domain: contactDomain,
112
+ };
113
+ };
@@ -0,0 +1,14 @@
1
+ import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
2
+ import type { BatchTransport } from "../exporter/batch.js";
3
+ import type { CluebaseContext, ResolvedCluebaseConfig } from "./types.js";
4
+ export type CluebaseOtelProvider = WebTracerProvider;
5
+ type CreateCluebaseOtelPipelineOptions = {
6
+ resolved: ResolvedCluebaseConfig;
7
+ getTransport: () => BatchTransport | null;
8
+ getContext: () => CluebaseContext;
9
+ };
10
+ export declare const createCluebaseOtelPipeline: ({ resolved, getTransport, getContext, }: CreateCluebaseOtelPipelineOptions) => CluebaseOtelProvider | null;
11
+ export declare const shutdownCluebaseOtelProvider: (provider: CluebaseOtelProvider | null) => void;
12
+ export declare const shutdownCluebaseOtelProviderAsync: (provider: CluebaseOtelProvider | null) => Promise<void>;
13
+ export declare const forceFlushCluebaseOtelProvider: (provider: CluebaseOtelProvider | null, signal?: AbortSignal) => Promise<void>;
14
+ export {};