@agentuity/frontend 0.0.111 → 0.1.0

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 (81) hide show
  1. package/dist/analytics/beacon.d.ts +15 -0
  2. package/dist/analytics/beacon.d.ts.map +1 -0
  3. package/dist/analytics/beacon.js +177 -0
  4. package/dist/analytics/beacon.js.map +1 -0
  5. package/dist/analytics/collectors/clicks.d.ts +10 -0
  6. package/dist/analytics/collectors/clicks.d.ts.map +1 -0
  7. package/dist/analytics/collectors/clicks.js +84 -0
  8. package/dist/analytics/collectors/clicks.js.map +1 -0
  9. package/dist/analytics/collectors/errors.d.ts +5 -0
  10. package/dist/analytics/collectors/errors.d.ts.map +1 -0
  11. package/dist/analytics/collectors/errors.js +43 -0
  12. package/dist/analytics/collectors/errors.js.map +1 -0
  13. package/dist/analytics/collectors/forms.d.ts +5 -0
  14. package/dist/analytics/collectors/forms.d.ts.map +1 -0
  15. package/dist/analytics/collectors/forms.js +55 -0
  16. package/dist/analytics/collectors/forms.js.map +1 -0
  17. package/dist/analytics/collectors/pageview.d.ts +15 -0
  18. package/dist/analytics/collectors/pageview.d.ts.map +1 -0
  19. package/dist/analytics/collectors/pageview.js +64 -0
  20. package/dist/analytics/collectors/pageview.js.map +1 -0
  21. package/dist/analytics/collectors/scroll.d.ts +17 -0
  22. package/dist/analytics/collectors/scroll.d.ts.map +1 -0
  23. package/dist/analytics/collectors/scroll.js +93 -0
  24. package/dist/analytics/collectors/scroll.js.map +1 -0
  25. package/dist/analytics/collectors/spa.d.ts +10 -0
  26. package/dist/analytics/collectors/spa.d.ts.map +1 -0
  27. package/dist/analytics/collectors/spa.js +53 -0
  28. package/dist/analytics/collectors/spa.js.map +1 -0
  29. package/dist/analytics/collectors/visibility.d.ts +18 -0
  30. package/dist/analytics/collectors/visibility.d.ts.map +1 -0
  31. package/dist/analytics/collectors/visibility.js +81 -0
  32. package/dist/analytics/collectors/visibility.js.map +1 -0
  33. package/dist/analytics/collectors/webvitals.d.ts +6 -0
  34. package/dist/analytics/collectors/webvitals.d.ts.map +1 -0
  35. package/dist/analytics/collectors/webvitals.js +111 -0
  36. package/dist/analytics/collectors/webvitals.js.map +1 -0
  37. package/dist/analytics/events.d.ts +18 -0
  38. package/dist/analytics/events.d.ts.map +1 -0
  39. package/dist/analytics/events.js +126 -0
  40. package/dist/analytics/events.js.map +1 -0
  41. package/dist/analytics/index.d.ts +12 -0
  42. package/dist/analytics/index.d.ts.map +1 -0
  43. package/dist/analytics/index.js +12 -0
  44. package/dist/analytics/index.js.map +1 -0
  45. package/dist/analytics/offline.d.ts +19 -0
  46. package/dist/analytics/offline.d.ts.map +1 -0
  47. package/dist/analytics/offline.js +145 -0
  48. package/dist/analytics/offline.js.map +1 -0
  49. package/dist/analytics/types.d.ts +113 -0
  50. package/dist/analytics/types.d.ts.map +1 -0
  51. package/dist/analytics/types.js +2 -0
  52. package/dist/analytics/types.js.map +1 -0
  53. package/dist/analytics/utils/storage.d.ts +13 -0
  54. package/dist/analytics/utils/storage.d.ts.map +1 -0
  55. package/dist/analytics/utils/storage.js +63 -0
  56. package/dist/analytics/utils/storage.js.map +1 -0
  57. package/dist/analytics/utils/utm.d.ts +12 -0
  58. package/dist/analytics/utils/utm.d.ts.map +1 -0
  59. package/dist/analytics/utils/utm.js +27 -0
  60. package/dist/analytics/utils/utm.js.map +1 -0
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +2 -0
  64. package/dist/index.js.map +1 -1
  65. package/package.json +3 -3
  66. package/src/analytics/beacon.ts +203 -0
  67. package/src/analytics/collectors/clicks.ts +100 -0
  68. package/src/analytics/collectors/errors.ts +49 -0
  69. package/src/analytics/collectors/forms.ts +64 -0
  70. package/src/analytics/collectors/pageview.ts +76 -0
  71. package/src/analytics/collectors/scroll.ts +112 -0
  72. package/src/analytics/collectors/spa.ts +60 -0
  73. package/src/analytics/collectors/visibility.ts +94 -0
  74. package/src/analytics/collectors/webvitals.ts +129 -0
  75. package/src/analytics/events.ts +144 -0
  76. package/src/analytics/index.ts +21 -0
  77. package/src/analytics/offline.ts +163 -0
  78. package/src/analytics/types.ts +139 -0
  79. package/src/analytics/utils/storage.ts +64 -0
  80. package/src/analytics/utils/utm.ts +36 -0
  81. package/src/index.ts +18 -0
@@ -0,0 +1,100 @@
1
+ import { createBaseEvent } from './pageview';
2
+ import { queueEvent } from '../events';
3
+
4
+ /**
5
+ * Initialize click tracking
6
+ * Tracks clicks on elements with data-analytics attribute
7
+ */
8
+ export function initClickTracking(): void {
9
+ if (typeof document === 'undefined') {
10
+ return;
11
+ }
12
+
13
+ document.addEventListener(
14
+ 'click',
15
+ (e) => {
16
+ const target = e.target as HTMLElement | null;
17
+ if (!target) return;
18
+
19
+ // Find closest element with data-analytics attribute
20
+ const analyticsElement = target.closest('[data-analytics]');
21
+ if (!analyticsElement) return;
22
+
23
+ const eventName = analyticsElement.getAttribute('data-analytics');
24
+ if (!eventName) return;
25
+
26
+ const event = createBaseEvent('click');
27
+ event.event_name = eventName;
28
+
29
+ // Collect additional data attributes
30
+ const eventData: Record<string, unknown> = {};
31
+ for (const attr of Array.from(analyticsElement.attributes)) {
32
+ if (attr.name.startsWith('data-analytics-')) {
33
+ const key = attr.name.replace('data-analytics-', '');
34
+ eventData[key] = attr.value;
35
+ }
36
+ }
37
+
38
+ // Add element info
39
+ eventData.tag = analyticsElement.tagName.toLowerCase();
40
+ if (analyticsElement.id) {
41
+ eventData.id = analyticsElement.id;
42
+ }
43
+ const text = (analyticsElement as HTMLElement).innerText?.slice(0, 100);
44
+ if (text) {
45
+ eventData.text = text;
46
+ }
47
+
48
+ if (Object.keys(eventData).length > 0) {
49
+ event.event_data = eventData;
50
+ }
51
+
52
+ queueEvent(event);
53
+ },
54
+ { capture: true, passive: true }
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Initialize outbound link tracking
60
+ */
61
+ export function initOutboundLinkTracking(): void {
62
+ if (typeof document === 'undefined') {
63
+ return;
64
+ }
65
+
66
+ document.addEventListener(
67
+ 'click',
68
+ (e) => {
69
+ const target = e.target as HTMLElement | null;
70
+ if (!target) return;
71
+
72
+ const link = target.closest('a');
73
+ if (!link) return;
74
+
75
+ const href = link.href;
76
+ if (!href) return;
77
+
78
+ // Check if it's an outbound link
79
+ try {
80
+ const url = new URL(href, window.location.origin);
81
+ if (url.hostname === window.location.hostname) {
82
+ return; // Same domain, not outbound
83
+ }
84
+
85
+ const event = createBaseEvent('outbound_link');
86
+ event.event_name = 'outbound_link';
87
+ event.event_data = {
88
+ href,
89
+ hostname: url.hostname,
90
+ text: link.innerText?.slice(0, 100) || '',
91
+ };
92
+
93
+ queueEvent(event);
94
+ } catch {
95
+ // Invalid URL, ignore
96
+ }
97
+ },
98
+ { capture: true, passive: true }
99
+ );
100
+ }
@@ -0,0 +1,49 @@
1
+ import { createBaseEvent } from './pageview';
2
+ import { queueEvent } from '../events';
3
+
4
+ /**
5
+ * Initialize JavaScript error tracking
6
+ */
7
+ export function initErrorTracking(): void {
8
+ if (typeof window === 'undefined') {
9
+ return;
10
+ }
11
+
12
+ // Handle uncaught errors
13
+ window.addEventListener('error', (e) => {
14
+ const event = createBaseEvent('error');
15
+ event.event_name = 'js_error';
16
+ event.event_data = {
17
+ message: e.message || 'Unknown error',
18
+ filename: e.filename || '',
19
+ lineno: e.lineno || 0,
20
+ colno: e.colno || 0,
21
+ stack: e.error?.stack?.slice(0, 1000) || '',
22
+ };
23
+
24
+ queueEvent(event);
25
+ });
26
+
27
+ // Handle unhandled promise rejections
28
+ window.addEventListener('unhandledrejection', (e) => {
29
+ const event = createBaseEvent('error');
30
+ event.event_name = 'unhandled_rejection';
31
+
32
+ let message = 'Unhandled Promise Rejection';
33
+ let stack = '';
34
+
35
+ if (e.reason instanceof Error) {
36
+ message = e.reason.message;
37
+ stack = e.reason.stack?.slice(0, 1000) || '';
38
+ } else if (typeof e.reason === 'string') {
39
+ message = e.reason;
40
+ }
41
+
42
+ event.event_data = {
43
+ message,
44
+ stack,
45
+ };
46
+
47
+ queueEvent(event);
48
+ });
49
+ }
@@ -0,0 +1,64 @@
1
+ import { createBaseEvent } from './pageview';
2
+ import { queueEvent } from '../events';
3
+
4
+ /**
5
+ * Initialize form submission tracking
6
+ */
7
+ export function initFormTracking(): void {
8
+ if (typeof document === 'undefined') {
9
+ return;
10
+ }
11
+
12
+ document.addEventListener(
13
+ 'submit',
14
+ (e) => {
15
+ const form = e.target as HTMLFormElement | null;
16
+ if (!form || form.tagName !== 'FORM') {
17
+ return;
18
+ }
19
+
20
+ const event = createBaseEvent('form_submit');
21
+ event.event_name = 'form_submit';
22
+
23
+ const eventData: Record<string, unknown> = {};
24
+
25
+ // Form identification
26
+ if (form.id) {
27
+ eventData.form_id = form.id;
28
+ }
29
+ if (form.name) {
30
+ eventData.form_name = form.name;
31
+ }
32
+ if (form.action) {
33
+ eventData.form_action = form.action;
34
+ }
35
+ eventData.form_method = form.method || 'get';
36
+
37
+ // Count form fields (don't capture values for privacy)
38
+ const inputs = form.querySelectorAll('input, select, textarea');
39
+ eventData.field_count = inputs.length;
40
+
41
+ // Check for common form types
42
+ const hasEmail = form.querySelector('input[type="email"]') !== null;
43
+ const hasPassword = form.querySelector('input[type="password"]') !== null;
44
+ const hasSearch = form.querySelector('input[type="search"]') !== null;
45
+
46
+ if (hasEmail && hasPassword) {
47
+ eventData.form_type = 'auth';
48
+ } else if (hasEmail) {
49
+ eventData.form_type = 'email';
50
+ } else if (hasSearch) {
51
+ eventData.form_type = 'search';
52
+ } else if (hasPassword) {
53
+ eventData.form_type = 'password';
54
+ } else {
55
+ eventData.form_type = 'other';
56
+ }
57
+
58
+ event.event_data = eventData;
59
+
60
+ queueEvent(event);
61
+ },
62
+ { capture: true }
63
+ );
64
+ }
@@ -0,0 +1,76 @@
1
+ import type { AnalyticsEvent } from '../types';
2
+ import { queueEvent } from '../events';
3
+ import { getUTMParams } from '../utils/utm';
4
+
5
+ /**
6
+ * Create a base event with common properties
7
+ */
8
+ export function createBaseEvent(eventType: AnalyticsEvent['event_type']): AnalyticsEvent {
9
+ const utm = getUTMParams();
10
+
11
+ return {
12
+ id: crypto.randomUUID
13
+ ? crypto.randomUUID()
14
+ : `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
15
+ timestamp: Date.now(),
16
+ timezone_offset: new Date().getTimezoneOffset(),
17
+
18
+ event_type: eventType,
19
+
20
+ url: window.location.href,
21
+ path: window.location.pathname,
22
+ referrer: document.referrer || '',
23
+ title: document.title || '',
24
+
25
+ screen_width: window.screen?.width || 0,
26
+ screen_height: window.screen?.height || 0,
27
+ viewport_width: window.innerWidth || 0,
28
+ viewport_height: window.innerHeight || 0,
29
+ device_pixel_ratio: window.devicePixelRatio || 1,
30
+ user_agent: navigator.userAgent || '',
31
+ language: navigator.language || '',
32
+
33
+ ...utm,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Track a pageview event
39
+ */
40
+ export function trackPageview(customPath?: string): void {
41
+ const event = createBaseEvent('pageview');
42
+
43
+ if (customPath) {
44
+ event.path = customPath;
45
+ event.url = window.location.origin + customPath;
46
+ }
47
+
48
+ // Add performance timing if available
49
+ if (typeof performance !== 'undefined') {
50
+ const timing = performance.getEntriesByType('navigation')[0] as
51
+ | PerformanceNavigationTiming
52
+ | undefined;
53
+ if (timing) {
54
+ event.load_time = Math.round(timing.loadEventEnd - timing.startTime);
55
+ event.dom_ready = Math.round(timing.domContentLoadedEventEnd - timing.startTime);
56
+ event.ttfb = Math.round(timing.responseStart - timing.requestStart);
57
+ }
58
+ }
59
+
60
+ queueEvent(event);
61
+ }
62
+
63
+ /**
64
+ * Initialize pageview tracking
65
+ * Tracks initial pageview when called
66
+ */
67
+ export function initPageviewTracking(): void {
68
+ // Track initial pageview after DOM is ready
69
+ if (document.readyState === 'complete') {
70
+ trackPageview();
71
+ } else {
72
+ window.addEventListener('load', () => {
73
+ trackPageview();
74
+ });
75
+ }
76
+ }
@@ -0,0 +1,112 @@
1
+ import { createBaseEvent } from './pageview';
2
+ import { queueEvent } from '../events';
3
+
4
+ const SCROLL_MILESTONES = [25, 50, 75, 100];
5
+ let trackedMilestones: Set<number> = new Set();
6
+ let maxScrollDepth = 0;
7
+ let isScrollTrackingInitialized = false;
8
+ let scrollHandler: (() => void) | null = null;
9
+
10
+ /**
11
+ * Calculate current scroll depth percentage
12
+ */
13
+ function getScrollDepth(): number {
14
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
15
+ return 0;
16
+ }
17
+
18
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
19
+ const scrollHeight =
20
+ document.documentElement.scrollHeight - document.documentElement.clientHeight;
21
+
22
+ if (scrollHeight <= 0) {
23
+ return 100; // Page doesn't scroll
24
+ }
25
+
26
+ return Math.min(100, Math.round((scrollTop / scrollHeight) * 100));
27
+ }
28
+
29
+ /**
30
+ * Handle scroll event
31
+ */
32
+ function handleScroll(): void {
33
+ const depth = getScrollDepth();
34
+
35
+ if (depth > maxScrollDepth) {
36
+ maxScrollDepth = depth;
37
+ }
38
+
39
+ // Check for milestone crossings
40
+ for (const milestone of SCROLL_MILESTONES) {
41
+ if (depth >= milestone && !trackedMilestones.has(milestone)) {
42
+ trackedMilestones.add(milestone);
43
+
44
+ const event = createBaseEvent('scroll');
45
+ event.event_name = `scroll_${milestone}`;
46
+ event.scroll_depth = milestone;
47
+
48
+ queueEvent(event);
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Initialize scroll depth tracking
55
+ */
56
+ export function initScrollTracking(): void {
57
+ if (typeof window === 'undefined') {
58
+ return;
59
+ }
60
+
61
+ if (isScrollTrackingInitialized) {
62
+ return;
63
+ }
64
+ isScrollTrackingInitialized = true;
65
+
66
+ // Reset on page load
67
+ trackedMilestones = new Set();
68
+ maxScrollDepth = 0;
69
+
70
+ // Throttled scroll handler
71
+ let ticking = false;
72
+ scrollHandler = () => {
73
+ if (!ticking) {
74
+ requestAnimationFrame(() => {
75
+ handleScroll();
76
+ ticking = false;
77
+ });
78
+ ticking = true;
79
+ }
80
+ };
81
+
82
+ window.addEventListener('scroll', scrollHandler, { passive: true });
83
+
84
+ // Check initial scroll position (for pages that load scrolled)
85
+ handleScroll();
86
+ }
87
+
88
+ /**
89
+ * Remove scroll tracking listener
90
+ */
91
+ export function removeScrollTracking(): void {
92
+ if (scrollHandler) {
93
+ window.removeEventListener('scroll', scrollHandler);
94
+ scrollHandler = null;
95
+ }
96
+ isScrollTrackingInitialized = false;
97
+ }
98
+
99
+ /**
100
+ * Get max scroll depth (for time on page events)
101
+ */
102
+ export function getMaxScrollDepth(): number {
103
+ return maxScrollDepth;
104
+ }
105
+
106
+ /**
107
+ * Reset tracked milestones (for SPA navigation)
108
+ */
109
+ export function resetScrollTracking(): void {
110
+ trackedMilestones = new Set();
111
+ maxScrollDepth = 0;
112
+ }
@@ -0,0 +1,60 @@
1
+ import { trackPageview } from './pageview';
2
+
3
+ let currentPath = '';
4
+ let originalPushState: typeof history.pushState | null = null;
5
+ let originalReplaceState: typeof history.replaceState | null = null;
6
+
7
+ /**
8
+ * Handle URL change for SPA navigation
9
+ */
10
+ function handleUrlChange(): void {
11
+ const newPath = window.location.pathname;
12
+ if (newPath !== currentPath) {
13
+ currentPath = newPath;
14
+ trackPageview(newPath);
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Initialize SPA navigation tracking
20
+ * Hooks into history.pushState, history.replaceState, and popstate event
21
+ */
22
+ export function initSPATracking(): void {
23
+ if (typeof window === 'undefined' || typeof history === 'undefined') {
24
+ return;
25
+ }
26
+
27
+ currentPath = window.location.pathname;
28
+
29
+ // Hook into history.pushState
30
+ originalPushState = history.pushState.bind(history);
31
+ history.pushState = function (...args) {
32
+ originalPushState?.apply(this, args);
33
+ handleUrlChange();
34
+ };
35
+
36
+ // Hook into history.replaceState
37
+ originalReplaceState = history.replaceState.bind(history);
38
+ history.replaceState = function (...args) {
39
+ originalReplaceState?.apply(this, args);
40
+ handleUrlChange();
41
+ };
42
+
43
+ // Listen for popstate (back/forward navigation)
44
+ window.addEventListener('popstate', handleUrlChange);
45
+ }
46
+
47
+ /**
48
+ * Cleanup SPA tracking (for testing)
49
+ */
50
+ export function cleanupSPATracking(): void {
51
+ if (originalPushState) {
52
+ history.pushState = originalPushState;
53
+ originalPushState = null;
54
+ }
55
+ if (originalReplaceState) {
56
+ history.replaceState = originalReplaceState;
57
+ originalReplaceState = null;
58
+ }
59
+ window.removeEventListener('popstate', handleUrlChange);
60
+ }
@@ -0,0 +1,94 @@
1
+ import { createBaseEvent } from './pageview';
2
+ import { queueEvent } from '../events';
3
+ import { getMaxScrollDepth } from './scroll';
4
+
5
+ let pageEntryTime = 0;
6
+ let hiddenTime = 0;
7
+ let lastHiddenTimestamp = 0;
8
+ let visibilityTrackingInitialized = false;
9
+ let visibilityChangeHandler: (() => void) | null = null;
10
+
11
+ /**
12
+ * Get total time spent on page (excluding hidden time)
13
+ */
14
+ function getTimeOnPage(): number {
15
+ if (pageEntryTime === 0) {
16
+ return 0;
17
+ }
18
+
19
+ const totalTime = Date.now() - pageEntryTime;
20
+ return Math.max(0, totalTime - hiddenTime);
21
+ }
22
+
23
+ /**
24
+ * Initialize visibility tracking
25
+ * Tracks when user leaves/returns to the page
26
+ */
27
+ export function initVisibilityTracking(): void {
28
+ if (typeof document === 'undefined') {
29
+ return;
30
+ }
31
+
32
+ if (visibilityTrackingInitialized) {
33
+ return;
34
+ }
35
+ visibilityTrackingInitialized = true;
36
+
37
+ pageEntryTime = Date.now();
38
+ hiddenTime = 0;
39
+ lastHiddenTimestamp = 0;
40
+
41
+ visibilityChangeHandler = () => {
42
+ if (document.visibilityState === 'hidden') {
43
+ lastHiddenTimestamp = Date.now();
44
+
45
+ // Track page leave with engagement metrics
46
+ const event = createBaseEvent('visibility');
47
+ event.event_name = 'page_hidden';
48
+ event.time_on_page = getTimeOnPage();
49
+ event.scroll_depth = getMaxScrollDepth();
50
+
51
+ queueEvent(event);
52
+ } else if (document.visibilityState === 'visible') {
53
+ // Calculate hidden duration
54
+ if (lastHiddenTimestamp > 0) {
55
+ hiddenTime += Date.now() - lastHiddenTimestamp;
56
+ lastHiddenTimestamp = 0;
57
+ }
58
+
59
+ const event = createBaseEvent('visibility');
60
+ event.event_name = 'page_visible';
61
+
62
+ queueEvent(event);
63
+ }
64
+ };
65
+
66
+ document.addEventListener('visibilitychange', visibilityChangeHandler);
67
+ }
68
+
69
+ /**
70
+ * Remove visibility tracking listener
71
+ */
72
+ export function removeVisibilityTracking(): void {
73
+ if (visibilityChangeHandler) {
74
+ document.removeEventListener('visibilitychange', visibilityChangeHandler);
75
+ visibilityChangeHandler = null;
76
+ }
77
+ visibilityTrackingInitialized = false;
78
+ }
79
+
80
+ /**
81
+ * Reset visibility tracking (for SPA navigation)
82
+ */
83
+ export function resetVisibilityTracking(): void {
84
+ pageEntryTime = Date.now();
85
+ hiddenTime = 0;
86
+ lastHiddenTimestamp = 0;
87
+ }
88
+
89
+ /**
90
+ * Get current time on page
91
+ */
92
+ export function getCurrentTimeOnPage(): number {
93
+ return getTimeOnPage();
94
+ }
@@ -0,0 +1,129 @@
1
+ import { createBaseEvent } from './pageview';
2
+ import { queueEvent, flushEvents } from '../events';
3
+
4
+ /**
5
+ * Initialize Core Web Vitals tracking
6
+ * Uses PerformanceObserver to track LCP, FCP, CLS, INP
7
+ */
8
+ export function initWebVitalsTracking(): void {
9
+ if (typeof window === 'undefined' || typeof PerformanceObserver === 'undefined') {
10
+ return;
11
+ }
12
+
13
+ // Track First Contentful Paint (FCP)
14
+ try {
15
+ const fcpObserver = new PerformanceObserver((list) => {
16
+ for (const entry of list.getEntries()) {
17
+ if (entry.name === 'first-contentful-paint') {
18
+ const event = createBaseEvent('web_vital');
19
+ event.event_name = 'fcp';
20
+ event.fcp = Math.round(entry.startTime);
21
+ queueEvent(event);
22
+ flushEvents();
23
+ fcpObserver.disconnect();
24
+ }
25
+ }
26
+ });
27
+ fcpObserver.observe({ type: 'paint', buffered: true });
28
+ } catch {
29
+ // PerformanceObserver not supported for this entry type
30
+ }
31
+
32
+ // Track Largest Contentful Paint (LCP)
33
+ try {
34
+ let lcpValue = 0;
35
+ const lcpObserver = new PerformanceObserver((list) => {
36
+ const entries = list.getEntries();
37
+ const lastEntry = entries[entries.length - 1];
38
+ if (lastEntry) {
39
+ lcpValue = lastEntry.startTime;
40
+ }
41
+ });
42
+ lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
43
+
44
+ // Report LCP when page becomes hidden
45
+ document.addEventListener(
46
+ 'visibilitychange',
47
+ () => {
48
+ if (document.visibilityState === 'hidden' && lcpValue > 0) {
49
+ const event = createBaseEvent('web_vital');
50
+ event.event_name = 'lcp';
51
+ event.lcp = Math.round(lcpValue);
52
+ queueEvent(event);
53
+ flushEvents();
54
+ lcpObserver.disconnect();
55
+ }
56
+ },
57
+ { once: true }
58
+ );
59
+ } catch {
60
+ // PerformanceObserver not supported for this entry type
61
+ }
62
+
63
+ // Track Cumulative Layout Shift (CLS)
64
+ try {
65
+ let clsValue = 0;
66
+ const clsObserver = new PerformanceObserver((list) => {
67
+ for (const entry of list.getEntries()) {
68
+ const layoutShift = entry as PerformanceEntry & {
69
+ hadRecentInput?: boolean;
70
+ value?: number;
71
+ };
72
+ if (!layoutShift.hadRecentInput && layoutShift.value) {
73
+ clsValue += layoutShift.value;
74
+ }
75
+ }
76
+ });
77
+ clsObserver.observe({ type: 'layout-shift', buffered: true });
78
+
79
+ // Report CLS when page becomes hidden
80
+ document.addEventListener(
81
+ 'visibilitychange',
82
+ () => {
83
+ if (document.visibilityState === 'hidden') {
84
+ const event = createBaseEvent('web_vital');
85
+ event.event_name = 'cls';
86
+ event.cls = Math.round(clsValue * 1000) / 1000; // Round to 3 decimal places
87
+ queueEvent(event);
88
+ flushEvents();
89
+ clsObserver.disconnect();
90
+ }
91
+ },
92
+ { once: true }
93
+ );
94
+ } catch {
95
+ // PerformanceObserver not supported for this entry type
96
+ }
97
+
98
+ // Track Interaction to Next Paint (INP)
99
+ try {
100
+ let inpValue = 0;
101
+ const inpObserver = new PerformanceObserver((list) => {
102
+ for (const entry of list.getEntries()) {
103
+ const eventEntry = entry as PerformanceEntry & { duration?: number };
104
+ if (eventEntry.duration && eventEntry.duration > inpValue) {
105
+ inpValue = eventEntry.duration;
106
+ }
107
+ }
108
+ });
109
+ inpObserver.observe({ type: 'event', buffered: true });
110
+
111
+ // Report INP when page becomes hidden
112
+ document.addEventListener(
113
+ 'visibilitychange',
114
+ () => {
115
+ if (document.visibilityState === 'hidden' && inpValue > 0) {
116
+ const event = createBaseEvent('web_vital');
117
+ event.event_name = 'inp';
118
+ event.inp = Math.round(inpValue);
119
+ queueEvent(event);
120
+ flushEvents();
121
+ inpObserver.disconnect();
122
+ }
123
+ },
124
+ { once: true }
125
+ );
126
+ } catch {
127
+ // PerformanceObserver not supported for this entry type
128
+ }
129
+ }