@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.
- package/README.md +222 -0
- package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
- package/dist/_generated/correlation-attribute-keys.js +33 -0
- package/dist/_generated/identity-pii-policy.d.ts +38 -0
- package/dist/_generated/identity-pii-policy.js +103 -0
- package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
- package/dist/_generated/ingest-privacy-tables.js +890 -0
- package/dist/_generated/otel-resource-keys.d.ts +21 -0
- package/dist/_generated/otel-resource-keys.js +72 -0
- package/dist/_generated/otel-span-conversion.d.ts +48 -0
- package/dist/_generated/otel-span-conversion.js +69 -0
- package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
- package/dist/_generated/parameter-snapshot-leaf.js +37 -0
- package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
- package/dist/_generated/sdk-runtime-policy.js +14 -0
- package/dist/_generated/usage-governor-tables.d.ts +15 -0
- package/dist/_generated/usage-governor-tables.js +44 -0
- package/dist/context/browser-storage.d.ts +15 -0
- package/dist/context/browser-storage.js +38 -0
- package/dist/context/environment.d.ts +37 -0
- package/dist/context/environment.js +229 -0
- package/dist/context/identity.d.ts +16 -0
- package/dist/context/identity.js +139 -0
- package/dist/context/organization.d.ts +16 -0
- package/dist/context/organization.js +60 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +130 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +117 -0
- package/dist/context/trace.d.ts +76 -0
- package/dist/context/trace.js +163 -0
- package/dist/exporter/batch.d.ts +94 -0
- package/dist/exporter/batch.js +485 -0
- package/dist/exporter/browser-outbox-records.d.ts +159 -0
- package/dist/exporter/browser-outbox-records.js +341 -0
- package/dist/exporter/browser-outbox.d.ts +112 -0
- package/dist/exporter/browser-outbox.js +876 -0
- package/dist/exporter/client.d.ts +92 -0
- package/dist/exporter/client.js +541 -0
- package/dist/exporter/delivery-response-contract.d.ts +103 -0
- package/dist/exporter/delivery-response-contract.js +147 -0
- package/dist/exporter/delivery-response.d.ts +37 -0
- package/dist/exporter/delivery-response.js +151 -0
- package/dist/exporter/diagnostics.d.ts +42 -0
- package/dist/exporter/diagnostics.js +148 -0
- package/dist/exporter/event-id.d.ts +10 -0
- package/dist/exporter/event-id.js +75 -0
- package/dist/exporter/frontend-resource.d.ts +20 -0
- package/dist/exporter/frontend-resource.js +52 -0
- package/dist/exporter/local-minimization.d.ts +29 -0
- package/dist/exporter/local-minimization.js +87 -0
- package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
- package/dist/exporter/observation-source-event-exporter.js +154 -0
- package/dist/exporter/parameter-snapshot.d.ts +14 -0
- package/dist/exporter/parameter-snapshot.js +215 -0
- package/dist/exporter/path-template.d.ts +1 -0
- package/dist/exporter/path-template.js +67 -0
- package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
- package/dist/exporter/raw-event-to-source-signal.js +297 -0
- package/dist/exporter/retry.d.ts +8 -0
- package/dist/exporter/retry.js +8 -0
- package/dist/exporter/sanitize.d.ts +30 -0
- package/dist/exporter/sanitize.js +177 -0
- package/dist/exporter/schema-evidence.d.ts +20 -0
- package/dist/exporter/schema-evidence.js +232 -0
- package/dist/exporter/source-event.d.ts +99 -0
- package/dist/exporter/source-event.js +13 -0
- package/dist/exporter/span-to-source-signal.d.ts +8 -0
- package/dist/exporter/span-to-source-signal.js +174 -0
- package/dist/exporter/text-fingerprint.d.ts +9 -0
- package/dist/exporter/text-fingerprint.js +16 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +158 -0
- package/dist/instrumentation/action.d.ts +2 -0
- package/dist/instrumentation/action.js +171 -0
- package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
- package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
- package/dist/instrumentation/common.d.ts +73 -0
- package/dist/instrumentation/common.js +90 -0
- package/dist/instrumentation/dom.d.ts +63 -0
- package/dist/instrumentation/dom.js +640 -0
- package/dist/instrumentation/drag.d.ts +2 -0
- package/dist/instrumentation/drag.js +82 -0
- package/dist/instrumentation/error.d.ts +2 -0
- package/dist/instrumentation/error.js +318 -0
- package/dist/instrumentation/form.d.ts +2 -0
- package/dist/instrumentation/form.js +132 -0
- package/dist/instrumentation/input.d.ts +2 -0
- package/dist/instrumentation/input.js +180 -0
- package/dist/instrumentation/location.d.ts +11 -0
- package/dist/instrumentation/location.js +69 -0
- package/dist/instrumentation/naming-evidence.d.ts +20 -0
- package/dist/instrumentation/naming-evidence.js +11 -0
- package/dist/instrumentation/navigation.d.ts +2 -0
- package/dist/instrumentation/navigation.js +144 -0
- package/dist/instrumentation/network-request.d.ts +103 -0
- package/dist/instrumentation/network-request.js +285 -0
- package/dist/instrumentation/network-span-attributes.d.ts +25 -0
- package/dist/instrumentation/network-span-attributes.js +153 -0
- package/dist/instrumentation/network.d.ts +16 -0
- package/dist/instrumentation/network.js +502 -0
- package/dist/instrumentation/page.d.ts +2 -0
- package/dist/instrumentation/page.js +56 -0
- package/dist/instrumentation/performance.d.ts +2 -0
- package/dist/instrumentation/performance.js +365 -0
- package/dist/integrations/next-router.d.ts +16 -0
- package/dist/integrations/next-router.js +18 -0
- package/dist/integrations/react-router.d.ts +7 -0
- package/dist/integrations/react-router.js +61 -0
- package/dist/internal/internal-log.d.ts +23 -0
- package/dist/internal/internal-log.js +47 -0
- package/dist/internal/metrics.d.ts +10 -0
- package/dist/internal/metrics.js +50 -0
- package/dist/provider/_version.d.ts +1 -0
- package/dist/provider/_version.js +3 -0
- package/dist/provider/config.d.ts +12 -0
- package/dist/provider/config.js +177 -0
- package/dist/provider/constants.d.ts +62 -0
- package/dist/provider/constants.js +118 -0
- package/dist/provider/contracts.d.ts +51 -0
- package/dist/provider/contracts.js +46 -0
- package/dist/provider/privacy-policy-store.d.ts +20 -0
- package/dist/provider/privacy-policy-store.js +56 -0
- package/dist/provider/sampling.d.ts +2 -0
- package/dist/provider/sampling.js +17 -0
- package/dist/provider/sdk-event-pipeline.d.ts +60 -0
- package/dist/provider/sdk-event-pipeline.js +478 -0
- package/dist/provider/sdk-input-normalization.d.ts +7 -0
- package/dist/provider/sdk-input-normalization.js +113 -0
- package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
- package/dist/provider/sdk-otel-pipeline.js +148 -0
- package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
- package/dist/provider/sdk-runtime-surfaces.js +216 -0
- package/dist/provider/sdk.d.ts +2 -0
- package/dist/provider/sdk.js +614 -0
- package/dist/provider/types.d.ts +323 -0
- package/dist/provider/types.js +1 -0
- package/dist/provider/usage-governor.d.ts +7 -0
- package/dist/provider/usage-governor.js +116 -0
- 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,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;
|