@agentuity/frontend 0.1.2 → 0.1.4

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 (80) hide show
  1. package/dist/analytics/beacon-standalone.d.ts +106 -0
  2. package/dist/analytics/beacon-standalone.d.ts.map +1 -0
  3. package/dist/analytics/beacon-standalone.js +577 -0
  4. package/dist/analytics/beacon-standalone.js.map +1 -0
  5. package/dist/analytics/index.d.ts +15 -5
  6. package/dist/analytics/index.d.ts.map +1 -1
  7. package/dist/analytics/index.js +21 -5
  8. package/dist/analytics/index.js.map +1 -1
  9. package/dist/analytics/types.d.ts +63 -35
  10. package/dist/analytics/types.d.ts.map +1 -1
  11. package/dist/beacon-script.d.ts +16 -0
  12. package/dist/beacon-script.d.ts.map +1 -0
  13. package/dist/beacon-script.js +12 -0
  14. package/dist/beacon-script.js.map +1 -0
  15. package/dist/beacon.js +1 -0
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +5 -2
  19. package/dist/index.js.map +1 -1
  20. package/package.json +4 -4
  21. package/src/analytics/beacon-standalone.ts +718 -0
  22. package/src/analytics/index.ts +29 -8
  23. package/src/analytics/types.ts +78 -49
  24. package/src/beacon-script.ts +24 -0
  25. package/src/index.ts +10 -7
  26. package/dist/analytics/beacon.d.ts +0 -15
  27. package/dist/analytics/beacon.d.ts.map +0 -1
  28. package/dist/analytics/beacon.js +0 -177
  29. package/dist/analytics/beacon.js.map +0 -1
  30. package/dist/analytics/collectors/clicks.d.ts +0 -10
  31. package/dist/analytics/collectors/clicks.d.ts.map +0 -1
  32. package/dist/analytics/collectors/clicks.js +0 -84
  33. package/dist/analytics/collectors/clicks.js.map +0 -1
  34. package/dist/analytics/collectors/errors.d.ts +0 -5
  35. package/dist/analytics/collectors/errors.d.ts.map +0 -1
  36. package/dist/analytics/collectors/errors.js +0 -43
  37. package/dist/analytics/collectors/errors.js.map +0 -1
  38. package/dist/analytics/collectors/forms.d.ts +0 -5
  39. package/dist/analytics/collectors/forms.d.ts.map +0 -1
  40. package/dist/analytics/collectors/forms.js +0 -55
  41. package/dist/analytics/collectors/forms.js.map +0 -1
  42. package/dist/analytics/collectors/pageview.d.ts +0 -15
  43. package/dist/analytics/collectors/pageview.d.ts.map +0 -1
  44. package/dist/analytics/collectors/pageview.js +0 -64
  45. package/dist/analytics/collectors/pageview.js.map +0 -1
  46. package/dist/analytics/collectors/scroll.d.ts +0 -17
  47. package/dist/analytics/collectors/scroll.d.ts.map +0 -1
  48. package/dist/analytics/collectors/scroll.js +0 -93
  49. package/dist/analytics/collectors/scroll.js.map +0 -1
  50. package/dist/analytics/collectors/spa.d.ts +0 -10
  51. package/dist/analytics/collectors/spa.d.ts.map +0 -1
  52. package/dist/analytics/collectors/spa.js +0 -53
  53. package/dist/analytics/collectors/spa.js.map +0 -1
  54. package/dist/analytics/collectors/visibility.d.ts +0 -18
  55. package/dist/analytics/collectors/visibility.d.ts.map +0 -1
  56. package/dist/analytics/collectors/visibility.js +0 -81
  57. package/dist/analytics/collectors/visibility.js.map +0 -1
  58. package/dist/analytics/collectors/webvitals.d.ts +0 -6
  59. package/dist/analytics/collectors/webvitals.d.ts.map +0 -1
  60. package/dist/analytics/collectors/webvitals.js +0 -111
  61. package/dist/analytics/collectors/webvitals.js.map +0 -1
  62. package/dist/analytics/events.d.ts +0 -18
  63. package/dist/analytics/events.d.ts.map +0 -1
  64. package/dist/analytics/events.js +0 -126
  65. package/dist/analytics/events.js.map +0 -1
  66. package/dist/analytics/offline.d.ts +0 -19
  67. package/dist/analytics/offline.d.ts.map +0 -1
  68. package/dist/analytics/offline.js +0 -145
  69. package/dist/analytics/offline.js.map +0 -1
  70. package/src/analytics/beacon.ts +0 -203
  71. package/src/analytics/collectors/clicks.ts +0 -100
  72. package/src/analytics/collectors/errors.ts +0 -49
  73. package/src/analytics/collectors/forms.ts +0 -64
  74. package/src/analytics/collectors/pageview.ts +0 -76
  75. package/src/analytics/collectors/scroll.ts +0 -112
  76. package/src/analytics/collectors/spa.ts +0 -60
  77. package/src/analytics/collectors/visibility.ts +0 -94
  78. package/src/analytics/collectors/webvitals.ts +0 -129
  79. package/src/analytics/events.ts +0 -146
  80. package/src/analytics/offline.ts +0 -163
@@ -0,0 +1,718 @@
1
+ /**
2
+ * Standalone beacon script - this file is bundled and minified
3
+ * to create the production analytics.js served at /_agentuity/webanalytics/analytics.js
4
+ *
5
+ * This is the single source of truth for the beacon logic.
6
+ */
7
+
8
+ interface ScrollEvent {
9
+ depth: number;
10
+ timestamp: number;
11
+ }
12
+
13
+ interface AnalyticsCustomEvent {
14
+ timestamp: number;
15
+ name: string;
16
+ data: string;
17
+ }
18
+
19
+ interface GeoLocation {
20
+ country?: string;
21
+ country_latitude?: string | number;
22
+ country_longitude?: string | number;
23
+ region?: string;
24
+ region_latitude?: string | number;
25
+ region_longitude?: string | number;
26
+ city?: string;
27
+ city_latitude?: string | number;
28
+ city_longitude?: string | number;
29
+ timezone?: string;
30
+ latitude?: string | number;
31
+ longitude?: string | number;
32
+ }
33
+
34
+ interface PageViewData {
35
+ id: string;
36
+ timestamp: number;
37
+ timezone_offset: number;
38
+ url: string;
39
+ path: string;
40
+ referrer: string;
41
+ title: string;
42
+ screen_width: number;
43
+ screen_height: number;
44
+ viewport_width: number;
45
+ viewport_height: number;
46
+ device_pixel_ratio: number;
47
+ user_agent: string;
48
+ language: string;
49
+ scroll_depth: number;
50
+ time_on_page: number;
51
+ scroll_events: ScrollEvent[];
52
+ custom_events: AnalyticsCustomEvent[];
53
+ load_time?: number;
54
+ dom_ready?: number;
55
+ ttfb?: number;
56
+ fcp?: number;
57
+ lcp?: number;
58
+ cls?: number;
59
+ inp?: number;
60
+ country?: string;
61
+ country_latitude?: number;
62
+ country_longitude?: number;
63
+ region?: string;
64
+ region_latitude?: number;
65
+ region_longitude?: number;
66
+ city?: string;
67
+ city_latitude?: number;
68
+ city_longitude?: number;
69
+ timezone?: string;
70
+ latitude?: number;
71
+ longitude?: number;
72
+ utm_source?: string;
73
+ utm_medium?: string;
74
+ utm_campaign?: string;
75
+ utm_term?: string;
76
+ utm_content?: string;
77
+ [key: string]: unknown;
78
+ }
79
+
80
+ interface AnalyticsConfig {
81
+ enabled: boolean;
82
+ orgId: string;
83
+ projectId: string;
84
+ isDevmode: boolean;
85
+ trackClicks?: boolean;
86
+ trackScroll?: boolean;
87
+ trackWebVitals?: boolean;
88
+ trackErrors?: boolean;
89
+ trackSPANavigation?: boolean;
90
+ sampleRate?: number;
91
+ }
92
+
93
+ interface SessionData {
94
+ threadId?: string;
95
+ }
96
+
97
+ interface AgentuityWindow {
98
+ __AGENTUITY_ANALYTICS__?: AnalyticsConfig;
99
+ __AGENTUITY_SESSION__?: SessionData;
100
+ agentuityAnalytics?: {
101
+ track: (name: string, properties?: Record<string, unknown>) => void;
102
+ identify: (userId: string, traits?: Record<string, unknown>) => void;
103
+ flush: () => void;
104
+ };
105
+ }
106
+
107
+ const COLLECT_ENDPOINT = '/_agentuity/webanalytics/collect';
108
+ const MAX_CUSTOM_EVENTS = 1000;
109
+
110
+ /**
111
+ * Safely stringify an object, handling circular references and other errors.
112
+ * Returns the JSON string on success, or a fallback string on failure.
113
+ */
114
+ function safeStringify(obj: unknown): string {
115
+ if (obj === undefined || obj === null) {
116
+ return '';
117
+ }
118
+ try {
119
+ const seen = new WeakSet();
120
+ return JSON.stringify(obj, (_key, value) => {
121
+ if (typeof value === 'object' && value !== null) {
122
+ if (seen.has(value)) {
123
+ return '[Circular]';
124
+ }
125
+ seen.add(value);
126
+ }
127
+ return value;
128
+ });
129
+ } catch (err) {
130
+ const message = err instanceof Error ? err.message : String(err);
131
+ console.warn('[Agentuity Analytics] Failed to stringify properties:', message);
132
+ return `[unserializable: ${message}]`;
133
+ }
134
+ }
135
+
136
+ (function () {
137
+ const w = window as Window & AgentuityWindow;
138
+ const d = document;
139
+ const configRaw = w.__AGENTUITY_ANALYTICS__;
140
+
141
+ if (!configRaw || !configRaw.enabled) return;
142
+
143
+ // Prevent duplicate initialization (e.g., from HMR)
144
+ const initFlag = w as unknown as { __AGENTUITY_BEACON_INIT__?: boolean };
145
+ if (configRaw.isDevmode) {
146
+ console.debug(
147
+ '[Agentuity Analytics] Script loaded, init flag:',
148
+ initFlag.__AGENTUITY_BEACON_INIT__,
149
+ 'path:',
150
+ location.pathname
151
+ );
152
+ }
153
+ if (initFlag.__AGENTUITY_BEACON_INIT__) {
154
+ if (configRaw.isDevmode) {
155
+ console.debug('[Agentuity Analytics] Already initialized, skipping');
156
+ }
157
+ return;
158
+ }
159
+ initFlag.__AGENTUITY_BEACON_INIT__ = true;
160
+
161
+ // Store in a non-nullable variable after the guard
162
+ const c: AnalyticsConfig = configRaw;
163
+
164
+ let geo: GeoLocation | null = null;
165
+ let sent = false;
166
+ let pageStart = Date.now();
167
+ let userId = '';
168
+ let userTraits: Record<string, string> = {};
169
+
170
+ const pv: PageViewData = {
171
+ id: '',
172
+ timestamp: 0,
173
+ timezone_offset: 0,
174
+ url: '',
175
+ path: '',
176
+ referrer: '',
177
+ title: '',
178
+ screen_width: 0,
179
+ screen_height: 0,
180
+ viewport_width: 0,
181
+ viewport_height: 0,
182
+ device_pixel_ratio: 1,
183
+ user_agent: '',
184
+ language: '',
185
+ scroll_depth: 0,
186
+ time_on_page: 0,
187
+ scroll_events: [],
188
+ custom_events: [],
189
+ };
190
+
191
+ function generateId(): string {
192
+ return crypto.randomUUID
193
+ ? crypto.randomUUID()
194
+ : `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
195
+ }
196
+
197
+ function getUTMParams(): Record<string, string> {
198
+ const params = new URLSearchParams(location.search);
199
+ const utm: Record<string, string> = {};
200
+ ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].forEach((key) => {
201
+ const value = params.get(key);
202
+ if (value) utm[key] = value;
203
+ });
204
+ return utm;
205
+ }
206
+
207
+ // Strip query string from URL to prevent sensitive data leakage
208
+ function stripQueryString(url: string): string {
209
+ if (!url) return '';
210
+ try {
211
+ const parsed = new URL(url);
212
+ return parsed.origin + parsed.pathname;
213
+ } catch {
214
+ // If URL parsing fails, try simple string split
215
+ return url.split('?')[0];
216
+ }
217
+ }
218
+
219
+ // Full init - called on page load and SPA navigation
220
+ function init(): void {
221
+ pv.id = generateId();
222
+ pv.timestamp = Date.now();
223
+ pv.timezone_offset = new Date().getTimezoneOffset();
224
+ pv.url = stripQueryString(location.href);
225
+ pv.path = location.pathname;
226
+ pv.referrer = stripQueryString(d.referrer);
227
+ pv.title = d.title || '';
228
+ pv.screen_width = screen.width || 0;
229
+ pv.screen_height = screen.height || 0;
230
+ pv.viewport_width = innerWidth || 0;
231
+ pv.viewport_height = innerHeight || 0;
232
+ pv.device_pixel_ratio = devicePixelRatio || 1;
233
+ pv.user_agent = navigator.userAgent || '';
234
+ pv.language = navigator.language || '';
235
+
236
+ const utm = getUTMParams();
237
+ for (const k in utm) {
238
+ pv[k] = utm[k];
239
+ }
240
+
241
+ pv.scroll_events = [];
242
+ pv.custom_events = [];
243
+ pv.scroll_depth = 0;
244
+ pv.fcp = 0;
245
+ pv.lcp = 0;
246
+ pv.cls = 0;
247
+ pv.inp = 0;
248
+ sent = false;
249
+ pageStart = Date.now();
250
+
251
+ if (typeof performance !== 'undefined' && performance.getEntriesByType) {
252
+ const nav = performance.getEntriesByType('navigation')[0] as
253
+ | PerformanceNavigationTiming
254
+ | undefined;
255
+ if (nav) {
256
+ pv.load_time = Math.round(nav.loadEventEnd - nav.startTime);
257
+ pv.dom_ready = Math.round(nav.domContentLoadedEventEnd - nav.startTime);
258
+ pv.ttfb = Math.round(nav.responseStart - nav.requestStart);
259
+ }
260
+ }
261
+
262
+ if (c.isDevmode) {
263
+ console.debug('[Agentuity Analytics] Session started (full init):', pv.id);
264
+ }
265
+ }
266
+
267
+ // Soft reset - called when user returns to page (keeps page-level metrics)
268
+ function resetSession(): void {
269
+ pv.id = generateId();
270
+ pv.timestamp = Date.now();
271
+ pv.scroll_events = [];
272
+ pv.custom_events = [];
273
+ pv.scroll_depth = 0;
274
+ pv.time_on_page = 0;
275
+ sent = false;
276
+ pageStart = Date.now();
277
+
278
+ if (c.isDevmode) {
279
+ console.debug('[Agentuity Analytics] Session started (soft reset):', pv.id);
280
+ }
281
+ }
282
+
283
+ // Fetch geo data
284
+ fetch('https://agentuity.sh/location')
285
+ .then((r) => r.json())
286
+ .then((g: GeoLocation) => {
287
+ geo = g;
288
+ try {
289
+ sessionStorage.setItem('agentuity_geo', JSON.stringify(g));
290
+ } catch {
291
+ // Ignore
292
+ }
293
+ })
294
+ .catch(() => {
295
+ try {
296
+ const cached = sessionStorage.getItem('agentuity_geo');
297
+ if (cached) geo = JSON.parse(cached);
298
+ } catch {
299
+ // Ignore
300
+ }
301
+ });
302
+
303
+ // Try to load cached geo immediately
304
+ try {
305
+ const cached = sessionStorage.getItem('agentuity_geo');
306
+ if (cached) geo = JSON.parse(cached);
307
+ } catch {
308
+ // Ignore
309
+ }
310
+
311
+ function getSession(): SessionData | undefined {
312
+ return w.__AGENTUITY_SESSION__;
313
+ }
314
+
315
+ function send(force = false): void {
316
+ if (sent && !force) {
317
+ if (c.isDevmode) {
318
+ console.debug('[Agentuity Analytics] send() skipped - already sent');
319
+ }
320
+ return;
321
+ }
322
+ if (c.sampleRate !== undefined && c.sampleRate < 1 && Math.random() > c.sampleRate) return;
323
+
324
+ sent = true;
325
+ pv.time_on_page = Date.now() - pageStart;
326
+
327
+ if (geo) {
328
+ pv.country = geo.country || '';
329
+ if (geo.country_latitude) pv.country_latitude = parseFloat(String(geo.country_latitude));
330
+ if (geo.country_longitude)
331
+ pv.country_longitude = parseFloat(String(geo.country_longitude));
332
+ pv.region = geo.region || '';
333
+ if (geo.region_latitude) pv.region_latitude = parseFloat(String(geo.region_latitude));
334
+ if (geo.region_longitude) pv.region_longitude = parseFloat(String(geo.region_longitude));
335
+ pv.city = geo.city || '';
336
+ if (geo.city_latitude) pv.city_latitude = parseFloat(String(geo.city_latitude));
337
+ if (geo.city_longitude) pv.city_longitude = parseFloat(String(geo.city_longitude));
338
+ pv.timezone = geo.timezone || '';
339
+ if (geo.latitude) pv.latitude = parseFloat(String(geo.latitude));
340
+ if (geo.longitude) pv.longitude = parseFloat(String(geo.longitude));
341
+ }
342
+
343
+ if (pv.cls) {
344
+ pv.cls = Math.round(pv.cls * 1000) / 1000;
345
+ }
346
+
347
+ const s = getSession();
348
+ const vid = localStorage.getItem('agentuity_visitor_id') || 'vid_' + generateId();
349
+ try {
350
+ localStorage.setItem('agentuity_visitor_id', vid);
351
+ } catch {
352
+ // Ignore
353
+ }
354
+
355
+ const payload = {
356
+ org_id: c.orgId,
357
+ project_id: c.projectId,
358
+ thread_id: s?.threadId || '',
359
+ visitor_id: vid,
360
+ user_id: userId,
361
+ user_traits: userTraits,
362
+ is_devmode: c.isDevmode,
363
+ pageview: pv,
364
+ };
365
+
366
+ // Clear pending data since we're sending now
367
+ try {
368
+ sessionStorage.removeItem('agentuity_pending_pageview');
369
+ } catch {
370
+ // Storage may be unavailable
371
+ }
372
+
373
+ if (c.isDevmode) {
374
+ console.debug('[Agentuity Analytics]', JSON.stringify(payload, null, 2));
375
+ return;
376
+ }
377
+
378
+ const body = JSON.stringify(payload);
379
+ if (navigator.sendBeacon) {
380
+ navigator.sendBeacon(COLLECT_ENDPOINT, body);
381
+ } else {
382
+ fetch(COLLECT_ENDPOINT, {
383
+ method: 'POST',
384
+ body,
385
+ keepalive: true,
386
+ }).catch(() => {
387
+ // Silent failure
388
+ });
389
+ }
390
+ }
391
+
392
+ // Send on page hide, reset session on page visible
393
+ d.addEventListener('visibilitychange', () => {
394
+ if (c.isDevmode) {
395
+ console.debug('[Agentuity Analytics] visibilitychange:', d.visibilityState, 'sent:', sent);
396
+ }
397
+ if (d.visibilityState === 'hidden') {
398
+ send();
399
+ } else if (d.visibilityState === 'visible') {
400
+ // User returned to the page - start a new attention session
401
+ // Keep page-level metrics (url, geo, vitals) but reset session-level metrics
402
+ resetSession();
403
+ }
404
+ });
405
+ w.addEventListener('pagehide', () => {
406
+ if (c.isDevmode) {
407
+ console.debug('[Agentuity Analytics] pagehide event');
408
+ try {
409
+ sessionStorage.setItem('agentuity_last_event', `pagehide:${Date.now()}:${pv.path}`);
410
+ } catch {
411
+ // Storage may be unavailable
412
+ }
413
+ }
414
+ send();
415
+ });
416
+
417
+ // Catch hard navigations (URL change, link click to new page, refresh)
418
+ w.addEventListener('beforeunload', () => {
419
+ if (c.isDevmode) {
420
+ console.debug('[Agentuity Analytics] beforeunload event');
421
+ try {
422
+ sessionStorage.setItem('agentuity_last_event', `beforeunload:${Date.now()}:${pv.path}`);
423
+ } catch {
424
+ // Storage may be unavailable
425
+ }
426
+ }
427
+ send();
428
+ });
429
+
430
+ // In devmode, check if previous page sent data (helps verify unload events work)
431
+ if (c.isDevmode) {
432
+ try {
433
+ const lastEvent = sessionStorage.getItem('agentuity_last_event');
434
+ if (lastEvent) {
435
+ console.debug('[Agentuity Analytics] Previous page event:', lastEvent);
436
+ sessionStorage.removeItem('agentuity_last_event');
437
+ }
438
+ } catch {
439
+ // Storage may be unavailable
440
+ }
441
+ }
442
+
443
+ // Fallback: check for unsent data from previous page (in case unload events didn't fire)
444
+ try {
445
+ const pendingData = sessionStorage.getItem('agentuity_pending_pageview');
446
+ if (pendingData) {
447
+ sessionStorage.removeItem('agentuity_pending_pageview');
448
+ const pending = JSON.parse(pendingData);
449
+ // Only send if it's from a different page
450
+ if (pending.pageview?.path !== location.pathname) {
451
+ if (c.isDevmode) {
452
+ console.debug(
453
+ '[Agentuity Analytics] Sending unsent data from previous page:',
454
+ pending.pageview?.path
455
+ );
456
+ console.debug('[Agentuity Analytics]', JSON.stringify(pending, null, 2));
457
+ } else {
458
+ const body = JSON.stringify(pending);
459
+ if (navigator.sendBeacon) {
460
+ navigator.sendBeacon(COLLECT_ENDPOINT, body);
461
+ }
462
+ }
463
+ }
464
+ }
465
+ } catch {
466
+ // Storage or JSON parsing may fail
467
+ }
468
+
469
+ // Store current pageview data periodically so it can be recovered if unload events don't fire
470
+ function savePendingData(): void {
471
+ try {
472
+ pv.time_on_page = Date.now() - pageStart;
473
+ const s = getSession();
474
+ const vid = localStorage.getItem('agentuity_visitor_id') || 'vid_' + generateId();
475
+ const payload = {
476
+ org_id: c.orgId,
477
+ project_id: c.projectId,
478
+ thread_id: s?.threadId || '',
479
+ visitor_id: vid,
480
+ user_id: userId,
481
+ user_traits: userTraits,
482
+ is_devmode: c.isDevmode,
483
+ pageview: { ...pv },
484
+ };
485
+ sessionStorage.setItem('agentuity_pending_pageview', JSON.stringify(payload));
486
+ } catch {
487
+ // Storage may be unavailable
488
+ }
489
+ }
490
+
491
+ // Save pending data every 2 seconds
492
+ setInterval(savePendingData, 2000);
493
+ // Also save on any interaction
494
+ d.addEventListener('click', savePendingData, { passive: true });
495
+ d.addEventListener('scroll', savePendingData, { passive: true, once: true });
496
+
497
+ if (c.isDevmode) {
498
+ console.debug('[Agentuity Analytics] Beacon initialized, visibility:', d.visibilityState);
499
+ }
500
+
501
+ // Scroll tracking
502
+ if (c.trackScroll !== false) {
503
+ const scrolled = new Set<number>();
504
+
505
+ function getScrollDepth(): number {
506
+ const st = w.scrollY || d.documentElement.scrollTop;
507
+ const sh = d.documentElement.scrollHeight - d.documentElement.clientHeight;
508
+ return sh <= 0 ? 100 : Math.min(100, Math.round((st / sh) * 100));
509
+ }
510
+
511
+ w.addEventListener(
512
+ 'scroll',
513
+ () => {
514
+ const dp = getScrollDepth();
515
+ if (dp > pv.scroll_depth) pv.scroll_depth = dp;
516
+
517
+ [25, 50, 75, 100].forEach((m) => {
518
+ if (dp >= m && !scrolled.has(m)) {
519
+ scrolled.add(m);
520
+ pv.scroll_events.push({
521
+ depth: m,
522
+ timestamp: Date.now() - pageStart,
523
+ });
524
+ }
525
+ });
526
+ },
527
+ { passive: true }
528
+ );
529
+ }
530
+
531
+ // Web Vitals tracking
532
+ if (c.trackWebVitals !== false && typeof PerformanceObserver !== 'undefined') {
533
+ // FCP
534
+ try {
535
+ const fcpObs = new PerformanceObserver((list) => {
536
+ list.getEntries().forEach((entry) => {
537
+ if (entry.name === 'first-contentful-paint') {
538
+ pv.fcp = Math.round(entry.startTime);
539
+ fcpObs.disconnect();
540
+ }
541
+ });
542
+ });
543
+ fcpObs.observe({ type: 'paint', buffered: true });
544
+ } catch {
545
+ // Not supported
546
+ }
547
+
548
+ // LCP
549
+ try {
550
+ new PerformanceObserver((list) => {
551
+ const entries = list.getEntries();
552
+ if (entries.length) {
553
+ pv.lcp = Math.round(entries[entries.length - 1].startTime);
554
+ }
555
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
556
+ } catch {
557
+ // Not supported
558
+ }
559
+
560
+ // CLS
561
+ try {
562
+ new PerformanceObserver((list) => {
563
+ list.getEntries().forEach((entry) => {
564
+ const layoutShift = entry as PerformanceEntry & {
565
+ hadRecentInput?: boolean;
566
+ value?: number;
567
+ };
568
+ if (!layoutShift.hadRecentInput && layoutShift.value) {
569
+ pv.cls = (pv.cls || 0) + layoutShift.value;
570
+ }
571
+ });
572
+ }).observe({ type: 'layout-shift', buffered: true });
573
+ } catch {
574
+ // Not supported
575
+ }
576
+
577
+ // INP
578
+ try {
579
+ new PerformanceObserver((list) => {
580
+ list.getEntries().forEach((entry) => {
581
+ const eventEntry = entry as PerformanceEntry & { duration?: number };
582
+ if (eventEntry.duration && eventEntry.duration > (pv.inp || 0)) {
583
+ pv.inp = Math.round(eventEntry.duration);
584
+ }
585
+ });
586
+ }).observe({ type: 'event', buffered: true });
587
+ } catch {
588
+ // Not supported
589
+ }
590
+ }
591
+
592
+ // SPA navigation tracking
593
+ if (c.trackSPANavigation !== false) {
594
+ const origPush = history.pushState;
595
+ const origReplace = history.replaceState;
596
+ let currentPath = location.pathname + location.search;
597
+ let lastHref = location.href;
598
+
599
+ if (c.isDevmode) {
600
+ console.debug('[Agentuity Analytics] SPA tracking enabled, initial path:', currentPath);
601
+ }
602
+
603
+ function handleNav(): void {
604
+ const newPath = location.pathname + location.search;
605
+ if (newPath !== currentPath) {
606
+ if (c.isDevmode) {
607
+ console.debug('[Agentuity Analytics] SPA navigation:', currentPath, '->', newPath);
608
+ }
609
+ send(true); // Force send on SPA navigation
610
+ currentPath = newPath;
611
+ lastHref = location.href;
612
+ init();
613
+ }
614
+ }
615
+
616
+ history.pushState = function (...args) {
617
+ origPush.apply(this, args);
618
+ setTimeout(handleNav, 0);
619
+ };
620
+
621
+ history.replaceState = function (...args) {
622
+ origReplace.apply(this, args);
623
+ setTimeout(handleNav, 0);
624
+ };
625
+
626
+ w.addEventListener('popstate', handleNav);
627
+
628
+ // Fallback: poll for URL changes in case router bypasses history API
629
+ setInterval(() => {
630
+ if (location.href !== lastHref) {
631
+ lastHref = location.href;
632
+ handleNav();
633
+ }
634
+ }, 200);
635
+ }
636
+
637
+ // Click tracking
638
+ if (c.trackClicks !== false) {
639
+ d.addEventListener(
640
+ 'click',
641
+ (e) => {
642
+ const target = e.target as Element | null;
643
+ if (!target) return;
644
+
645
+ const analyticsEl = target.closest('[data-analytics]');
646
+ if (!analyticsEl) return;
647
+
648
+ if (pv.custom_events.length < MAX_CUSTOM_EVENTS) {
649
+ pv.custom_events.push({
650
+ timestamp: Date.now(),
651
+ name: 'click:' + analyticsEl.getAttribute('data-analytics'),
652
+ data: '',
653
+ });
654
+ }
655
+ },
656
+ true
657
+ );
658
+ }
659
+
660
+ // Error tracking
661
+ if (c.trackErrors !== false) {
662
+ w.addEventListener('error', (e) => {
663
+ if (pv.custom_events.length < MAX_CUSTOM_EVENTS) {
664
+ pv.custom_events.push({
665
+ timestamp: Date.now(),
666
+ name: 'error:js_error',
667
+ data: JSON.stringify({
668
+ message: e.message || 'Unknown',
669
+ filename: e.filename || '',
670
+ lineno: e.lineno || 0,
671
+ }),
672
+ });
673
+ }
674
+ });
675
+
676
+ w.addEventListener('unhandledrejection', (e) => {
677
+ if (pv.custom_events.length < MAX_CUSTOM_EVENTS) {
678
+ pv.custom_events.push({
679
+ timestamp: Date.now(),
680
+ name: 'error:unhandled_rejection',
681
+ data: JSON.stringify({
682
+ message: e.reason instanceof Error ? e.reason.message : String(e.reason),
683
+ }),
684
+ });
685
+ }
686
+ });
687
+ }
688
+
689
+ // Initialize on load
690
+ if (d.readyState === 'complete') {
691
+ init();
692
+ } else {
693
+ w.addEventListener('load', init);
694
+ }
695
+
696
+ // Public API
697
+ w.agentuityAnalytics = {
698
+ track(name: string, properties?: Record<string, unknown>): void {
699
+ if (pv.custom_events.length < MAX_CUSTOM_EVENTS) {
700
+ pv.custom_events.push({
701
+ timestamp: Date.now(),
702
+ name,
703
+ data: safeStringify(properties),
704
+ });
705
+ }
706
+ },
707
+ identify(id: string, traits?: Record<string, unknown>): void {
708
+ userId = id;
709
+ if (traits) {
710
+ userTraits = {};
711
+ for (const [key, value] of Object.entries(traits)) {
712
+ userTraits[key] = String(value);
713
+ }
714
+ }
715
+ },
716
+ flush: () => send(true),
717
+ };
718
+ })();