@senzops/web 1.3.1 → 1.3.2
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/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.global.js +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/analytics.ts +148 -0
- package/src/index.ts +7 -519
- package/src/rum.ts +443 -0
- package/src/utils.ts +40 -0
package/src/rum.ts
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { generateHex, generateUUID, getBrowserContext, getPayloadSize } from './utils';
|
|
2
|
+
|
|
3
|
+
export interface RumConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
sampleRate?: number; // 0.0 to 1.0 (Defaults to 1.0)
|
|
7
|
+
allowedOrigins?: (string | RegExp)[]; // Origins allowed to receive W3C traceparent headers
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class SenzorRumAgent {
|
|
11
|
+
private config: RumConfig = { apiKey: '', sampleRate: 1.0, allowedOrigins: [] };
|
|
12
|
+
private endpoint: string = 'https://api.senzor.dev/api/ingest/rum';
|
|
13
|
+
private initialized: boolean = false;
|
|
14
|
+
private isSampled: boolean = true;
|
|
15
|
+
|
|
16
|
+
// State
|
|
17
|
+
private sessionId: string = '';
|
|
18
|
+
private traceId: string = '';
|
|
19
|
+
private traceStartTime: number = 0;
|
|
20
|
+
private isInitialLoad: boolean = true;
|
|
21
|
+
|
|
22
|
+
// Buffers
|
|
23
|
+
private spans: any[] = [];
|
|
24
|
+
private errors: any[] = [];
|
|
25
|
+
private breadcrumbs: any[] = [];
|
|
26
|
+
private vitals: any = {};
|
|
27
|
+
private frustrations = { rageClicks: 0, deadClicks: 0, errorCount: 0 };
|
|
28
|
+
private clickHistory: { x: number; y: number; time: number }[] = [];
|
|
29
|
+
|
|
30
|
+
// Intervals
|
|
31
|
+
private flushInterval: any;
|
|
32
|
+
|
|
33
|
+
public init(config: RumConfig) {
|
|
34
|
+
if (this.initialized) return;
|
|
35
|
+
this.initialized = true;
|
|
36
|
+
this.config = { ...this.config, ...config };
|
|
37
|
+
if (config.endpoint) this.endpoint = config.endpoint;
|
|
38
|
+
|
|
39
|
+
if (!this.config.apiKey) {
|
|
40
|
+
console.error('[Senzor RUM] apiKey is required.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Determine Sampling (Errors are ALWAYS 100% sampled, only Traces drop)
|
|
45
|
+
this.isSampled = Math.random() <= (this.config.sampleRate ?? 1.0);
|
|
46
|
+
|
|
47
|
+
this.manageSession();
|
|
48
|
+
this.startNewTrace(true);
|
|
49
|
+
|
|
50
|
+
this.setupErrorListeners();
|
|
51
|
+
this.setupPerformanceObservers();
|
|
52
|
+
this.setupUXListeners();
|
|
53
|
+
if (this.isSampled) this.patchNetwork();
|
|
54
|
+
|
|
55
|
+
// Micro-batch flush every 10s
|
|
56
|
+
this.flushInterval = setInterval(() => this.flush(), 10000);
|
|
57
|
+
|
|
58
|
+
// SPA and Unload Listeners
|
|
59
|
+
this.setupRoutingListeners();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private manageSession() {
|
|
63
|
+
if (!sessionStorage.getItem('sz_rum_sid')) {
|
|
64
|
+
sessionStorage.setItem('sz_rum_sid', generateUUID());
|
|
65
|
+
}
|
|
66
|
+
this.sessionId = sessionStorage.getItem('sz_rum_sid') as string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private startNewTrace(isInitialLoad: boolean) {
|
|
70
|
+
this.traceId = generateHex(32); // W3C Standard Trace ID
|
|
71
|
+
this.traceStartTime = Date.now();
|
|
72
|
+
this.isInitialLoad = isInitialLoad;
|
|
73
|
+
this.spans = [];
|
|
74
|
+
this.vitals = {};
|
|
75
|
+
this.frustrations = { rageClicks: 0, deadClicks: 0, errorCount: 0 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Breadcrumbs (For Error Context) ---
|
|
79
|
+
private addBreadcrumb(type: string, message: string, data?: any) {
|
|
80
|
+
this.breadcrumbs.push({ type, message, data, time: Date.now() });
|
|
81
|
+
if (this.breadcrumbs.length > 15) this.breadcrumbs.shift(); // Keep last 15 actions
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- 1. UX Frustration Detection ---
|
|
85
|
+
private setupUXListeners() {
|
|
86
|
+
document.addEventListener('click', (e) => {
|
|
87
|
+
const target = e.target as HTMLElement;
|
|
88
|
+
const tag = target.tagName ? target.tagName.toLowerCase() : '';
|
|
89
|
+
|
|
90
|
+
// Breadcrumb
|
|
91
|
+
this.addBreadcrumb('click', `Clicked ${tag}${target.id ? '#' + target.id : ''}${target.className ? '.' + target.className.split(' ')[0] : ''}`);
|
|
92
|
+
|
|
93
|
+
// Dead Click Heuristic
|
|
94
|
+
const interactiveElements = ['a', 'button', 'input', 'select', 'textarea', 'label'];
|
|
95
|
+
const isInteractive = interactiveElements.includes(tag) || target.closest('button') || target.closest('a') || target.hasAttribute('role') || target.onclick;
|
|
96
|
+
if (!isInteractive) {
|
|
97
|
+
this.frustrations.deadClicks++;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Rage Click Heuristic
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
this.clickHistory.push({ x: e.clientX, y: e.clientY, time: now });
|
|
103
|
+
this.clickHistory = this.clickHistory.filter(c => now - c.time < 1000);
|
|
104
|
+
|
|
105
|
+
if (this.clickHistory.length >= 3) {
|
|
106
|
+
const first = this.clickHistory[0];
|
|
107
|
+
let isRage = true;
|
|
108
|
+
for (let i = 1; i < this.clickHistory.length; i++) {
|
|
109
|
+
const dx = Math.abs(this.clickHistory[i].x - first.x);
|
|
110
|
+
const dy = Math.abs(this.clickHistory[i].y - first.y);
|
|
111
|
+
if (dx > 50 || dy > 50) isRage = false;
|
|
112
|
+
}
|
|
113
|
+
if (isRage) {
|
|
114
|
+
this.frustrations.rageClicks++;
|
|
115
|
+
this.addBreadcrumb('frustration', 'Rage Click Detected');
|
|
116
|
+
this.clickHistory = []; // Reset
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, { capture: true, passive: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- 2. Google Core Web Vitals ---
|
|
123
|
+
private setupPerformanceObservers() {
|
|
124
|
+
if (!this.isSampled || typeof PerformanceObserver === 'undefined') return;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
new PerformanceObserver((entryList) => {
|
|
128
|
+
for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
|
|
129
|
+
this.vitals.fcp = entry.startTime;
|
|
130
|
+
}
|
|
131
|
+
}).observe({ type: 'paint', buffered: true });
|
|
132
|
+
|
|
133
|
+
new PerformanceObserver((entryList) => {
|
|
134
|
+
const entries = entryList.getEntries();
|
|
135
|
+
const lastEntry = entries[entries.length - 1];
|
|
136
|
+
if (lastEntry) this.vitals.lcp = lastEntry.startTime;
|
|
137
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
138
|
+
|
|
139
|
+
let clsScore = 0;
|
|
140
|
+
new PerformanceObserver((entryList) => {
|
|
141
|
+
for (const entry of entryList.getEntries()) {
|
|
142
|
+
if (!(entry as any).hadRecentInput) {
|
|
143
|
+
clsScore += (entry as any).value;
|
|
144
|
+
this.vitals.cls = clsScore;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
148
|
+
|
|
149
|
+
new PerformanceObserver((entryList) => {
|
|
150
|
+
for (const entry of entryList.getEntries()) {
|
|
151
|
+
const evt = entry as any;
|
|
152
|
+
const delay = evt.duration || (evt.processingStart && evt.startTime ? evt.processingStart - evt.startTime : 0);
|
|
153
|
+
if (!this.vitals.inp || delay > this.vitals.inp) {
|
|
154
|
+
this.vitals.inp = delay;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}).observe({ type: 'event', buffered: true, durationThreshold: 40 } as any);
|
|
158
|
+
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// Browser doesn't support specific observer type, degrade gracefully
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private getNavigationTimings() {
|
|
165
|
+
if (typeof performance === 'undefined') return {};
|
|
166
|
+
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
|
167
|
+
if (!nav) return {};
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
dns: Math.max(0, nav.domainLookupEnd - nav.domainLookupStart),
|
|
171
|
+
tcp: Math.max(0, nav.connectEnd - nav.connectStart),
|
|
172
|
+
ssl: nav.secureConnectionStart ? Math.max(0, nav.requestStart - nav.secureConnectionStart) : 0,
|
|
173
|
+
ttfb: Math.max(0, nav.responseStart - nav.requestStart),
|
|
174
|
+
domInteractive: Math.max(0, nav.domInteractive - nav.startTime),
|
|
175
|
+
domComplete: Math.max(0, nav.domComplete - nav.startTime),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- 3. Distributed Tracing & Verbose Network Meta ---
|
|
180
|
+
private shouldAttachTraceHeader(url: string): boolean {
|
|
181
|
+
if (!this.config.allowedOrigins || this.config.allowedOrigins.length === 0) return false;
|
|
182
|
+
try {
|
|
183
|
+
const targetUrl = new URL(url, window.location.origin);
|
|
184
|
+
return this.config.allowedOrigins.some(allowed => {
|
|
185
|
+
if (typeof allowed === 'string') return targetUrl.origin.includes(allowed);
|
|
186
|
+
if (allowed instanceof RegExp) return allowed.test(targetUrl.origin);
|
|
187
|
+
return false;
|
|
188
|
+
});
|
|
189
|
+
} catch { return false; }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private patchNetwork() {
|
|
193
|
+
const self = this;
|
|
194
|
+
|
|
195
|
+
// --- Patch XHR ---
|
|
196
|
+
const originalXhrOpen = XMLHttpRequest.prototype.open;
|
|
197
|
+
const originalXhrSend = XMLHttpRequest.prototype.send;
|
|
198
|
+
const originalXhrSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
199
|
+
|
|
200
|
+
XMLHttpRequest.prototype.open = function (method: string, url: string, ...rest: any[]) {
|
|
201
|
+
(this as any).__szMethod = method.toUpperCase();
|
|
202
|
+
(this as any).__szUrl = url;
|
|
203
|
+
(this as any).__szHeaders = {};
|
|
204
|
+
return originalXhrOpen.apply(this, [method, url, ...rest] as any);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
XMLHttpRequest.prototype.setRequestHeader = function (header: string, value: string) {
|
|
208
|
+
(this as any).__szHeaders[header] = value;
|
|
209
|
+
return originalXhrSetRequestHeader.call(this, header, value);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
XMLHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) {
|
|
213
|
+
const xhr = this as any;
|
|
214
|
+
const spanId = generateHex(16);
|
|
215
|
+
const startTime = Date.now() - self.traceStartTime;
|
|
216
|
+
const method = xhr.__szMethod;
|
|
217
|
+
let fullUrl = xhr.__szUrl;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
fullUrl = new URL(xhr.__szUrl, window.location.origin).toString();
|
|
221
|
+
} catch (e) { /* ignore */ }
|
|
222
|
+
|
|
223
|
+
if (self.shouldAttachTraceHeader(fullUrl)) {
|
|
224
|
+
xhr.setRequestHeader('traceparent', `00-${self.traceId}-${spanId}-01`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
xhr.addEventListener('loadend', () => {
|
|
228
|
+
const duration = (Date.now() - self.traceStartTime) - startTime;
|
|
229
|
+
|
|
230
|
+
// Capture Verbose Metadata for XHR
|
|
231
|
+
const meta: any = {
|
|
232
|
+
url: fullUrl,
|
|
233
|
+
method: method,
|
|
234
|
+
library: 'xhr',
|
|
235
|
+
status: xhr.status,
|
|
236
|
+
responseType: xhr.responseType,
|
|
237
|
+
requestPayloadSize: getPayloadSize(body)
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const responseLength = xhr.responseText ? xhr.responseText.length : undefined;
|
|
242
|
+
if (responseLength) meta.responsePayloadSize = responseLength;
|
|
243
|
+
} catch (e) { /* Ignore responseText access errors on binary/blob */ }
|
|
244
|
+
|
|
245
|
+
self.spans.push({
|
|
246
|
+
spanId,
|
|
247
|
+
name: `${method} ${new URL(fullUrl, window.location.origin).pathname}`,
|
|
248
|
+
type: 'http',
|
|
249
|
+
startTime,
|
|
250
|
+
duration,
|
|
251
|
+
status: xhr.status,
|
|
252
|
+
meta
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return originalXhrSend.call(this, body);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// --- Patch Fetch ---
|
|
260
|
+
const originalFetch = window.fetch;
|
|
261
|
+
window.fetch = async function (...args) {
|
|
262
|
+
const requestInfo = args[0];
|
|
263
|
+
const init = args[1];
|
|
264
|
+
|
|
265
|
+
let url = '';
|
|
266
|
+
let method = 'GET';
|
|
267
|
+
|
|
268
|
+
if (typeof requestInfo === 'string' || requestInfo instanceof URL) {
|
|
269
|
+
url = requestInfo.toString();
|
|
270
|
+
method = (init?.method || 'GET').toUpperCase();
|
|
271
|
+
} else if (requestInfo instanceof Request) {
|
|
272
|
+
url = requestInfo.url;
|
|
273
|
+
method = requestInfo.method.toUpperCase();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let fullUrl = url;
|
|
277
|
+
try { fullUrl = new URL(url, window.location.origin).toString(); } catch (e) { }
|
|
278
|
+
|
|
279
|
+
const spanId = generateHex(16);
|
|
280
|
+
const startTime = Date.now() - self.traceStartTime;
|
|
281
|
+
|
|
282
|
+
// Safely inject traceparent without breaking Streams
|
|
283
|
+
if (self.shouldAttachTraceHeader(fullUrl)) {
|
|
284
|
+
const traceHeader = `00-${self.traceId}-${spanId}-01`;
|
|
285
|
+
if (requestInfo instanceof Request) {
|
|
286
|
+
const currentHeaders = new Headers(requestInfo.headers);
|
|
287
|
+
currentHeaders.set('traceparent', traceHeader);
|
|
288
|
+
args[1] = { ...(init || {}), headers: currentHeaders };
|
|
289
|
+
} else {
|
|
290
|
+
const currentHeaders = new Headers(init?.headers || {});
|
|
291
|
+
currentHeaders.set('traceparent', traceHeader);
|
|
292
|
+
args[1] = { ...(init || {}), headers: currentHeaders };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const response = await originalFetch.apply(this, args);
|
|
298
|
+
const duration = (Date.now() - self.traceStartTime) - startTime;
|
|
299
|
+
|
|
300
|
+
self.spans.push({
|
|
301
|
+
spanId,
|
|
302
|
+
name: `${method} ${new URL(fullUrl, window.location.origin).pathname}`,
|
|
303
|
+
type: 'http',
|
|
304
|
+
startTime,
|
|
305
|
+
duration,
|
|
306
|
+
status: response.status,
|
|
307
|
+
meta: {
|
|
308
|
+
url: fullUrl,
|
|
309
|
+
method,
|
|
310
|
+
library: 'fetch',
|
|
311
|
+
status: response.status,
|
|
312
|
+
statusText: response.statusText,
|
|
313
|
+
type: response.type,
|
|
314
|
+
redirected: response.redirected,
|
|
315
|
+
requestPayloadSize: getPayloadSize(init?.body)
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
return response;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
const duration = (Date.now() - self.traceStartTime) - startTime;
|
|
321
|
+
|
|
322
|
+
self.spans.push({
|
|
323
|
+
spanId,
|
|
324
|
+
name: `${method} ${new URL(fullUrl, window.location.origin).pathname}`,
|
|
325
|
+
type: 'http',
|
|
326
|
+
startTime,
|
|
327
|
+
duration,
|
|
328
|
+
status: 0,
|
|
329
|
+
meta: {
|
|
330
|
+
url: fullUrl,
|
|
331
|
+
method,
|
|
332
|
+
library: 'fetch',
|
|
333
|
+
status: 0,
|
|
334
|
+
error: error instanceof Error ? error.message : String(error),
|
|
335
|
+
requestPayloadSize: getPayloadSize(init?.body)
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --- 4. Universal Error Engine Hooks ---
|
|
344
|
+
private setupErrorListeners() {
|
|
345
|
+
const handleGlobalError = (errorObj: Error, type: string) => {
|
|
346
|
+
this.frustrations.errorCount++;
|
|
347
|
+
const message = errorObj.message || String(errorObj);
|
|
348
|
+
|
|
349
|
+
this.errors.push({
|
|
350
|
+
errorClass: errorObj.name || 'Error',
|
|
351
|
+
message: message,
|
|
352
|
+
stackTrace: errorObj.stack || '',
|
|
353
|
+
traceId: this.isSampled ? this.traceId : undefined,
|
|
354
|
+
context: {
|
|
355
|
+
type,
|
|
356
|
+
...getBrowserContext(),
|
|
357
|
+
breadcrumbs: [...this.breadcrumbs] // Snapshot of actions leading up to crash
|
|
358
|
+
},
|
|
359
|
+
timestamp: new Date().toISOString()
|
|
360
|
+
});
|
|
361
|
+
this.flush(); // Flush immediately on error
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
window.addEventListener('error', (event) => {
|
|
365
|
+
if (event.error) handleGlobalError(event.error, 'Uncaught Exception');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
369
|
+
handleGlobalError(event.reason instanceof Error ? event.reason : new Error(String(event.reason)), 'Unhandled Promise Rejection');
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// --- 5. Lifecycle & Beaconing ---
|
|
374
|
+
private setupRoutingListeners() {
|
|
375
|
+
const originalPushState = history.pushState;
|
|
376
|
+
history.pushState = (...args) => {
|
|
377
|
+
this.flush(); // Flush previous page view
|
|
378
|
+
originalPushState.apply(history, args);
|
|
379
|
+
this.startNewTrace(false);
|
|
380
|
+
this.addBreadcrumb('navigation', window.location.pathname);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
window.addEventListener('popstate', () => {
|
|
384
|
+
this.flush();
|
|
385
|
+
this.startNewTrace(false);
|
|
386
|
+
this.addBreadcrumb('navigation', window.location.pathname);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
document.addEventListener('visibilitychange', () => {
|
|
390
|
+
if (document.visibilityState === 'hidden') this.flush();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
window.addEventListener('pagehide', () => this.flush());
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private flush() {
|
|
397
|
+
if (this.spans.length === 0 && this.errors.length === 0 && !this.isInitialLoad) return;
|
|
398
|
+
|
|
399
|
+
const payload: any = { traces: [], errors: this.errors };
|
|
400
|
+
|
|
401
|
+
if (this.isSampled) {
|
|
402
|
+
payload.traces.push({
|
|
403
|
+
traceId: this.traceId,
|
|
404
|
+
sessionId: this.sessionId,
|
|
405
|
+
traceType: this.isInitialLoad ? 'initial_load' : 'route_change',
|
|
406
|
+
path: window.location.pathname,
|
|
407
|
+
referrer: document.referrer || '',
|
|
408
|
+
vitals: { ...this.vitals },
|
|
409
|
+
timings: this.isInitialLoad ? this.getNavigationTimings() : {},
|
|
410
|
+
frustration: { ...this.frustrations },
|
|
411
|
+
...getBrowserContext(), // URL, UserAgent
|
|
412
|
+
spans: [...this.spans],
|
|
413
|
+
duration: Date.now() - this.traceStartTime,
|
|
414
|
+
timestamp: new Date(this.traceStartTime).toISOString()
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Reset Buffers
|
|
419
|
+
this.spans = [];
|
|
420
|
+
this.errors = [];
|
|
421
|
+
this.frustrations = { rageClicks: 0, deadClicks: 0, errorCount: 0 };
|
|
422
|
+
this.isInitialLoad = false;
|
|
423
|
+
|
|
424
|
+
if (payload.traces.length > 0 || payload.errors.length > 0) {
|
|
425
|
+
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
|
|
426
|
+
|
|
427
|
+
// Safely append API Key to URL for Beacon support
|
|
428
|
+
const separator = this.endpoint.includes('?') ? '&' : '?';
|
|
429
|
+
const authUrl = `${this.endpoint}${separator}apiKey=${this.config.apiKey}`;
|
|
430
|
+
|
|
431
|
+
if (navigator.sendBeacon) {
|
|
432
|
+
navigator.sendBeacon(authUrl, blob);
|
|
433
|
+
} else {
|
|
434
|
+
fetch(authUrl, {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
body: blob,
|
|
437
|
+
keepalive: true,
|
|
438
|
+
headers: { 'x-service-api-key': this.config.apiKey }
|
|
439
|
+
}).catch(() => { });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Native UUID Generator (No external dependencies)
|
|
2
|
+
export function generateUUID(): string {
|
|
3
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
4
|
+
return crypto.randomUUID();
|
|
5
|
+
}
|
|
6
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
7
|
+
const r = (Math.random() * 16) | 0;
|
|
8
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
9
|
+
return v.toString(16);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// W3C Trace & Span ID Generators (Hex strings)
|
|
14
|
+
export function generateHex(length: number): string {
|
|
15
|
+
let result = '';
|
|
16
|
+
while (result.length < length) {
|
|
17
|
+
result += Math.random().toString(16).slice(2);
|
|
18
|
+
}
|
|
19
|
+
return result.slice(0, length);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Retrieves safe browser metadata for tracing context
|
|
23
|
+
export const getBrowserContext = () => {
|
|
24
|
+
return {
|
|
25
|
+
userAgent: navigator.userAgent,
|
|
26
|
+
url: window.location.href, // Provides the dynamic URL
|
|
27
|
+
deviceMemory: (navigator as any).deviceMemory || undefined,
|
|
28
|
+
connectionType: (navigator as any).connection?.effectiveType || undefined
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Safely attempts to parse payload sizes for rich APM metadata
|
|
33
|
+
export const getPayloadSize = (body: any): number | undefined => {
|
|
34
|
+
if (!body) return 0;
|
|
35
|
+
if (typeof body === 'string') return body.length;
|
|
36
|
+
if (body instanceof Blob || body instanceof File) return body.size;
|
|
37
|
+
if (body instanceof FormData) return undefined; // FormData size cannot be synchronously calculated
|
|
38
|
+
if (body instanceof ArrayBuffer) return body.byteLength;
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|