@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,365 @@
1
+ import { WebVitalsInstrumentation } from "@opentelemetry/browser-instrumentation/experimental/web-vitals";
2
+ import { DocumentLoadInstrumentation } from "@opentelemetry/instrumentation-document-load";
3
+ import { FRONTEND_EVENT_NAME } from "../provider/contracts.js";
4
+ import { currentPageUrl } from "./common.js";
5
+ const SUMMARY_WINDOW_MS = 60000;
6
+ const LONG_TASK_THRESHOLD_MS = 250;
7
+ const INTERACTION_LONG_TASK_THRESHOLD_MS = 100;
8
+ const INTERACTION_LONG_TASK_WINDOW_MS = 2000;
9
+ const createInitialState = () => ({
10
+ windowStartedAtMs: Date.now(),
11
+ summaryCount: 0,
12
+ droppedCount: 0,
13
+ longTaskCount: 0,
14
+ resourceFailureCount: 0,
15
+ worstDurationMs: 0,
16
+ worstMetricName: null,
17
+ webVitals: {
18
+ lcp_ms: null,
19
+ cls: null,
20
+ fcp_ms: null,
21
+ ttfb_ms: null,
22
+ inp_ms: null,
23
+ },
24
+ loadDurationMs: null,
25
+ interactionReadyMs: null,
26
+ lastInteractionAtMs: null,
27
+ });
28
+ const durationBucket = (value) => {
29
+ if (value < 100) {
30
+ return "lt_100ms";
31
+ }
32
+ if (value < 250) {
33
+ return "100_249ms";
34
+ }
35
+ if (value < 1000) {
36
+ return "250_999ms";
37
+ }
38
+ if (value < 2500) {
39
+ return "1000_2499ms";
40
+ }
41
+ return "gte_2500ms";
42
+ };
43
+ const countBucket = (value) => {
44
+ if (value <= 0) {
45
+ return "0";
46
+ }
47
+ if (value === 1) {
48
+ return "1";
49
+ }
50
+ if (value <= 3) {
51
+ return "2_3";
52
+ }
53
+ if (value <= 10) {
54
+ return "4_10";
55
+ }
56
+ return "gt_10";
57
+ };
58
+ const optionalDurationBucket = (value) => value === null ? null : durationBucket(value);
59
+ const ratingForSummary = (state) => {
60
+ const lcp = state.webVitals.lcp_ms;
61
+ const cls = state.webVitals.cls;
62
+ const inp = state.webVitals.inp_ms;
63
+ if ((lcp !== null && lcp > 4000) ||
64
+ (cls !== null && cls > 0.25) ||
65
+ (inp !== null && inp > 500) ||
66
+ state.worstDurationMs >= 2500) {
67
+ return "poor";
68
+ }
69
+ if ((lcp !== null && lcp > 2500) ||
70
+ (cls !== null && cls > 0.1) ||
71
+ (inp !== null && inp > 200) ||
72
+ state.worstDurationMs >= 1000) {
73
+ return "needs_improvement";
74
+ }
75
+ return "good";
76
+ };
77
+ const updateWorstDuration = (state, metricName, valueMs) => {
78
+ if (!Number.isFinite(valueMs) || valueMs < state.worstDurationMs) {
79
+ return;
80
+ }
81
+ state.worstDurationMs = Math.round(valueMs);
82
+ state.worstMetricName = metricName;
83
+ };
84
+ const observeEntryType = (observers, entryType, callback) => {
85
+ if (typeof PerformanceObserver === "undefined" ||
86
+ !PerformanceObserver.supportedEntryTypes?.includes(entryType)) {
87
+ return;
88
+ }
89
+ try {
90
+ const observer = new PerformanceObserver(callback);
91
+ observer.observe({ type: entryType, buffered: true });
92
+ observers.push(observer);
93
+ }
94
+ catch {
95
+ // Some browsers expose an entry type but still reject buffered observation.
96
+ }
97
+ };
98
+ const readPerformanceNavigation = () => {
99
+ if (typeof performance === "undefined") {
100
+ return null;
101
+ }
102
+ const entry = performance.getEntriesByType("navigation")[0];
103
+ return entry && typeof entry.duration === "number"
104
+ ? entry
105
+ : null;
106
+ };
107
+ const recordNavigationTiming = (state, navigation) => {
108
+ state.summaryCount += 1;
109
+ state.loadDurationMs = Math.max(0, Math.round(navigation.duration));
110
+ updateWorstDuration(state, "page_load", navigation.duration);
111
+ const ttfb = Math.max(0, navigation.responseStart - navigation.requestStart);
112
+ state.webVitals.ttfb_ms = Math.round(ttfb);
113
+ updateWorstDuration(state, "ttfb", ttfb);
114
+ };
115
+ const numberEntryField = (entry, key) => {
116
+ const value = entry[key];
117
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
118
+ };
119
+ const recordLcpEntry = (state, entry) => {
120
+ const valueMs = numberEntryField(entry, "renderTime") ??
121
+ numberEntryField(entry, "loadTime") ??
122
+ entry.startTime;
123
+ if (!Number.isFinite(valueMs)) {
124
+ return;
125
+ }
126
+ state.summaryCount += 1;
127
+ state.webVitals.lcp_ms = Math.round(valueMs);
128
+ updateWorstDuration(state, "lcp", valueMs);
129
+ };
130
+ const recordPaintEntry = (state, entry) => {
131
+ if (entry.name !== "first-contentful-paint") {
132
+ return;
133
+ }
134
+ state.summaryCount += 1;
135
+ state.webVitals.fcp_ms = Math.round(entry.startTime);
136
+ updateWorstDuration(state, "fcp", entry.startTime);
137
+ };
138
+ const recordLayoutShiftEntry = (state, entry) => {
139
+ if (entry.hadRecentInput === true) {
140
+ return;
141
+ }
142
+ const value = numberEntryField(entry, "value");
143
+ if (value === null) {
144
+ return;
145
+ }
146
+ state.summaryCount += 1;
147
+ state.webVitals.cls = Number(((state.webVitals.cls ?? 0) + value).toFixed(4));
148
+ };
149
+ const recordEventTimingEntry = (state, entry) => {
150
+ const interactionId = numberEntryField(entry, "interactionId");
151
+ if (interactionId === null || interactionId <= 0) {
152
+ return;
153
+ }
154
+ const durationMs = Math.max(0, Math.round(entry.duration));
155
+ state.summaryCount += 1;
156
+ state.webVitals.inp_ms = Math.max(state.webVitals.inp_ms ?? 0, durationMs);
157
+ updateWorstDuration(state, "inp", durationMs);
158
+ };
159
+ const readWebVitalName = (attributes) => {
160
+ const value = attributes?.["browser.web_vital.name"];
161
+ return typeof value === "string" && value.length > 0 ? value : null;
162
+ };
163
+ const readWebVitalValue = (attributes) => {
164
+ const value = attributes?.["browser.web_vital.value"];
165
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
166
+ };
167
+ export const startPerformanceCapture = (context) => {
168
+ if (typeof window === "undefined" || typeof document === "undefined") {
169
+ return { stop: () => undefined };
170
+ }
171
+ let state = createInitialState();
172
+ const observers = [];
173
+ const documentLoadInstrumentation = new DocumentLoadInstrumentation({
174
+ semconvStabilityOptIn: "http/dup",
175
+ applyCustomAttributesOnSpan: {
176
+ documentLoad: () => {
177
+ const navigation = readPerformanceNavigation();
178
+ if (navigation) {
179
+ recordNavigationTiming(state, navigation);
180
+ }
181
+ },
182
+ },
183
+ });
184
+ const webVitalsInstrumentation = new WebVitalsInstrumentation({
185
+ applyCustomLogRecordData: (logRecord) => {
186
+ const metricName = readWebVitalName(logRecord.attributes);
187
+ const metricValue = readWebVitalValue(logRecord.attributes);
188
+ if (!metricName || metricValue === null) {
189
+ return;
190
+ }
191
+ state.summaryCount += 1;
192
+ switch (metricName.toUpperCase()) {
193
+ case "LCP":
194
+ state.webVitals.lcp_ms = Math.round(metricValue);
195
+ updateWorstDuration(state, "lcp", metricValue);
196
+ break;
197
+ case "FCP":
198
+ state.webVitals.fcp_ms = Math.round(metricValue);
199
+ updateWorstDuration(state, "fcp", metricValue);
200
+ break;
201
+ case "INP":
202
+ state.webVitals.inp_ms = Math.round(metricValue);
203
+ updateWorstDuration(state, "inp", metricValue);
204
+ break;
205
+ case "CLS":
206
+ state.webVitals.cls = Number(metricValue.toFixed(4));
207
+ break;
208
+ case "TTFB":
209
+ state.webVitals.ttfb_ms = Math.round(metricValue);
210
+ updateWorstDuration(state, "ttfb", metricValue);
211
+ break;
212
+ default:
213
+ updateWorstDuration(state, metricName.toLowerCase(), metricValue);
214
+ break;
215
+ }
216
+ },
217
+ });
218
+ documentLoadInstrumentation.enable();
219
+ webVitalsInstrumentation.enable();
220
+ const emitSummary = (flushReason) => {
221
+ if (context.getConsent() === "denied") {
222
+ state = createInitialState();
223
+ return;
224
+ }
225
+ const summaryCount = state.summaryCount;
226
+ if (summaryCount === 0 && state.droppedCount === 0) {
227
+ return;
228
+ }
229
+ const viewId = context.getViewId();
230
+ const summaryWindowMs = Math.max(0, Date.now() - state.windowStartedAtMs);
231
+ context.onEvent({
232
+ type: FRONTEND_EVENT_NAME.performanceSummaryObserved,
233
+ occurredAtMs: Date.now(),
234
+ payload: {
235
+ viewId,
236
+ // must strip query string to avoid leaking OAuth
237
+ // callback / password-reset / magic-link tokens that may
238
+ // live in `window.location.href`. See `instrumentation/common.ts`.
239
+ pageUrl: currentPageUrl() ?? "",
240
+ collectorName: "performance_rum",
241
+ aggregationKind: "per_view_window",
242
+ summaryWindowMs,
243
+ summaryCount,
244
+ droppedCount: state.droppedCount,
245
+ sampleRate: 1,
246
+ budgetWindowMs: SUMMARY_WINDOW_MS,
247
+ rateLimitKey: viewId
248
+ ? `view:${viewId}:performance`
249
+ : "view:unknown:performance",
250
+ performanceKind: "view",
251
+ rating: ratingForSummary(state),
252
+ metricName: state.worstMetricName ?? "performance_summary",
253
+ metricValueBucket: durationBucket(state.worstDurationMs),
254
+ loadDurationBucket: optionalDurationBucket(state.loadDurationMs),
255
+ interactionReadyMsBucket: optionalDurationBucket(state.interactionReadyMs),
256
+ longTaskCountBucket: countBucket(state.longTaskCount),
257
+ resourceFailureCountBucket: countBucket(state.resourceFailureCount),
258
+ longTaskCount: state.longTaskCount,
259
+ resourceFailureCount: state.resourceFailureCount,
260
+ slowResourceCount: 0,
261
+ worstDurationBucket: durationBucket(state.worstDurationMs),
262
+ flushReason,
263
+ webVitalBuckets: {
264
+ lcp: optionalDurationBucket(state.webVitals.lcp_ms),
265
+ fcp: optionalDurationBucket(state.webVitals.fcp_ms),
266
+ ttfb: optionalDurationBucket(state.webVitals.ttfb_ms),
267
+ inp: optionalDurationBucket(state.webVitals.inp_ms),
268
+ cls: state.webVitals.cls === null
269
+ ? null
270
+ : state.webVitals.cls > 0.25
271
+ ? "poor"
272
+ : state.webVitals.cls > 0.1
273
+ ? "needs_improvement"
274
+ : "good",
275
+ },
276
+ },
277
+ });
278
+ state = createInitialState();
279
+ };
280
+ const intervalId = window.setInterval(() => {
281
+ emitSummary("interval");
282
+ }, SUMMARY_WINDOW_MS);
283
+ const pageHideHandler = () => {
284
+ emitSummary("pagehide");
285
+ };
286
+ const visibilityHandler = () => {
287
+ if (document.visibilityState === "hidden") {
288
+ emitSummary("visibility_hidden");
289
+ }
290
+ };
291
+ const resourceErrorHandler = (event) => {
292
+ if (context.getConsent() === "denied") {
293
+ return;
294
+ }
295
+ if (!(event.target instanceof Element)) {
296
+ return;
297
+ }
298
+ state.resourceFailureCount += 1;
299
+ state.summaryCount += 1;
300
+ };
301
+ const interactionHandler = () => {
302
+ state.lastInteractionAtMs = Date.now();
303
+ };
304
+ window.addEventListener("error", resourceErrorHandler, true);
305
+ window.addEventListener("pointerdown", interactionHandler, true);
306
+ window.addEventListener("pagehide", pageHideHandler, true);
307
+ document.addEventListener("visibilitychange", visibilityHandler, true);
308
+ // Long tasks and resource load failures are retained as Cluebase summary-only
309
+ // UX evidence because the accepted browser OTel modules above cover
310
+ // document load and Web Vitals, not this per-view aggregate.
311
+ observeEntryType(observers, "longtask", (list) => {
312
+ for (const entry of list.getEntries()) {
313
+ const nearInteraction = state.lastInteractionAtMs !== null &&
314
+ Date.now() - state.lastInteractionAtMs <=
315
+ INTERACTION_LONG_TASK_WINDOW_MS;
316
+ const threshold = nearInteraction
317
+ ? INTERACTION_LONG_TASK_THRESHOLD_MS
318
+ : LONG_TASK_THRESHOLD_MS;
319
+ if (entry.duration < threshold) {
320
+ continue;
321
+ }
322
+ state.longTaskCount += 1;
323
+ state.summaryCount += 1;
324
+ updateWorstDuration(state, "long_task", entry.duration);
325
+ }
326
+ });
327
+ observeEntryType(observers, "largest-contentful-paint", (list) => {
328
+ for (const entry of list.getEntries()) {
329
+ recordLcpEntry(state, entry);
330
+ }
331
+ });
332
+ observeEntryType(observers, "paint", (list) => {
333
+ for (const entry of list.getEntries()) {
334
+ recordPaintEntry(state, entry);
335
+ }
336
+ });
337
+ observeEntryType(observers, "layout-shift", (list) => {
338
+ for (const entry of list.getEntries()) {
339
+ recordLayoutShiftEntry(state, entry);
340
+ }
341
+ });
342
+ observeEntryType(observers, "event", (list) => {
343
+ for (const entry of list.getEntries()) {
344
+ recordEventTimingEntry(state, entry);
345
+ }
346
+ });
347
+ return {
348
+ flush: (reason) => {
349
+ emitSummary(reason);
350
+ },
351
+ stop: () => {
352
+ emitSummary("stop");
353
+ documentLoadInstrumentation.disable();
354
+ webVitalsInstrumentation.disable();
355
+ window.clearInterval(intervalId);
356
+ window.removeEventListener("error", resourceErrorHandler, true);
357
+ window.removeEventListener("pointerdown", interactionHandler, true);
358
+ window.removeEventListener("pagehide", pageHideHandler, true);
359
+ document.removeEventListener("visibilitychange", visibilityHandler, true);
360
+ for (const observer of observers) {
361
+ observer.disconnect();
362
+ }
363
+ },
364
+ };
365
+ };
@@ -0,0 +1,16 @@
1
+ export type NextRouterEventsLike = {
2
+ on: (event: string, handler: (...args: unknown[]) => void) => void;
3
+ off: (event: string, handler: (...args: unknown[]) => void) => void;
4
+ };
5
+ export type NextRouterLike = {
6
+ asPath?: string;
7
+ events: NextRouterEventsLike;
8
+ };
9
+ export type NextRouterIntegrationOptions = {
10
+ router: NextRouterLike;
11
+ onRouteChange: (fromPath: string, toPath: string) => void;
12
+ };
13
+ export type NextRouterIntegrationHandle = {
14
+ detach: () => void;
15
+ };
16
+ export declare const attachNextRouterIntegration: (options: NextRouterIntegrationOptions) => NextRouterIntegrationHandle;
@@ -0,0 +1,18 @@
1
+ export const attachNextRouterIntegration = (options) => {
2
+ const { router, onRouteChange } = options;
3
+ let lastPath = router.asPath ?? "";
4
+ const handler = (nextUrl) => {
5
+ const nextPath = typeof nextUrl === "string" ? nextUrl : (router.asPath ?? "");
6
+ if (!nextPath || nextPath === lastPath) {
7
+ return;
8
+ }
9
+ onRouteChange(lastPath, nextPath);
10
+ lastPath = nextPath;
11
+ };
12
+ router.events.on("routeChangeComplete", handler);
13
+ return {
14
+ detach: () => {
15
+ router.events.off("routeChangeComplete", handler);
16
+ },
17
+ };
18
+ };
@@ -0,0 +1,7 @@
1
+ export type ReactRouterIntegrationOptions = {
2
+ onRouteChange: (fromPath: string, toPath: string) => void;
3
+ };
4
+ export type ReactRouterIntegrationHandle = {
5
+ detach: () => void;
6
+ };
7
+ export declare const attachReactRouterIntegration: (options: ReactRouterIntegrationOptions) => ReactRouterIntegrationHandle;
@@ -0,0 +1,61 @@
1
+ import { HISTORY_WRAPPED_MARKER, isHistoryAlreadyCluebaseWrapped, } from "../instrumentation/common.js";
2
+ export const attachReactRouterIntegration = (options) => {
3
+ if (typeof window === "undefined") {
4
+ return {
5
+ detach: () => undefined,
6
+ };
7
+ }
8
+ let lastPath = window.location.pathname;
9
+ let attached = true;
10
+ const emit = () => {
11
+ if (!attached) {
12
+ return;
13
+ }
14
+ const nextPath = window.location.pathname;
15
+ if (nextPath === lastPath) {
16
+ return;
17
+ }
18
+ options.onRouteChange(lastPath, nextPath);
19
+ lastPath = nextPath;
20
+ };
21
+ // Idempotent guard for Cluebase-owned History API wrappers. OTel navigation
22
+ // instrumentation may patch history separately and remains the primary
23
+ // navigation source.
24
+ const sentinelPushState = history.pushState;
25
+ const sentinelReplaceState = history.replaceState;
26
+ const alreadyWrapped = isHistoryAlreadyCluebaseWrapped(history);
27
+ const originalPushState = sentinelPushState;
28
+ const originalReplaceState = sentinelReplaceState;
29
+ const wrappedPushState = function (...args) {
30
+ originalPushState.apply(this, args);
31
+ emit();
32
+ };
33
+ wrappedPushState[HISTORY_WRAPPED_MARKER] = true;
34
+ const wrappedReplaceState = function (...args) {
35
+ originalReplaceState.apply(this, args);
36
+ emit();
37
+ };
38
+ wrappedReplaceState[HISTORY_WRAPPED_MARKER] = true;
39
+ if (!alreadyWrapped) {
40
+ history.pushState = wrappedPushState;
41
+ history.replaceState = wrappedReplaceState;
42
+ }
43
+ const popstateHandler = () => {
44
+ emit();
45
+ };
46
+ window.addEventListener("popstate", popstateHandler, true);
47
+ return {
48
+ detach: () => {
49
+ attached = false;
50
+ if (!alreadyWrapped) {
51
+ if (history.pushState === wrappedPushState) {
52
+ history.pushState = originalPushState;
53
+ }
54
+ if (history.replaceState === wrappedReplaceState) {
55
+ history.replaceState = originalReplaceState;
56
+ }
57
+ }
58
+ window.removeEventListener("popstate", popstateHandler, true);
59
+ },
60
+ };
61
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * SDK-internal diagnostic logger for the Frontend SDK.
3
+ *
4
+ * Mirror of Node SDK ``internal-log.ts`` and Python SDK ``_internal_log.py``.
5
+ * Exposes a single configurable {@link InternalLogger}. Default is a no-op
6
+ * so the SDK does not write to the host page's console.
7
+ */
8
+ export type InternalLogContext = {
9
+ [key: string]: string | number | boolean | null;
10
+ };
11
+ type InternalLogContextInput = {
12
+ [key: string]: string | number | boolean | null | undefined;
13
+ };
14
+ export interface InternalLogger {
15
+ debug(message: string, context?: InternalLogContext): void;
16
+ info(message: string, context?: InternalLogContext): void;
17
+ warn(message: string, context?: InternalLogContext): void;
18
+ error(message: string, context?: InternalLogContext): void;
19
+ }
20
+ export declare function setInternalLogger(logger: InternalLogger | null | undefined): void;
21
+ export declare function getInternalLogger(): InternalLogger;
22
+ export declare function logSwallowed(operation: string, _cause: unknown, context?: InternalLogContextInput): void;
23
+ export {};
@@ -0,0 +1,47 @@
1
+ /**
2
+ * SDK-internal diagnostic logger for the Frontend SDK.
3
+ *
4
+ * Mirror of Node SDK ``internal-log.ts`` and Python SDK ``_internal_log.py``.
5
+ * Exposes a single configurable {@link InternalLogger}. Default is a no-op
6
+ * so the SDK does not write to the host page's console.
7
+ */
8
+ const SAFE_CONTEXT_KEYS = new Set([
9
+ "attempt",
10
+ "code",
11
+ "count",
12
+ "error_code",
13
+ "http_status",
14
+ "operation",
15
+ "reason",
16
+ "strategy",
17
+ ]);
18
+ const SAFE_TOKEN = /^[a-z0-9_.-]{1,128}$/;
19
+ const NO_OP_LOGGER = {
20
+ debug() { },
21
+ info() { },
22
+ warn() { },
23
+ error() { },
24
+ };
25
+ let currentLogger = NO_OP_LOGGER;
26
+ export function setInternalLogger(logger) {
27
+ currentLogger = logger ?? NO_OP_LOGGER;
28
+ }
29
+ export function getInternalLogger() {
30
+ return currentLogger;
31
+ }
32
+ export function logSwallowed(operation, _cause, context = {}) {
33
+ const details = {};
34
+ for (const [key, value] of Object.entries(context)) {
35
+ if (SAFE_CONTEXT_KEYS.has(key) &&
36
+ (value === null || typeof value === "boolean" ||
37
+ (typeof value === "number" && Number.isFinite(value)) ||
38
+ (typeof value === "string" && SAFE_TOKEN.test(value)))) {
39
+ details[key] = value;
40
+ }
41
+ }
42
+ // Exception text and stacks may contain customer payloads, credentials, or
43
+ // source paths. The internal logger is an optional diagnostic sink, so it
44
+ // keeps only bounded caller-provided classification fields, matching the
45
+ // backend SDKs and the API/worker log contract.
46
+ currentLogger.debug(`cluebase.sdk swallowed ${operation}`, details);
47
+ }
@@ -0,0 +1,10 @@
1
+ export type InternalMetricKey = "enqueue_count" | "sent_count" | "send_failure_count" | "dropped_count" | "events_summarized_count" | "sampling_dropped_count" | "oversized_event_count" | "oversized_event_dropped_count" | "snapshot_truncated_count" | "retry_count" | "batch_flush_count" | "payload_size_bytes" | "masking_error_count" | "capture_error_count" | "transport_failed_count" | "transport_retry_count" | "transport_requeue_count" | "browser_token_failure_count" | "browser_token_missing_cached_count" | "pending_batch_count" | "public_drop_count" | "ingest_dropped_event_count" | "parameter_snapshot_oversized_dropped";
2
+ export type InternalMetricsSnapshot = Record<InternalMetricKey, number>;
3
+ export declare class InternalMetricsStore {
4
+ private values;
5
+ constructor();
6
+ increment(key: InternalMetricKey, by?: number): void;
7
+ set(key: InternalMetricKey, value: number): void;
8
+ getSnapshot(): InternalMetricsSnapshot;
9
+ reset(): void;
10
+ }
@@ -0,0 +1,50 @@
1
+ const createInitialSnapshot = () => {
2
+ return {
3
+ enqueue_count: 0,
4
+ sent_count: 0,
5
+ send_failure_count: 0,
6
+ dropped_count: 0,
7
+ events_summarized_count: 0,
8
+ sampling_dropped_count: 0,
9
+ oversized_event_count: 0,
10
+ oversized_event_dropped_count: 0,
11
+ snapshot_truncated_count: 0,
12
+ retry_count: 0,
13
+ batch_flush_count: 0,
14
+ payload_size_bytes: 0,
15
+ masking_error_count: 0,
16
+ capture_error_count: 0,
17
+ transport_failed_count: 0,
18
+ transport_retry_count: 0,
19
+ transport_requeue_count: 0,
20
+ browser_token_failure_count: 0,
21
+ browser_token_missing_cached_count: 0,
22
+ pending_batch_count: 0,
23
+ public_drop_count: 0,
24
+ ingest_dropped_event_count: 0,
25
+ parameter_snapshot_oversized_dropped: 0,
26
+ };
27
+ };
28
+ export class InternalMetricsStore {
29
+ constructor() {
30
+ this.values = createInitialSnapshot();
31
+ }
32
+ increment(key, by = 1) {
33
+ if (!Number.isFinite(by)) {
34
+ return;
35
+ }
36
+ this.values[key] += by;
37
+ }
38
+ set(key, value) {
39
+ if (!Number.isFinite(value)) {
40
+ return;
41
+ }
42
+ this.values[key] = value;
43
+ }
44
+ getSnapshot() {
45
+ return { ...this.values };
46
+ }
47
+ reset() {
48
+ this.values = createInitialSnapshot();
49
+ }
50
+ }
@@ -0,0 +1 @@
1
+ export declare const SDK_VERSION: "0.0.1";
@@ -0,0 +1,3 @@
1
+ // This file is generated by scripts/generate-version-module.mjs.
2
+ // Do NOT edit by hand. The source of truth is `package.json` `version`.
3
+ export const SDK_VERSION = "0.0.1";
@@ -0,0 +1,12 @@
1
+ import type { CluebaseInitOptions, ResolvedCluebaseConfig } from "./types.js";
2
+ /**
3
+ * Read an address Cluebase is able to send to, or `null` when it cannot.
4
+ *
5
+ * `cluebase.init` refuses such an endpoint loudly and `resolveConfig` drops it to
6
+ * `""`; the two reactions differ but the question does not, so neither restates
7
+ * the rule.
8
+ */
9
+ export declare const parseSendableUrl: (value: string) => URL | null;
10
+ export declare const resolveConfig: (options: CluebaseInitOptions) => ResolvedCluebaseConfig;
11
+ export declare const validateConfig: (config: ResolvedCluebaseConfig) => string[];
12
+ export declare const isConfigValid: (config: ResolvedCluebaseConfig) => boolean;