@arthurreira/analytics 0.20.0 → 0.22.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.
package/dist/client.d.ts CHANGED
@@ -2,16 +2,20 @@ interface AnalyticsProps {
2
2
  apiKey: string;
3
3
  apiUrl: string;
4
4
  wsUrl?: string;
5
+ routeName?: string;
5
6
  }
6
- declare function Analytics({ apiKey, apiUrl, wsUrl }: AnalyticsProps): null;
7
+ declare function Analytics({ apiKey, apiUrl, wsUrl, routeName }: AnalyticsProps): null;
7
8
 
8
9
  interface ExceptionFields {
9
10
  exception_type: string;
10
11
  exception_message: string;
11
12
  stack_trace: string | null;
13
+ exception_filename?: string | null;
14
+ exception_lineno?: number | null;
15
+ exception_colno?: number | null;
12
16
  }
13
17
 
14
- declare function useAnalytics(apiUrl: string, apiKey: string, currentPath: string, wsUrl?: string): {
18
+ declare function useAnalytics(apiUrl: string, apiKey: string, currentPath: string, wsUrl?: string, routeName?: string): {
15
19
  trackPageview: (path: string) => void;
16
20
  trackClick: (e: MouseEvent, element: HTMLElement) => void;
17
21
  trackScroll: (depth: number) => void;
@@ -20,6 +24,11 @@ declare function useAnalytics(apiUrl: string, apiKey: string, currentPath: strin
20
24
  trackError: (error: Error) => void;
21
25
  trackCTA: (ctaId: string, ctaVariant?: string) => void;
22
26
  trackSearch: (query: string) => void;
27
+ trackWebVital: (metric: {
28
+ name: string;
29
+ value: number;
30
+ rating: string;
31
+ }) => void;
23
32
  };
24
33
 
25
34
  export { Analytics, useAnalytics };
package/dist/client.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // src/components/Analytics.tsx
5
5
  import { useEffect as useEffect2, useRef as useRef2 } from "react";
6
6
  import { usePathname } from "next/navigation";
7
+ import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
7
8
 
8
9
  // src/hooks/useAnalytics.ts
9
10
  import { useEffect, useRef } from "react";
@@ -17,12 +18,60 @@ function isOptedOut() {
17
18
  }
18
19
 
19
20
  // src/lib/api.ts
20
- var BASE_FIELDS = (sessionId, eventType, path) => ({
21
+ var BASE_FIELDS = (sessionId, eventType, path, eventName, eventCategory) => ({
21
22
  session_id: sessionId,
22
23
  event_type: eventType,
24
+ event_name: eventName ?? null,
25
+ event_category: eventCategory ?? null,
23
26
  path,
24
- page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : ""
27
+ page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : "",
28
+ // Page context
29
+ page_title: typeof document !== "undefined" ? document.title || null : null,
30
+ referrer: typeof document !== "undefined" ? document.referrer || null : null,
31
+ // Viewport
32
+ viewport_width: typeof window !== "undefined" ? window.innerWidth : null,
33
+ viewport_height: typeof window !== "undefined" ? window.innerHeight : null,
34
+ // Timing — how long since page loaded
35
+ time_on_page_ms: typeof performance !== "undefined" ? Math.round(performance.now()) : null,
36
+ // User engagement state at moment of event
37
+ document_visibility: typeof document !== "undefined" ? document.visibilityState : null
25
38
  });
39
+ function getConnectionType() {
40
+ if (typeof navigator === "undefined") return null;
41
+ const nav = navigator;
42
+ return nav.connection?.effectiveType ?? null;
43
+ }
44
+ function resolveInteractiveTarget(el) {
45
+ const interactive = el.closest(
46
+ 'a, button, [role="button"], input, select, textarea, label, summary'
47
+ );
48
+ return interactive ?? el;
49
+ }
50
+ function resolveElementHref(element) {
51
+ if (element.tagName.toLowerCase() === "a") {
52
+ return element.href || null;
53
+ }
54
+ const anchor = element.closest("a");
55
+ return anchor ? anchor.href || null : null;
56
+ }
57
+ function safeClassName(el) {
58
+ const cn = el.className;
59
+ return typeof cn === "string" ? cn || null : null;
60
+ }
61
+ function cleanLabel(raw, maxLen) {
62
+ if (!raw) return null;
63
+ const cleaned = raw.replace(/\s+/g, " ").trim().slice(0, maxLen);
64
+ return cleaned || null;
65
+ }
66
+ function isExternalHref(href) {
67
+ if (!href) return null;
68
+ if (typeof window === "undefined") return null;
69
+ try {
70
+ return new URL(href).origin !== window.location.origin;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
26
75
  function getGpu() {
27
76
  try {
28
77
  const canvas = document.createElement("canvas");
@@ -61,7 +110,6 @@ async function createSession(apiUrl, apiKey, visitorId) {
61
110
  sessionData.utm_term = params.get("utm_term");
62
111
  sessionData.utm_content = params.get("utm_content");
63
112
  }
64
- console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
65
113
  try {
66
114
  const response = await fetch(`${apiUrl}/sessions`, {
67
115
  method: "POST",
@@ -71,17 +119,10 @@ async function createSession(apiUrl, apiKey, visitorId) {
71
119
  },
72
120
  body: JSON.stringify(sessionData)
73
121
  });
74
- console.log(`[AF Analytics SDK] Session creation response: ${response.status} ${response.statusText}`);
75
- if (!response.ok) {
76
- const errText = await response.text();
77
- console.error(`[AF Analytics SDK] Session creation failed: ${errText}`);
78
- return null;
79
- }
122
+ if (!response.ok) return null;
80
123
  const data = await response.json();
81
- console.log(`[AF Analytics SDK] Session created: ${data.id}`);
82
124
  return data.id ?? null;
83
- } catch (err) {
84
- console.error(`[AF Analytics SDK] Session creation error: ${err}`);
125
+ } catch {
85
126
  return null;
86
127
  }
87
128
  }
@@ -96,45 +137,73 @@ async function sendEvent(apiUrl, apiKey, payload) {
96
137
  body: JSON.stringify(payload)
97
138
  });
98
139
  }
99
- async function trackPageview(apiUrl, apiKey, sessionId, path) {
140
+ async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
141
+ const navigationEntry = typeof performance !== "undefined" ? performance.getEntriesByType("navigation")[0] : void 0;
142
+ const title = typeof document !== "undefined" ? document.title || null : null;
143
+ const eventName = title || path;
100
144
  await sendEvent(apiUrl, apiKey, {
101
- ...BASE_FIELDS(sessionId, "pageview", path),
102
- page_title: typeof document !== "undefined" ? document.title || null : null,
103
- referrer: typeof document !== "undefined" ? document.referrer || null : null,
104
- scroll_depth: 0
145
+ ...BASE_FIELDS(sessionId, "pageview", path, eventName, "navigation"),
146
+ scroll_depth: 0,
147
+ route_name: routeName ?? null,
148
+ navigation_type: navigationEntry?.type ?? null,
149
+ connection_type: getConnectionType()
105
150
  });
106
151
  }
107
152
  async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
153
+ const target = resolveInteractiveTarget(element);
154
+ const href = resolveElementHref(target);
155
+ const ariaLabel = target.getAttribute("aria-label");
156
+ const rawText = target.innerText ?? target.textContent ?? void 0;
157
+ const eventName = cleanLabel(ariaLabel, 60) ?? cleanLabel(rawText, 60) ?? target.tagName.toLowerCase();
108
158
  await sendEvent(apiUrl, apiKey, {
109
- ...BASE_FIELDS(sessionId, "click", path),
159
+ ...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
110
160
  x_position: e.clientX,
111
161
  y_position: e.clientY,
112
- element_id: element.id || null,
113
- element_class: element.className || null,
114
- element_text: element.innerText?.slice(0, 100) || null
162
+ element_id: target.id || null,
163
+ element_class: safeClassName(target),
164
+ element_text: cleanLabel(rawText, 100),
165
+ tag_name: target.tagName.toLowerCase(),
166
+ element_href: href,
167
+ element_role: target.getAttribute("role") || null,
168
+ element_aria_label: ariaLabel || null,
169
+ element_type: target.type || null,
170
+ element_name: target.name || null,
171
+ is_external_link: isExternalHref(href),
172
+ modifier_keys: {
173
+ alt: e.altKey,
174
+ ctrl: e.ctrlKey,
175
+ meta: e.metaKey,
176
+ shift: e.shiftKey
177
+ },
178
+ click_count: e.detail
115
179
  });
116
180
  }
117
181
  async function trackScroll(apiUrl, apiKey, sessionId, path, depth) {
118
182
  await sendEvent(apiUrl, apiKey, {
119
- ...BASE_FIELDS(sessionId, "scroll", path),
120
- scroll_depth: depth
183
+ ...BASE_FIELDS(sessionId, "scroll", path, `Scroll ${depth}%`, "interaction"),
184
+ scroll_depth: depth,
185
+ scroll_y_px: typeof window !== "undefined" ? window.scrollY : null,
186
+ document_height_px: typeof document !== "undefined" ? document.documentElement.scrollHeight : null
121
187
  });
122
188
  }
123
189
  async function trackCopy(apiUrl, apiKey, sessionId, path) {
190
+ const selection = typeof window !== "undefined" ? window.getSelection()?.toString() ?? null : null;
124
191
  await sendEvent(apiUrl, apiKey, {
125
- ...BASE_FIELDS(sessionId, "copy", path),
126
- copied_text: typeof window !== "undefined" ? window.getSelection()?.toString().slice(0, 200) || null : null
192
+ ...BASE_FIELDS(sessionId, "copy", path, "Copy", "interaction"),
193
+ copied_text: selection?.slice(0, 200) || null,
194
+ selection_length: selection?.length ?? 0
127
195
  });
128
196
  }
129
197
  async function trackSearch(apiUrl, apiKey, sessionId, path, query) {
130
198
  await sendEvent(apiUrl, apiKey, {
131
- ...BASE_FIELDS(sessionId, "search", path),
199
+ ...BASE_FIELDS(sessionId, "search", path, `Search: ${query.slice(0, 60)}`, "interaction"),
132
200
  search_query: query
133
201
  });
134
202
  }
135
203
  async function trackException(apiUrl, apiKey, sessionId, path, fields) {
204
+ const eventName = fields.exception_type ? `${fields.exception_type}: ${fields.exception_message.slice(0, 60)}` : fields.exception_message.slice(0, 80);
136
205
  await sendEvent(apiUrl, apiKey, {
137
- ...BASE_FIELDS(sessionId, "js_exception", path),
206
+ ...BASE_FIELDS(sessionId, "js_exception", path, eventName, "error"),
138
207
  ...fields
139
208
  });
140
209
  }
@@ -146,12 +215,29 @@ async function trackError(apiUrl, apiKey, sessionId, path, error) {
146
215
  });
147
216
  }
148
217
  async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
218
+ const eventName = ctaVariant ? `${ctaId} (${ctaVariant})` : ctaId;
149
219
  await sendEvent(apiUrl, apiKey, {
150
- ...BASE_FIELDS(sessionId, "cta_click", path),
220
+ ...BASE_FIELDS(sessionId, "cta_click", path, eventName, "conversion"),
151
221
  cta_id: ctaId,
152
222
  cta_variant: ctaVariant || null
153
223
  });
154
224
  }
225
+ var WEB_VITAL_FIELDS = {
226
+ lcp_ms: "lcp_ms",
227
+ cls_score: "cls_score",
228
+ inp_ms: "inp_ms",
229
+ ttfb_ms: "ttfb_ms",
230
+ fcp_ms: "fcp_ms"
231
+ };
232
+ async function trackWebVital(apiUrl, apiKey, sessionId, path, metric) {
233
+ const field = WEB_VITAL_FIELDS[metric.name];
234
+ if (!field) return;
235
+ await sendEvent(apiUrl, apiKey, {
236
+ ...BASE_FIELDS(sessionId, "web_vital", path, metric.name.toUpperCase(), "performance"),
237
+ [field]: metric.value,
238
+ rating: metric.rating
239
+ });
240
+ }
155
241
 
156
242
  // src/lib/presence.ts
157
243
  function connectPresence(apiKey, sessionId, wsUrl) {
@@ -306,7 +392,7 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
306
392
  });
307
393
  return _sessionFlight;
308
394
  }
309
- function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
395
+ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl, routeName) {
310
396
  const sessionId = useRef(null);
311
397
  const pendingEvents = useRef([]);
312
398
  const hasSentEnd = useRef(false);
@@ -383,7 +469,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
383
469
  if (wsUrl) {
384
470
  presenceRef.current = connectPresence(apiKey, id, wsUrl);
385
471
  }
386
- trackPageview(apiUrl, apiKey, id, currentPathRef.current);
472
+ trackPageview(apiUrl, apiKey, id, currentPathRef.current, routeName ?? null);
387
473
  });
388
474
  };
389
475
  const handleStorage = (e) => {
@@ -399,7 +485,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
399
485
  if (hasSentEnd.current || !sessionId.current) {
400
486
  restartSession();
401
487
  } else {
402
- trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current);
488
+ trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current, routeName ?? null);
403
489
  }
404
490
  };
405
491
  let visHideTimer = null;
@@ -437,7 +523,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
437
523
  }
438
524
  return {
439
525
  trackPageview: (path) => {
440
- enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
526
+ enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path, routeName ?? null));
441
527
  },
442
528
  trackClick: (e, element) => {
443
529
  enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
@@ -459,22 +545,40 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
459
545
  },
460
546
  trackSearch: (query) => {
461
547
  enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
548
+ },
549
+ trackWebVital: (metric) => {
550
+ enqueueOrRun(() => trackWebVital(apiUrl, apiKey, sessionId.current, currentPath, metric));
462
551
  }
463
552
  };
464
553
  }
465
554
 
466
555
  // src/components/Analytics.tsx
467
- function Analytics({ apiKey, apiUrl, wsUrl }) {
556
+ function Analytics({ apiKey, apiUrl, wsUrl, routeName }) {
468
557
  const pathname = usePathname();
469
- const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl);
558
+ const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2, trackWebVital: trackWebVital2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl, routeName);
470
559
  const lastTracked = useRef2(null);
471
560
  const lastScrollDepth = useRef2(0);
561
+ const trackWebVitalRef = useRef2(trackWebVital2);
562
+ useEffect2(() => {
563
+ trackWebVitalRef.current = trackWebVital2;
564
+ });
472
565
  useEffect2(() => {
473
566
  if (lastTracked.current === pathname) return;
474
567
  lastTracked.current = pathname;
475
568
  lastScrollDepth.current = 0;
476
- trackPageview2(pathname);
569
+ setTimeout(() => trackPageview2(pathname), 0);
477
570
  }, [pathname, trackPageview2]);
571
+ useEffect2(() => {
572
+ if (typeof window === "undefined") return;
573
+ const report = (name, value, rating) => {
574
+ trackWebVitalRef.current({ name, value, rating });
575
+ };
576
+ onLCP((m) => report("lcp_ms", Math.round(m.value), m.rating));
577
+ onCLS((m) => report("cls_score", m.value, m.rating));
578
+ onINP((m) => report("inp_ms", Math.round(m.value), m.rating));
579
+ onTTFB((m) => report("ttfb_ms", Math.round(m.value), m.rating));
580
+ onFCP((m) => report("fcp_ms", Math.round(m.value), m.rating));
581
+ }, []);
478
582
  useEffect2(() => {
479
583
  if (typeof window === "undefined") return;
480
584
  const handler = (e) => trackClick2(e, e.target);
@@ -508,7 +612,10 @@ function Analytics({ apiKey, apiUrl, wsUrl }) {
508
612
  const handler = (e) => trackException2({
509
613
  exception_type: e.error?.name ?? "Error",
510
614
  exception_message: e.message,
511
- stack_trace: e.error?.stack ?? null
615
+ stack_trace: e.error?.stack ?? null,
616
+ exception_filename: e.filename || null,
617
+ exception_lineno: e.lineno || null,
618
+ exception_colno: e.colno || null
512
619
  });
513
620
  window.addEventListener("error", handler);
514
621
  return () => window.removeEventListener("error", handler);
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ declare function isOptedOut(): boolean;
8
8
  declare function consent(enabled: boolean): void;
9
9
 
10
10
  declare function createSession(apiUrl: string, apiKey: string, visitorId?: string): Promise<string | null>;
11
- declare function trackPageview(apiUrl: string, apiKey: string, sessionId: string, path: string): Promise<void>;
11
+ declare function trackPageview(apiUrl: string, apiKey: string, sessionId: string, path: string, routeName?: string | null): Promise<void>;
12
12
  declare function trackClick(apiUrl: string, apiKey: string, sessionId: string, path: string, e: MouseEvent, element: HTMLElement): Promise<void>;
13
13
  declare function trackScroll(apiUrl: string, apiKey: string, sessionId: string, path: string, depth: number): Promise<void>;
14
14
  declare function trackCopy(apiUrl: string, apiKey: string, sessionId: string, path: string): Promise<void>;
@@ -17,14 +17,22 @@ interface ExceptionFields {
17
17
  exception_type: string;
18
18
  exception_message: string;
19
19
  stack_trace: string | null;
20
+ exception_filename?: string | null;
21
+ exception_lineno?: number | null;
22
+ exception_colno?: number | null;
20
23
  }
21
24
  declare function trackException(apiUrl: string, apiKey: string, sessionId: string, path: string, fields: ExceptionFields): Promise<void>;
22
25
  declare function trackError(apiUrl: string, apiKey: string, sessionId: string, path: string, error: Error): Promise<void>;
23
26
  declare function trackCTA(apiUrl: string, apiKey: string, sessionId: string, path: string, ctaId: string, ctaVariant?: string): Promise<void>;
27
+ declare function trackWebVital(apiUrl: string, apiKey: string, sessionId: string, path: string, metric: {
28
+ name: string;
29
+ value: number;
30
+ rating: string;
31
+ }): Promise<void>;
24
32
 
25
33
  interface PresenceConnection {
26
34
  disconnect: () => void;
27
35
  }
28
36
  declare function connectPresence(apiKey: string, sessionId: string, wsUrl: string): PresenceConnection;
29
37
 
30
- export { type ExceptionFields, type PresenceConnection, connectPresence, consent, createSession, isOptedOut, optIn, optOut, trackCTA, trackClick, trackCopy, trackError, trackException, trackPageview, trackScroll, trackSearch };
38
+ export { type ExceptionFields, type PresenceConnection, connectPresence, consent, createSession, isOptedOut, optIn, optOut, trackCTA, trackClick, trackCopy, trackError, trackException, trackPageview, trackScroll, trackSearch, trackWebVital };
package/dist/index.js CHANGED
@@ -19,12 +19,60 @@ function consent(enabled) {
19
19
  }
20
20
 
21
21
  // src/lib/api.ts
22
- var BASE_FIELDS = (sessionId, eventType, path) => ({
22
+ var BASE_FIELDS = (sessionId, eventType, path, eventName, eventCategory) => ({
23
23
  session_id: sessionId,
24
24
  event_type: eventType,
25
+ event_name: eventName ?? null,
26
+ event_category: eventCategory ?? null,
25
27
  path,
26
- page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : ""
28
+ page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : "",
29
+ // Page context
30
+ page_title: typeof document !== "undefined" ? document.title || null : null,
31
+ referrer: typeof document !== "undefined" ? document.referrer || null : null,
32
+ // Viewport
33
+ viewport_width: typeof window !== "undefined" ? window.innerWidth : null,
34
+ viewport_height: typeof window !== "undefined" ? window.innerHeight : null,
35
+ // Timing — how long since page loaded
36
+ time_on_page_ms: typeof performance !== "undefined" ? Math.round(performance.now()) : null,
37
+ // User engagement state at moment of event
38
+ document_visibility: typeof document !== "undefined" ? document.visibilityState : null
27
39
  });
40
+ function getConnectionType() {
41
+ if (typeof navigator === "undefined") return null;
42
+ const nav = navigator;
43
+ return nav.connection?.effectiveType ?? null;
44
+ }
45
+ function resolveInteractiveTarget(el) {
46
+ const interactive = el.closest(
47
+ 'a, button, [role="button"], input, select, textarea, label, summary'
48
+ );
49
+ return interactive ?? el;
50
+ }
51
+ function resolveElementHref(element) {
52
+ if (element.tagName.toLowerCase() === "a") {
53
+ return element.href || null;
54
+ }
55
+ const anchor = element.closest("a");
56
+ return anchor ? anchor.href || null : null;
57
+ }
58
+ function safeClassName(el) {
59
+ const cn = el.className;
60
+ return typeof cn === "string" ? cn || null : null;
61
+ }
62
+ function cleanLabel(raw, maxLen) {
63
+ if (!raw) return null;
64
+ const cleaned = raw.replace(/\s+/g, " ").trim().slice(0, maxLen);
65
+ return cleaned || null;
66
+ }
67
+ function isExternalHref(href) {
68
+ if (!href) return null;
69
+ if (typeof window === "undefined") return null;
70
+ try {
71
+ return new URL(href).origin !== window.location.origin;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
28
76
  function getGpu() {
29
77
  try {
30
78
  const canvas = document.createElement("canvas");
@@ -63,7 +111,6 @@ async function createSession(apiUrl, apiKey, visitorId) {
63
111
  sessionData.utm_term = params.get("utm_term");
64
112
  sessionData.utm_content = params.get("utm_content");
65
113
  }
66
- console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
67
114
  try {
68
115
  const response = await fetch(`${apiUrl}/sessions`, {
69
116
  method: "POST",
@@ -73,17 +120,10 @@ async function createSession(apiUrl, apiKey, visitorId) {
73
120
  },
74
121
  body: JSON.stringify(sessionData)
75
122
  });
76
- console.log(`[AF Analytics SDK] Session creation response: ${response.status} ${response.statusText}`);
77
- if (!response.ok) {
78
- const errText = await response.text();
79
- console.error(`[AF Analytics SDK] Session creation failed: ${errText}`);
80
- return null;
81
- }
123
+ if (!response.ok) return null;
82
124
  const data = await response.json();
83
- console.log(`[AF Analytics SDK] Session created: ${data.id}`);
84
125
  return data.id ?? null;
85
- } catch (err) {
86
- console.error(`[AF Analytics SDK] Session creation error: ${err}`);
126
+ } catch {
87
127
  return null;
88
128
  }
89
129
  }
@@ -98,45 +138,73 @@ async function sendEvent(apiUrl, apiKey, payload) {
98
138
  body: JSON.stringify(payload)
99
139
  });
100
140
  }
101
- async function trackPageview(apiUrl, apiKey, sessionId, path) {
141
+ async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
142
+ const navigationEntry = typeof performance !== "undefined" ? performance.getEntriesByType("navigation")[0] : void 0;
143
+ const title = typeof document !== "undefined" ? document.title || null : null;
144
+ const eventName = title || path;
102
145
  await sendEvent(apiUrl, apiKey, {
103
- ...BASE_FIELDS(sessionId, "pageview", path),
104
- page_title: typeof document !== "undefined" ? document.title || null : null,
105
- referrer: typeof document !== "undefined" ? document.referrer || null : null,
106
- scroll_depth: 0
146
+ ...BASE_FIELDS(sessionId, "pageview", path, eventName, "navigation"),
147
+ scroll_depth: 0,
148
+ route_name: routeName ?? null,
149
+ navigation_type: navigationEntry?.type ?? null,
150
+ connection_type: getConnectionType()
107
151
  });
108
152
  }
109
153
  async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
154
+ const target = resolveInteractiveTarget(element);
155
+ const href = resolveElementHref(target);
156
+ const ariaLabel = target.getAttribute("aria-label");
157
+ const rawText = target.innerText ?? target.textContent ?? void 0;
158
+ const eventName = cleanLabel(ariaLabel, 60) ?? cleanLabel(rawText, 60) ?? target.tagName.toLowerCase();
110
159
  await sendEvent(apiUrl, apiKey, {
111
- ...BASE_FIELDS(sessionId, "click", path),
160
+ ...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
112
161
  x_position: e.clientX,
113
162
  y_position: e.clientY,
114
- element_id: element.id || null,
115
- element_class: element.className || null,
116
- element_text: element.innerText?.slice(0, 100) || null
163
+ element_id: target.id || null,
164
+ element_class: safeClassName(target),
165
+ element_text: cleanLabel(rawText, 100),
166
+ tag_name: target.tagName.toLowerCase(),
167
+ element_href: href,
168
+ element_role: target.getAttribute("role") || null,
169
+ element_aria_label: ariaLabel || null,
170
+ element_type: target.type || null,
171
+ element_name: target.name || null,
172
+ is_external_link: isExternalHref(href),
173
+ modifier_keys: {
174
+ alt: e.altKey,
175
+ ctrl: e.ctrlKey,
176
+ meta: e.metaKey,
177
+ shift: e.shiftKey
178
+ },
179
+ click_count: e.detail
117
180
  });
118
181
  }
119
182
  async function trackScroll(apiUrl, apiKey, sessionId, path, depth) {
120
183
  await sendEvent(apiUrl, apiKey, {
121
- ...BASE_FIELDS(sessionId, "scroll", path),
122
- scroll_depth: depth
184
+ ...BASE_FIELDS(sessionId, "scroll", path, `Scroll ${depth}%`, "interaction"),
185
+ scroll_depth: depth,
186
+ scroll_y_px: typeof window !== "undefined" ? window.scrollY : null,
187
+ document_height_px: typeof document !== "undefined" ? document.documentElement.scrollHeight : null
123
188
  });
124
189
  }
125
190
  async function trackCopy(apiUrl, apiKey, sessionId, path) {
191
+ const selection = typeof window !== "undefined" ? window.getSelection()?.toString() ?? null : null;
126
192
  await sendEvent(apiUrl, apiKey, {
127
- ...BASE_FIELDS(sessionId, "copy", path),
128
- copied_text: typeof window !== "undefined" ? window.getSelection()?.toString().slice(0, 200) || null : null
193
+ ...BASE_FIELDS(sessionId, "copy", path, "Copy", "interaction"),
194
+ copied_text: selection?.slice(0, 200) || null,
195
+ selection_length: selection?.length ?? 0
129
196
  });
130
197
  }
131
198
  async function trackSearch(apiUrl, apiKey, sessionId, path, query) {
132
199
  await sendEvent(apiUrl, apiKey, {
133
- ...BASE_FIELDS(sessionId, "search", path),
200
+ ...BASE_FIELDS(sessionId, "search", path, `Search: ${query.slice(0, 60)}`, "interaction"),
134
201
  search_query: query
135
202
  });
136
203
  }
137
204
  async function trackException(apiUrl, apiKey, sessionId, path, fields) {
205
+ const eventName = fields.exception_type ? `${fields.exception_type}: ${fields.exception_message.slice(0, 60)}` : fields.exception_message.slice(0, 80);
138
206
  await sendEvent(apiUrl, apiKey, {
139
- ...BASE_FIELDS(sessionId, "js_exception", path),
207
+ ...BASE_FIELDS(sessionId, "js_exception", path, eventName, "error"),
140
208
  ...fields
141
209
  });
142
210
  }
@@ -148,12 +216,29 @@ async function trackError(apiUrl, apiKey, sessionId, path, error) {
148
216
  });
149
217
  }
150
218
  async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
219
+ const eventName = ctaVariant ? `${ctaId} (${ctaVariant})` : ctaId;
151
220
  await sendEvent(apiUrl, apiKey, {
152
- ...BASE_FIELDS(sessionId, "cta_click", path),
221
+ ...BASE_FIELDS(sessionId, "cta_click", path, eventName, "conversion"),
153
222
  cta_id: ctaId,
154
223
  cta_variant: ctaVariant || null
155
224
  });
156
225
  }
226
+ var WEB_VITAL_FIELDS = {
227
+ lcp_ms: "lcp_ms",
228
+ cls_score: "cls_score",
229
+ inp_ms: "inp_ms",
230
+ ttfb_ms: "ttfb_ms",
231
+ fcp_ms: "fcp_ms"
232
+ };
233
+ async function trackWebVital(apiUrl, apiKey, sessionId, path, metric) {
234
+ const field = WEB_VITAL_FIELDS[metric.name];
235
+ if (!field) return;
236
+ await sendEvent(apiUrl, apiKey, {
237
+ ...BASE_FIELDS(sessionId, "web_vital", path, metric.name.toUpperCase(), "performance"),
238
+ [field]: metric.value,
239
+ rating: metric.rating
240
+ });
241
+ }
157
242
 
158
243
  // src/lib/presence.ts
159
244
  function connectPresence(apiKey, sessionId, wsUrl) {
@@ -238,5 +323,6 @@ export {
238
323
  trackException,
239
324
  trackPageview,
240
325
  trackScroll,
241
- trackSearch
326
+ trackSearch,
327
+ trackWebVital
242
328
  };
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@arthurreira/analytics",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
+ "scripts": {
6
+ "build": "tsup",
7
+ "prepare": "tsup",
8
+ "dev": "tsup src/index.ts --format esm --dts --watch"
9
+ },
5
10
  "exports": {
6
11
  ".": {
7
12
  "types": "./dist/index.d.ts",
@@ -29,12 +34,11 @@
29
34
  "typescript": "^5.9.2",
30
35
  "vitest": "^4.1.7"
31
36
  },
37
+ "dependencies": {
38
+ "web-vitals": "^5.1.0"
39
+ },
32
40
  "peerDependencies": {
33
41
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
34
42
  "react": "^18.0.0 || ^19.0.0"
35
- },
36
- "scripts": {
37
- "build": "tsup",
38
- "dev": "tsup src/index.ts --format esm --dts --watch"
39
43
  }
40
44
  }