@arthurreira/analytics 0.19.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.
@@ -72,12 +72,13 @@ var AfAnalytics = (() => {
72
72
  var BC_CHANNEL = "af_analytics_session";
73
73
  var BC_ADOPT_TIMEOUT_MS = 50;
74
74
  function readConfig() {
75
- var _a, _b, _c, _d;
75
+ var _a, _b, _c, _d, _e, _f;
76
76
  const script = _currentScript != null ? _currentScript : document.querySelector("script[data-api-key]");
77
77
  const apiKey = (_b = (_a = script == null ? void 0 : script.dataset.apiKey) == null ? void 0 : _a.trim()) != null ? _b : "";
78
78
  const apiUrl = ((_d = (_c = script == null ? void 0 : script.dataset.url) == null ? void 0 : _c.trim()) != null ? _d : "").replace(/\/$/, "");
79
79
  if (!apiKey || !apiUrl) return null;
80
- return { apiKey, apiUrl };
80
+ const wsUrl = (_f = (_e = script == null ? void 0 : script.dataset.wsUrl) == null ? void 0 : _e.trim()) != null ? _f : apiUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/realtime";
81
+ return { apiKey, apiUrl, wsUrl };
81
82
  }
82
83
  function generateId() {
83
84
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -184,7 +185,7 @@ var AfAnalytics = (() => {
184
185
  return () => channel.close();
185
186
  }
186
187
  async function createRemoteSession(apiUrl, apiKey, visitorId) {
187
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
188
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
188
189
  const nav = navigator;
189
190
  const conn = (_c = (_b = (_a = nav["connection"]) != null ? _a : nav["mozConnection"]) != null ? _b : nav["webkitConnection"]) != null ? _c : null;
190
191
  const params = new URLSearchParams(window.location.search);
@@ -209,14 +210,22 @@ var AfAnalytics = (() => {
209
210
  utm_term: params.get("utm_term"),
210
211
  utm_content: params.get("utm_content")
211
212
  };
212
- const res = await fetch(`${apiUrl}/sessions`, {
213
- method: "POST",
214
- headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
215
- body: JSON.stringify(body)
216
- });
217
- if (!res.ok) return "";
218
- const data = await res.json();
219
- return data.id;
213
+ for (let attempt = 0; attempt < 3; attempt++) {
214
+ try {
215
+ const res = await fetch(`${apiUrl}/sessions`, {
216
+ method: "POST",
217
+ headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
218
+ body: JSON.stringify(body)
219
+ });
220
+ if (res.ok) {
221
+ const data = await res.json();
222
+ return (_o = (_n = data.session_id) != null ? _n : data.id) != null ? _o : "";
223
+ }
224
+ } catch (e) {
225
+ }
226
+ if (attempt < 2) await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
227
+ }
228
+ return "";
220
229
  }
221
230
  async function getOrCreateSession(apiUrl, apiKey) {
222
231
  const stored = getStoredSession();
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,16 +199,32 @@ 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
- var DEFAULT_WS_URL = "wss://edge.arthurreira.dev/realtime";
158
- function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
227
+ function connectPresence(apiKey, sessionId, wsUrl) {
159
228
  if (typeof WebSocket === "undefined") {
160
229
  return { disconnect: () => {
161
230
  } };
@@ -307,7 +376,7 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
307
376
  });
308
377
  return _sessionFlight;
309
378
  }
310
- function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
379
+ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl, routeName) {
311
380
  const sessionId = useRef(null);
312
381
  const pendingEvents = useRef([]);
313
382
  const hasSentEnd = useRef(false);
@@ -384,7 +453,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
384
453
  if (wsUrl) {
385
454
  presenceRef.current = connectPresence(apiKey, id, wsUrl);
386
455
  }
387
- trackPageview(apiUrl, apiKey, id, currentPathRef.current);
456
+ trackPageview(apiUrl, apiKey, id, currentPathRef.current, routeName ?? null);
388
457
  });
389
458
  };
390
459
  const handleStorage = (e) => {
@@ -400,7 +469,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
400
469
  if (hasSentEnd.current || !sessionId.current) {
401
470
  restartSession();
402
471
  } else {
403
- trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current);
472
+ trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current, routeName ?? null);
404
473
  }
405
474
  };
406
475
  let visHideTimer = null;
@@ -438,7 +507,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
438
507
  }
439
508
  return {
440
509
  trackPageview: (path) => {
441
- enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
510
+ enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path, routeName ?? null));
442
511
  },
443
512
  trackClick: (e, element) => {
444
513
  enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
@@ -460,22 +529,36 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
460
529
  },
461
530
  trackSearch: (query) => {
462
531
  enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
532
+ },
533
+ trackWebVital: (metric) => {
534
+ enqueueOrRun(() => trackWebVital(apiUrl, apiKey, sessionId.current, currentPath, metric));
463
535
  }
464
536
  };
465
537
  }
466
538
 
467
539
  // src/components/Analytics.tsx
468
- function Analytics({ apiKey, apiUrl, wsUrl }) {
540
+ function Analytics({ apiKey, apiUrl, wsUrl, routeName }) {
469
541
  const pathname = usePathname();
470
- 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);
471
543
  const lastTracked = useRef2(null);
472
544
  const lastScrollDepth = useRef2(0);
473
545
  useEffect2(() => {
474
546
  if (lastTracked.current === pathname) return;
475
547
  lastTracked.current = pathname;
476
548
  lastScrollDepth.current = 0;
477
- trackPageview2(pathname);
549
+ setTimeout(() => trackPageview2(pathname), 0);
478
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]);
479
562
  useEffect2(() => {
480
563
  if (typeof window === "undefined") return;
481
564
  const handler = (e) => trackClick2(e, e.target);
@@ -509,7 +592,10 @@ function Analytics({ apiKey, apiUrl, wsUrl }) {
509
592
  const handler = (e) => trackException2({
510
593
  exception_type: e.error?.name ?? "Error",
511
594
  exception_message: e.message,
512
- 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
513
599
  });
514
600
  window.addEventListener("error", handler);
515
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,15 +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
- declare const DEFAULT_WS_URL = "wss://edge.arthurreira.dev/realtime";
26
33
  interface PresenceConnection {
27
34
  disconnect: () => void;
28
35
  }
29
- declare function connectPresence(apiKey: string, sessionId: string, wsUrl?: string): PresenceConnection;
36
+ declare function connectPresence(apiKey: string, sessionId: string, wsUrl: string): PresenceConnection;
30
37
 
31
- export { DEFAULT_WS_URL, 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,16 +200,32 @@ 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
- var DEFAULT_WS_URL = "wss://edge.arthurreira.dev/realtime";
160
- function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
228
+ function connectPresence(apiKey, sessionId, wsUrl) {
161
229
  if (typeof WebSocket === "undefined") {
162
230
  return { disconnect: () => {
163
231
  } };
@@ -226,7 +294,6 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
226
294
  };
227
295
  }
228
296
  export {
229
- DEFAULT_WS_URL,
230
297
  connectPresence,
231
298
  consent,
232
299
  createSession,
@@ -240,5 +307,6 @@ export {
240
307
  trackException,
241
308
  trackPageview,
242
309
  trackScroll,
243
- trackSearch
310
+ trackSearch,
311
+ trackWebVital
244
312
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arthurreira/analytics",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsup",
@@ -34,6 +34,9 @@
34
34
  "typescript": "^5.9.2",
35
35
  "vitest": "^4.1.7"
36
36
  },
37
+ "dependencies": {
38
+ "web-vitals": "^5.1.0"
39
+ },
37
40
  "peerDependencies": {
38
41
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
39
42
  "react": "^18.0.0 || ^19.0.0"