@arthurreira/analytics 0.20.0 → 0.21.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,45 @@ 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 resolveElementHref(element) {
45
+ if (element.tagName.toLowerCase() === "a") {
46
+ return element.href || null;
47
+ }
48
+ const anchor = element.closest("a");
49
+ return anchor ? anchor.href || null : null;
50
+ }
51
+ function isExternalHref(href) {
52
+ if (!href) return null;
53
+ if (typeof window === "undefined") return null;
54
+ try {
55
+ return new URL(href).origin !== window.location.origin;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
26
60
  function getGpu() {
27
61
  try {
28
62
  const canvas = document.createElement("canvas");
@@ -61,7 +95,6 @@ async function createSession(apiUrl, apiKey, visitorId) {
61
95
  sessionData.utm_term = params.get("utm_term");
62
96
  sessionData.utm_content = params.get("utm_content");
63
97
  }
64
- console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
65
98
  try {
66
99
  const response = await fetch(`${apiUrl}/sessions`, {
67
100
  method: "POST",
@@ -71,17 +104,10 @@ async function createSession(apiUrl, apiKey, visitorId) {
71
104
  },
72
105
  body: JSON.stringify(sessionData)
73
106
  });
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
- }
107
+ if (!response.ok) return null;
80
108
  const data = await response.json();
81
- console.log(`[AF Analytics SDK] Session created: ${data.id}`);
82
109
  return data.id ?? null;
83
- } catch (err) {
84
- console.error(`[AF Analytics SDK] Session creation error: ${err}`);
110
+ } catch {
85
111
  return null;
86
112
  }
87
113
  }
@@ -96,45 +122,72 @@ async function sendEvent(apiUrl, apiKey, payload) {
96
122
  body: JSON.stringify(payload)
97
123
  });
98
124
  }
99
- async function trackPageview(apiUrl, apiKey, sessionId, path) {
125
+ async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
126
+ const navigationEntry = typeof performance !== "undefined" ? performance.getEntriesByType("navigation")[0] : void 0;
127
+ const title = typeof document !== "undefined" ? document.title || null : null;
128
+ const eventName = title || path;
100
129
  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
130
+ ...BASE_FIELDS(sessionId, "pageview", path, eventName, "navigation"),
131
+ scroll_depth: 0,
132
+ route_name: routeName ?? null,
133
+ navigation_type: navigationEntry?.type ?? null,
134
+ connection_type: getConnectionType()
105
135
  });
106
136
  }
107
137
  async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
138
+ const href = resolveElementHref(element);
139
+ const ariaLabel = element.getAttribute("aria-label");
140
+ const text = element.innerText?.slice(0, 60);
141
+ const eventName = ariaLabel || text || element.tagName.toLowerCase();
108
142
  await sendEvent(apiUrl, apiKey, {
109
- ...BASE_FIELDS(sessionId, "click", path),
143
+ ...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
110
144
  x_position: e.clientX,
111
145
  y_position: e.clientY,
112
146
  element_id: element.id || null,
113
147
  element_class: element.className || null,
114
- element_text: element.innerText?.slice(0, 100) || null
148
+ element_text: element.innerText?.slice(0, 100) || null,
149
+ tag_name: element.tagName.toLowerCase(),
150
+ element_href: href,
151
+ element_role: element.getAttribute("role") || null,
152
+ element_aria_label: element.getAttribute("aria-label") || null,
153
+ element_type: element.type || null,
154
+ element_name: element.name || null,
155
+ is_external_link: isExternalHref(href),
156
+ modifier_keys: {
157
+ alt: e.altKey,
158
+ ctrl: e.ctrlKey,
159
+ meta: e.metaKey,
160
+ shift: e.shiftKey
161
+ },
162
+ click_count: e.detail
115
163
  });
116
164
  }
117
165
  async function trackScroll(apiUrl, apiKey, sessionId, path, depth) {
118
166
  await sendEvent(apiUrl, apiKey, {
119
- ...BASE_FIELDS(sessionId, "scroll", path),
120
- scroll_depth: depth
167
+ ...BASE_FIELDS(sessionId, "scroll", path, `Scroll ${depth}%`, "interaction"),
168
+ scroll_depth: depth,
169
+ scroll_y_px: typeof window !== "undefined" ? window.scrollY : null,
170
+ document_height_px: typeof document !== "undefined" ? document.documentElement.scrollHeight : null
121
171
  });
122
172
  }
123
173
  async function trackCopy(apiUrl, apiKey, sessionId, path) {
174
+ const selection = typeof window !== "undefined" ? window.getSelection()?.toString() ?? null : null;
124
175
  await sendEvent(apiUrl, apiKey, {
125
- ...BASE_FIELDS(sessionId, "copy", path),
126
- copied_text: typeof window !== "undefined" ? window.getSelection()?.toString().slice(0, 200) || null : null
176
+ ...BASE_FIELDS(sessionId, "copy", path, "Copy", "interaction"),
177
+ copied_text: selection?.slice(0, 200) || null,
178
+ selection_length: selection?.length ?? 0
127
179
  });
128
180
  }
129
181
  async function trackSearch(apiUrl, apiKey, sessionId, path, query) {
130
182
  await sendEvent(apiUrl, apiKey, {
131
- ...BASE_FIELDS(sessionId, "search", path),
183
+ ...BASE_FIELDS(sessionId, "search", path, `Search: ${query.slice(0, 60)}`, "interaction"),
132
184
  search_query: query
133
185
  });
134
186
  }
135
187
  async function trackException(apiUrl, apiKey, sessionId, path, fields) {
188
+ const eventName = fields.exception_type ? `${fields.exception_type}: ${fields.exception_message.slice(0, 60)}` : fields.exception_message.slice(0, 80);
136
189
  await sendEvent(apiUrl, apiKey, {
137
- ...BASE_FIELDS(sessionId, "js_exception", path),
190
+ ...BASE_FIELDS(sessionId, "js_exception", path, eventName, "error"),
138
191
  ...fields
139
192
  });
140
193
  }
@@ -146,12 +199,29 @@ async function trackError(apiUrl, apiKey, sessionId, path, error) {
146
199
  });
147
200
  }
148
201
  async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
202
+ const eventName = ctaVariant ? `${ctaId} (${ctaVariant})` : ctaId;
149
203
  await sendEvent(apiUrl, apiKey, {
150
- ...BASE_FIELDS(sessionId, "cta_click", path),
204
+ ...BASE_FIELDS(sessionId, "cta_click", path, eventName, "conversion"),
151
205
  cta_id: ctaId,
152
206
  cta_variant: ctaVariant || null
153
207
  });
154
208
  }
209
+ var WEB_VITAL_FIELDS = {
210
+ lcp_ms: "lcp_ms",
211
+ cls_score: "cls_score",
212
+ inp_ms: "inp_ms",
213
+ ttfb_ms: "ttfb_ms",
214
+ fcp_ms: "fcp_ms"
215
+ };
216
+ async function trackWebVital(apiUrl, apiKey, sessionId, path, metric) {
217
+ const field = WEB_VITAL_FIELDS[metric.name];
218
+ if (!field) return;
219
+ await sendEvent(apiUrl, apiKey, {
220
+ ...BASE_FIELDS(sessionId, "web_vital", path, metric.name.toUpperCase(), "performance"),
221
+ [field]: metric.value,
222
+ rating: metric.rating
223
+ });
224
+ }
155
225
 
156
226
  // src/lib/presence.ts
157
227
  function connectPresence(apiKey, sessionId, wsUrl) {
@@ -306,7 +376,7 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
306
376
  });
307
377
  return _sessionFlight;
308
378
  }
309
- function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
379
+ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl, routeName) {
310
380
  const sessionId = useRef(null);
311
381
  const pendingEvents = useRef([]);
312
382
  const hasSentEnd = useRef(false);
@@ -383,7 +453,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
383
453
  if (wsUrl) {
384
454
  presenceRef.current = connectPresence(apiKey, id, wsUrl);
385
455
  }
386
- trackPageview(apiUrl, apiKey, id, currentPathRef.current);
456
+ trackPageview(apiUrl, apiKey, id, currentPathRef.current, routeName ?? null);
387
457
  });
388
458
  };
389
459
  const handleStorage = (e) => {
@@ -399,7 +469,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
399
469
  if (hasSentEnd.current || !sessionId.current) {
400
470
  restartSession();
401
471
  } else {
402
- trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current);
472
+ trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current, routeName ?? null);
403
473
  }
404
474
  };
405
475
  let visHideTimer = null;
@@ -437,7 +507,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
437
507
  }
438
508
  return {
439
509
  trackPageview: (path) => {
440
- enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
510
+ enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path, routeName ?? null));
441
511
  },
442
512
  trackClick: (e, element) => {
443
513
  enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
@@ -459,22 +529,36 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
459
529
  },
460
530
  trackSearch: (query) => {
461
531
  enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
532
+ },
533
+ trackWebVital: (metric) => {
534
+ enqueueOrRun(() => trackWebVital(apiUrl, apiKey, sessionId.current, currentPath, metric));
462
535
  }
463
536
  };
464
537
  }
465
538
 
466
539
  // src/components/Analytics.tsx
467
- function Analytics({ apiKey, apiUrl, wsUrl }) {
540
+ function Analytics({ apiKey, apiUrl, wsUrl, routeName }) {
468
541
  const pathname = usePathname();
469
- const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl);
542
+ const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2, trackWebVital: trackWebVital2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl, routeName);
470
543
  const lastTracked = useRef2(null);
471
544
  const lastScrollDepth = useRef2(0);
472
545
  useEffect2(() => {
473
546
  if (lastTracked.current === pathname) return;
474
547
  lastTracked.current = pathname;
475
548
  lastScrollDepth.current = 0;
476
- trackPageview2(pathname);
549
+ setTimeout(() => trackPageview2(pathname), 0);
477
550
  }, [pathname, trackPageview2]);
551
+ useEffect2(() => {
552
+ if (typeof window === "undefined") return;
553
+ const report = (name, value, rating) => {
554
+ trackWebVital2({ name, value, rating });
555
+ };
556
+ onLCP((m) => report("lcp_ms", Math.round(m.value), m.rating));
557
+ onCLS((m) => report("cls_score", m.value, m.rating));
558
+ onINP((m) => report("inp_ms", Math.round(m.value), m.rating));
559
+ onTTFB((m) => report("ttfb_ms", Math.round(m.value), m.rating));
560
+ onFCP((m) => report("fcp_ms", Math.round(m.value), m.rating));
561
+ }, [trackWebVital2]);
478
562
  useEffect2(() => {
479
563
  if (typeof window === "undefined") return;
480
564
  const handler = (e) => trackClick2(e, e.target);
@@ -508,7 +592,10 @@ function Analytics({ apiKey, apiUrl, wsUrl }) {
508
592
  const handler = (e) => trackException2({
509
593
  exception_type: e.error?.name ?? "Error",
510
594
  exception_message: e.message,
511
- stack_trace: e.error?.stack ?? null
595
+ stack_trace: e.error?.stack ?? null,
596
+ exception_filename: e.filename || null,
597
+ exception_lineno: e.lineno || null,
598
+ exception_colno: e.colno || null
512
599
  });
513
600
  window.addEventListener("error", handler);
514
601
  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,45 @@ 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 resolveElementHref(element) {
46
+ if (element.tagName.toLowerCase() === "a") {
47
+ return element.href || null;
48
+ }
49
+ const anchor = element.closest("a");
50
+ return anchor ? anchor.href || null : null;
51
+ }
52
+ function isExternalHref(href) {
53
+ if (!href) return null;
54
+ if (typeof window === "undefined") return null;
55
+ try {
56
+ return new URL(href).origin !== window.location.origin;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
28
61
  function getGpu() {
29
62
  try {
30
63
  const canvas = document.createElement("canvas");
@@ -63,7 +96,6 @@ async function createSession(apiUrl, apiKey, visitorId) {
63
96
  sessionData.utm_term = params.get("utm_term");
64
97
  sessionData.utm_content = params.get("utm_content");
65
98
  }
66
- console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
67
99
  try {
68
100
  const response = await fetch(`${apiUrl}/sessions`, {
69
101
  method: "POST",
@@ -73,17 +105,10 @@ async function createSession(apiUrl, apiKey, visitorId) {
73
105
  },
74
106
  body: JSON.stringify(sessionData)
75
107
  });
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
- }
108
+ if (!response.ok) return null;
82
109
  const data = await response.json();
83
- console.log(`[AF Analytics SDK] Session created: ${data.id}`);
84
110
  return data.id ?? null;
85
- } catch (err) {
86
- console.error(`[AF Analytics SDK] Session creation error: ${err}`);
111
+ } catch {
87
112
  return null;
88
113
  }
89
114
  }
@@ -98,45 +123,72 @@ async function sendEvent(apiUrl, apiKey, payload) {
98
123
  body: JSON.stringify(payload)
99
124
  });
100
125
  }
101
- async function trackPageview(apiUrl, apiKey, sessionId, path) {
126
+ async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
127
+ const navigationEntry = typeof performance !== "undefined" ? performance.getEntriesByType("navigation")[0] : void 0;
128
+ const title = typeof document !== "undefined" ? document.title || null : null;
129
+ const eventName = title || path;
102
130
  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
131
+ ...BASE_FIELDS(sessionId, "pageview", path, eventName, "navigation"),
132
+ scroll_depth: 0,
133
+ route_name: routeName ?? null,
134
+ navigation_type: navigationEntry?.type ?? null,
135
+ connection_type: getConnectionType()
107
136
  });
108
137
  }
109
138
  async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
139
+ const href = resolveElementHref(element);
140
+ const ariaLabel = element.getAttribute("aria-label");
141
+ const text = element.innerText?.slice(0, 60);
142
+ const eventName = ariaLabel || text || element.tagName.toLowerCase();
110
143
  await sendEvent(apiUrl, apiKey, {
111
- ...BASE_FIELDS(sessionId, "click", path),
144
+ ...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
112
145
  x_position: e.clientX,
113
146
  y_position: e.clientY,
114
147
  element_id: element.id || null,
115
148
  element_class: element.className || null,
116
- element_text: element.innerText?.slice(0, 100) || null
149
+ element_text: element.innerText?.slice(0, 100) || null,
150
+ tag_name: element.tagName.toLowerCase(),
151
+ element_href: href,
152
+ element_role: element.getAttribute("role") || null,
153
+ element_aria_label: element.getAttribute("aria-label") || null,
154
+ element_type: element.type || null,
155
+ element_name: element.name || null,
156
+ is_external_link: isExternalHref(href),
157
+ modifier_keys: {
158
+ alt: e.altKey,
159
+ ctrl: e.ctrlKey,
160
+ meta: e.metaKey,
161
+ shift: e.shiftKey
162
+ },
163
+ click_count: e.detail
117
164
  });
118
165
  }
119
166
  async function trackScroll(apiUrl, apiKey, sessionId, path, depth) {
120
167
  await sendEvent(apiUrl, apiKey, {
121
- ...BASE_FIELDS(sessionId, "scroll", path),
122
- scroll_depth: depth
168
+ ...BASE_FIELDS(sessionId, "scroll", path, `Scroll ${depth}%`, "interaction"),
169
+ scroll_depth: depth,
170
+ scroll_y_px: typeof window !== "undefined" ? window.scrollY : null,
171
+ document_height_px: typeof document !== "undefined" ? document.documentElement.scrollHeight : null
123
172
  });
124
173
  }
125
174
  async function trackCopy(apiUrl, apiKey, sessionId, path) {
175
+ const selection = typeof window !== "undefined" ? window.getSelection()?.toString() ?? null : null;
126
176
  await sendEvent(apiUrl, apiKey, {
127
- ...BASE_FIELDS(sessionId, "copy", path),
128
- copied_text: typeof window !== "undefined" ? window.getSelection()?.toString().slice(0, 200) || null : null
177
+ ...BASE_FIELDS(sessionId, "copy", path, "Copy", "interaction"),
178
+ copied_text: selection?.slice(0, 200) || null,
179
+ selection_length: selection?.length ?? 0
129
180
  });
130
181
  }
131
182
  async function trackSearch(apiUrl, apiKey, sessionId, path, query) {
132
183
  await sendEvent(apiUrl, apiKey, {
133
- ...BASE_FIELDS(sessionId, "search", path),
184
+ ...BASE_FIELDS(sessionId, "search", path, `Search: ${query.slice(0, 60)}`, "interaction"),
134
185
  search_query: query
135
186
  });
136
187
  }
137
188
  async function trackException(apiUrl, apiKey, sessionId, path, fields) {
189
+ const eventName = fields.exception_type ? `${fields.exception_type}: ${fields.exception_message.slice(0, 60)}` : fields.exception_message.slice(0, 80);
138
190
  await sendEvent(apiUrl, apiKey, {
139
- ...BASE_FIELDS(sessionId, "js_exception", path),
191
+ ...BASE_FIELDS(sessionId, "js_exception", path, eventName, "error"),
140
192
  ...fields
141
193
  });
142
194
  }
@@ -148,12 +200,29 @@ async function trackError(apiUrl, apiKey, sessionId, path, error) {
148
200
  });
149
201
  }
150
202
  async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
203
+ const eventName = ctaVariant ? `${ctaId} (${ctaVariant})` : ctaId;
151
204
  await sendEvent(apiUrl, apiKey, {
152
- ...BASE_FIELDS(sessionId, "cta_click", path),
205
+ ...BASE_FIELDS(sessionId, "cta_click", path, eventName, "conversion"),
153
206
  cta_id: ctaId,
154
207
  cta_variant: ctaVariant || null
155
208
  });
156
209
  }
210
+ var WEB_VITAL_FIELDS = {
211
+ lcp_ms: "lcp_ms",
212
+ cls_score: "cls_score",
213
+ inp_ms: "inp_ms",
214
+ ttfb_ms: "ttfb_ms",
215
+ fcp_ms: "fcp_ms"
216
+ };
217
+ async function trackWebVital(apiUrl, apiKey, sessionId, path, metric) {
218
+ const field = WEB_VITAL_FIELDS[metric.name];
219
+ if (!field) return;
220
+ await sendEvent(apiUrl, apiKey, {
221
+ ...BASE_FIELDS(sessionId, "web_vital", path, metric.name.toUpperCase(), "performance"),
222
+ [field]: metric.value,
223
+ rating: metric.rating
224
+ });
225
+ }
157
226
 
158
227
  // src/lib/presence.ts
159
228
  function connectPresence(apiKey, sessionId, wsUrl) {
@@ -238,5 +307,6 @@ export {
238
307
  trackException,
239
308
  trackPageview,
240
309
  trackScroll,
241
- trackSearch
310
+ trackSearch,
311
+ trackWebVital
242
312
  };
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@arthurreira/analytics",
3
- "version": "0.20.0",
3
+ "version": "0.21.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
  }