@arthurreira/analytics 0.16.0 → 0.17.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.
@@ -107,7 +107,7 @@ var AfAnalytics = (() => {
107
107
  session_id: sessionId,
108
108
  event_type: eventType,
109
109
  path: window.location.pathname,
110
- page_url: window.location.href
110
+ page_url: `${window.location.origin}${window.location.pathname}`
111
111
  };
112
112
  }
113
113
  async function createRemoteSession(apiUrl, apiKey, visitorId) {
@@ -141,6 +141,7 @@ var AfAnalytics = (() => {
141
141
  headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
142
142
  body: JSON.stringify(body)
143
143
  });
144
+ if (!res.ok) return "";
144
145
  const data = await res.json();
145
146
  return data.id;
146
147
  }
@@ -201,6 +202,28 @@ var AfAnalytics = (() => {
201
202
  }).catch(() => {
202
203
  });
203
204
  enqueue((id) => trackPageview(apiUrl, apiKey, id));
205
+ let lastPath = window.location.pathname;
206
+ const origPushState = history.pushState.bind(history);
207
+ history.pushState = (...args) => {
208
+ origPushState(...args);
209
+ const newPath = window.location.pathname;
210
+ if (newPath !== lastPath) {
211
+ lastPath = newPath;
212
+ lastScrollDepth = 0;
213
+ enqueue((id) => trackPageview(apiUrl, apiKey, id));
214
+ }
215
+ };
216
+ window.addEventListener("popstate", () => {
217
+ const newPath = window.location.pathname;
218
+ if (newPath !== lastPath) {
219
+ lastPath = newPath;
220
+ lastScrollDepth = 0;
221
+ enqueue((id) => trackPageview(apiUrl, apiKey, id));
222
+ }
223
+ });
224
+ window.addEventListener("hashchange", () => {
225
+ enqueue((id) => trackPageview(apiUrl, apiKey, id));
226
+ });
204
227
  window.addEventListener("click", (e) => {
205
228
  enqueue((id) => trackClick(apiUrl, apiKey, id, e));
206
229
  });
@@ -246,9 +269,18 @@ var AfAnalytics = (() => {
246
269
  navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end`);
247
270
  clearSession();
248
271
  };
272
+ let visHideTimer = null;
249
273
  document.addEventListener("visibilitychange", () => {
250
- if (document.visibilityState === "hidden") endSession();
274
+ if (document.visibilityState === "hidden") {
275
+ visHideTimer = setTimeout(endSession, 5e3);
276
+ } else {
277
+ if (visHideTimer !== null) {
278
+ clearTimeout(visHideTimer);
279
+ visHideTimer = null;
280
+ }
281
+ }
251
282
  });
283
+ window.addEventListener("pagehide", endSession);
252
284
  window.addEventListener("beforeunload", endSession);
253
285
  }
254
286
  if (document.readyState === "loading") {
package/dist/client.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  interface AnalyticsProps {
2
2
  apiKey: string;
3
3
  apiUrl: string;
4
+ wsUrl?: string;
4
5
  }
5
- declare function Analytics({ apiKey, apiUrl }: AnalyticsProps): null;
6
+ declare function Analytics({ apiKey, apiUrl, wsUrl }: AnalyticsProps): null;
6
7
 
7
8
  interface ExceptionFields {
8
9
  exception_type: string;
@@ -10,7 +11,7 @@ interface ExceptionFields {
10
11
  stack_trace: string | null;
11
12
  }
12
13
 
13
- declare function useAnalytics(apiUrl: string, apiKey: string): {
14
+ declare function useAnalytics(apiUrl: string, apiKey: string, currentPath: string, wsUrl?: string): {
14
15
  trackPageview: (path: string) => void;
15
16
  trackClick: (e: MouseEvent, element: HTMLElement) => void;
16
17
  trackScroll: (depth: number) => void;
package/dist/client.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  // src/components/Analytics.tsx
5
5
  import { useEffect as useEffect2, useRef as useRef2 } from "react";
6
+ import { usePathname } from "next/navigation";
6
7
 
7
8
  // src/hooks/useAnalytics.ts
8
9
  import { useEffect, useRef } from "react";
@@ -12,7 +13,7 @@ var BASE_FIELDS = (sessionId, eventType, path) => ({
12
13
  session_id: sessionId,
13
14
  event_type: eventType,
14
15
  path,
15
- page_url: typeof window !== "undefined" ? window.location.href : ""
16
+ page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : ""
16
17
  });
17
18
  function getGpu() {
18
19
  try {
@@ -51,16 +52,29 @@ async function createSession(apiUrl, apiKey, visitorId) {
51
52
  sessionData.utm_term = params.get("utm_term");
52
53
  sessionData.utm_content = params.get("utm_content");
53
54
  }
54
- const response = await fetch(`${apiUrl}/sessions`, {
55
- method: "POST",
56
- headers: {
57
- "X-API-Key": apiKey,
58
- "Content-Type": "application/json"
59
- },
60
- body: JSON.stringify(sessionData)
61
- });
62
- const data = await response.json();
63
- return data.id;
55
+ console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
56
+ try {
57
+ const response = await fetch(`${apiUrl}/sessions`, {
58
+ method: "POST",
59
+ headers: {
60
+ "X-API-Key": apiKey,
61
+ "Content-Type": "application/json"
62
+ },
63
+ body: JSON.stringify(sessionData)
64
+ });
65
+ console.log(`[AF Analytics SDK] Session creation response: ${response.status} ${response.statusText}`);
66
+ if (!response.ok) {
67
+ const errText = await response.text();
68
+ console.error(`[AF Analytics SDK] Session creation failed: ${errText}`);
69
+ return null;
70
+ }
71
+ const data = await response.json();
72
+ console.log(`[AF Analytics SDK] Session created: ${data.id}`);
73
+ return data.id ?? null;
74
+ } catch (err) {
75
+ console.error(`[AF Analytics SDK] Session creation error: ${err}`);
76
+ return null;
77
+ }
64
78
  }
65
79
  async function sendEvent(apiUrl, apiKey, payload) {
66
80
  await fetch(`${apiUrl}/events`, {
@@ -75,7 +89,6 @@ async function sendEvent(apiUrl, apiKey, payload) {
75
89
  async function trackPageview(apiUrl, apiKey, sessionId, path) {
76
90
  await sendEvent(apiUrl, apiKey, {
77
91
  ...BASE_FIELDS(sessionId, "pageview", path),
78
- page_url: `${typeof window !== "undefined" ? window.location.origin : ""}${path}`,
79
92
  page_title: typeof document !== "undefined" ? document.title || null : null,
80
93
  referrer: typeof document !== "undefined" ? document.referrer || null : null,
81
94
  scroll_depth: 0
@@ -130,6 +143,77 @@ async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
130
143
  });
131
144
  }
132
145
 
146
+ // src/lib/presence.ts
147
+ var DEFAULT_WS_URL = "wss://edge.arthurreira.dev/realtime";
148
+ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
149
+ if (typeof WebSocket === "undefined") {
150
+ return { disconnect: () => {
151
+ } };
152
+ }
153
+ let ws = null;
154
+ let attempts = 0;
155
+ let intentionalClose = false;
156
+ let retryTimer = null;
157
+ let pingInterval = null;
158
+ let targetUrl;
159
+ try {
160
+ const u = new URL(wsUrl);
161
+ u.searchParams.set("api_key", apiKey);
162
+ u.searchParams.set("session_id", sessionId);
163
+ targetUrl = u.toString();
164
+ } catch {
165
+ return { disconnect: () => {
166
+ } };
167
+ }
168
+ function clearPing() {
169
+ if (pingInterval !== null) {
170
+ clearInterval(pingInterval);
171
+ pingInterval = null;
172
+ }
173
+ }
174
+ function connect() {
175
+ try {
176
+ ws = new WebSocket(targetUrl);
177
+ } catch {
178
+ return;
179
+ }
180
+ ws.onopen = () => {
181
+ attempts = 0;
182
+ ws.send(JSON.stringify({ type: "join", api_key: apiKey, session_id: sessionId }));
183
+ pingInterval = setInterval(() => {
184
+ if (ws?.readyState === WebSocket.OPEN) {
185
+ ws.send(JSON.stringify({ type: "ping", session_id: sessionId }));
186
+ }
187
+ }, 3e4);
188
+ };
189
+ ws.onclose = () => {
190
+ clearPing();
191
+ if (intentionalClose || attempts >= 3) return;
192
+ const delay = Math.pow(2, attempts) * 1e3;
193
+ attempts++;
194
+ retryTimer = setTimeout(connect, delay);
195
+ };
196
+ }
197
+ connect();
198
+ return {
199
+ disconnect: () => {
200
+ intentionalClose = true;
201
+ clearPing();
202
+ if (retryTimer !== null) {
203
+ clearTimeout(retryTimer);
204
+ retryTimer = null;
205
+ }
206
+ if (ws) {
207
+ if (ws.readyState === WebSocket.OPEN) {
208
+ ws.send(JSON.stringify({ type: "leave", session_id: sessionId }));
209
+ }
210
+ ws.close();
211
+ ws = null;
212
+ }
213
+ }
214
+ };
215
+ }
216
+
133
217
  // src/hooks/useAnalytics.ts
134
218
  var SESSION_EXPIRY_MINUTES = 30;
135
219
  var _sessionFlight = null;
@@ -143,11 +227,15 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
143
227
  localStorage.setItem("af_session_last_activity", String(now));
144
228
  return storedSessionId;
145
229
  }
230
+ localStorage.removeItem("af_session_id");
231
+ localStorage.removeItem("af_session_last_activity");
146
232
  }
147
233
  if (_sessionFlight) return _sessionFlight;
148
234
  _sessionFlight = createSession(apiUrl, apiKey, visitorId).then((id) => {
149
- localStorage.setItem("af_session_id", id);
150
- localStorage.setItem("af_session_last_activity", String(Date.now()));
235
+ if (id) {
236
+ localStorage.setItem("af_session_id", id);
237
+ localStorage.setItem("af_session_last_activity", String(Date.now()));
238
+ }
151
239
  _sessionFlight = null;
152
240
  return id;
153
241
  }).catch((err) => {
@@ -156,19 +244,22 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
156
244
  });
157
245
  return _sessionFlight;
158
246
  }
159
- function useAnalytics(apiUrl, apiKey) {
247
+ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
160
248
  const sessionId = useRef(null);
161
249
  const pendingEvents = useRef([]);
162
- const pathname = useRef(typeof window !== "undefined" ? window?.location?.pathname ?? "" : "");
163
250
  const hasSentEnd = useRef(false);
251
+ const presenceRef = useRef(null);
164
252
  useEffect(() => {
165
253
  if (typeof window === "undefined") return;
166
254
  const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
167
255
  localStorage.setItem("af_analytics_visitor_id", visitor_id);
168
256
  let cancelled = false;
169
257
  getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
170
- if (cancelled) return;
258
+ if (cancelled || !id) return;
171
259
  sessionId.current = id;
260
+ if (wsUrl) {
261
+ presenceRef.current = connectPresence(apiKey, id, wsUrl);
262
+ }
172
263
  pendingEvents.current.forEach((fn) => fn());
173
264
  pendingEvents.current = [];
174
265
  });
@@ -181,19 +272,30 @@ function useAnalytics(apiUrl, apiKey) {
181
272
  const sendEnd = () => {
182
273
  if (!sessionId.current || hasSentEnd.current) return;
183
274
  hasSentEnd.current = true;
275
+ presenceRef.current?.disconnect();
184
276
  navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end`);
185
277
  localStorage.removeItem("af_session_id");
186
278
  localStorage.removeItem("af_session_last_activity");
187
279
  };
280
+ let visHideTimer = null;
188
281
  const handleVisibility = () => {
189
- if (document.visibilityState === "hidden") sendEnd();
282
+ if (document.visibilityState === "hidden") {
283
+ visHideTimer = setTimeout(sendEnd, 5e3);
284
+ } else {
285
+ if (visHideTimer !== null) {
286
+ clearTimeout(visHideTimer);
287
+ visHideTimer = null;
288
+ }
289
+ }
190
290
  };
191
- const handleUnload = () => sendEnd();
192
291
  document.addEventListener("visibilitychange", handleVisibility);
193
- window.addEventListener("beforeunload", handleUnload);
292
+ window.addEventListener("pagehide", sendEnd);
293
+ window.addEventListener("beforeunload", sendEnd);
194
294
  return () => {
295
+ if (visHideTimer !== null) clearTimeout(visHideTimer);
195
296
  document.removeEventListener("visibilitychange", handleVisibility);
196
- window.removeEventListener("beforeunload", handleUnload);
297
+ window.removeEventListener("pagehide", sendEnd);
298
+ window.removeEventListener("beforeunload", sendEnd);
197
299
  };
198
300
  }, [apiUrl]);
199
301
  function enqueueOrRun(fn) {
@@ -209,38 +311,39 @@ function useAnalytics(apiUrl, apiKey) {
209
311
  enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
210
312
  },
211
313
  trackClick: (e, element) => {
212
- enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, pathname.current, e, element));
314
+ enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
213
315
  },
214
316
  trackScroll: (depth) => {
215
- enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current, pathname.current, depth));
317
+ enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current, currentPath, depth));
216
318
  },
217
319
  trackCopy: () => {
218
- enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current, pathname.current));
320
+ enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current, currentPath));
219
321
  },
220
322
  trackException: (fields) => {
221
- enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current, pathname.current, fields));
323
+ enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current, currentPath, fields));
222
324
  },
223
325
  trackError: (error) => {
224
- enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current, pathname.current, error));
326
+ enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current, currentPath, error));
225
327
  },
226
328
  trackCTA: (ctaId, ctaVariant) => {
227
- enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current, pathname.current, ctaId, ctaVariant));
329
+ enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current, currentPath, ctaId, ctaVariant));
228
330
  },
229
331
  trackSearch: (query) => {
230
- enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, pathname.current, query));
332
+ enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
231
333
  }
232
334
  };
233
335
  }
234
336
 
235
337
  // src/components/Analytics.tsx
236
- function Analytics({ apiKey, apiUrl }) {
237
- const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey);
338
+ function Analytics({ apiKey, apiUrl, wsUrl }) {
339
+ const pathname = usePathname();
340
+ const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl);
238
341
  const lastTracked = useRef2(null);
239
342
  const lastScrollDepth = useRef2(0);
240
- const pathname = typeof window !== "undefined" ? window.location.pathname : "";
241
343
  useEffect2(() => {
242
344
  if (lastTracked.current === pathname) return;
243
345
  lastTracked.current = pathname;
346
+ lastScrollDepth.current = 0;
244
347
  trackPageview2(pathname);
245
348
  }, [pathname, trackPageview2]);
246
349
  useEffect2(() => {
@@ -252,12 +355,10 @@ function Analytics({ apiKey, apiUrl }) {
252
355
  useEffect2(() => {
253
356
  if (typeof window === "undefined") return;
254
357
  const handler = () => {
255
- const scrolled = window.scrollY;
256
358
  const total = document.documentElement.scrollHeight - window.innerHeight;
257
359
  if (total <= 0) return;
258
- const depth = Math.round(scrolled / total * 100);
259
- const milestones = [25, 50, 75, 100];
260
- for (const m of milestones) {
360
+ const depth = Math.round(window.scrollY / total * 100);
361
+ for (const m of [25, 50, 75, 100]) {
261
362
  if (depth >= m && lastScrollDepth.current < m) {
262
363
  lastScrollDepth.current = m;
263
364
  trackScroll2(m);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- declare function createSession(apiUrl: string, apiKey: string, visitorId?: string): Promise<any>;
1
+ declare function createSession(apiUrl: string, apiKey: string, visitorId?: string): Promise<string | null>;
2
2
  declare function trackPageview(apiUrl: string, apiKey: string, sessionId: string, path: string): Promise<void>;
3
3
  declare function trackClick(apiUrl: string, apiKey: string, sessionId: string, path: string, e: MouseEvent, element: HTMLElement): Promise<void>;
4
4
  declare function trackScroll(apiUrl: string, apiKey: string, sessionId: string, path: string, depth: number): Promise<void>;
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var BASE_FIELDS = (sessionId, eventType, path) => ({
3
3
  session_id: sessionId,
4
4
  event_type: eventType,
5
5
  path,
6
- page_url: typeof window !== "undefined" ? window.location.href : ""
6
+ page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : ""
7
7
  });
8
8
  function getGpu() {
9
9
  try {
@@ -42,16 +42,29 @@ async function createSession(apiUrl, apiKey, visitorId) {
42
42
  sessionData.utm_term = params.get("utm_term");
43
43
  sessionData.utm_content = params.get("utm_content");
44
44
  }
45
- const response = await fetch(`${apiUrl}/sessions`, {
46
- method: "POST",
47
- headers: {
48
- "X-API-Key": apiKey,
49
- "Content-Type": "application/json"
50
- },
51
- body: JSON.stringify(sessionData)
52
- });
53
- const data = await response.json();
54
- return data.id;
45
+ console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
46
+ try {
47
+ const response = await fetch(`${apiUrl}/sessions`, {
48
+ method: "POST",
49
+ headers: {
50
+ "X-API-Key": apiKey,
51
+ "Content-Type": "application/json"
52
+ },
53
+ body: JSON.stringify(sessionData)
54
+ });
55
+ console.log(`[AF Analytics SDK] Session creation response: ${response.status} ${response.statusText}`);
56
+ if (!response.ok) {
57
+ const errText = await response.text();
58
+ console.error(`[AF Analytics SDK] Session creation failed: ${errText}`);
59
+ return null;
60
+ }
61
+ const data = await response.json();
62
+ console.log(`[AF Analytics SDK] Session created: ${data.id}`);
63
+ return data.id ?? null;
64
+ } catch (err) {
65
+ console.error(`[AF Analytics SDK] Session creation error: ${err}`);
66
+ return null;
67
+ }
55
68
  }
56
69
  async function sendEvent(apiUrl, apiKey, payload) {
57
70
  await fetch(`${apiUrl}/events`, {
@@ -66,7 +79,6 @@ async function sendEvent(apiUrl, apiKey, payload) {
66
79
  async function trackPageview(apiUrl, apiKey, sessionId, path) {
67
80
  await sendEvent(apiUrl, apiKey, {
68
81
  ...BASE_FIELDS(sessionId, "pageview", path),
69
- page_url: `${typeof window !== "undefined" ? window.location.origin : ""}${path}`,
70
82
  page_title: typeof document !== "undefined" ? document.title || null : null,
71
83
  referrer: typeof document !== "undefined" ? document.referrer || null : null,
72
84
  scroll_depth: 0
@@ -132,6 +144,7 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
132
144
  let attempts = 0;
133
145
  let intentionalClose = false;
134
146
  let retryTimer = null;
147
+ let pingInterval = null;
135
148
  let targetUrl;
136
149
  try {
137
150
  const u = new URL(wsUrl);
@@ -142,6 +155,12 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
142
155
  return { disconnect: () => {
143
156
  } };
144
157
  }
158
+ function clearPing() {
159
+ if (pingInterval !== null) {
160
+ clearInterval(pingInterval);
161
+ pingInterval = null;
162
+ }
163
+ }
145
164
  function connect() {
146
165
  try {
147
166
  ws = new WebSocket(targetUrl);
@@ -151,8 +170,14 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
151
170
  ws.onopen = () => {
152
171
  attempts = 0;
153
172
  ws.send(JSON.stringify({ type: "join", api_key: apiKey, session_id: sessionId }));
173
+ pingInterval = setInterval(() => {
174
+ if (ws?.readyState === WebSocket.OPEN) {
175
+ ws.send(JSON.stringify({ type: "ping", session_id: sessionId }));
176
+ }
177
+ }, 3e4);
154
178
  };
155
179
  ws.onclose = () => {
180
+ clearPing();
156
181
  if (intentionalClose || attempts >= 3) return;
157
182
  const delay = Math.pow(2, attempts) * 1e3;
158
183
  attempts++;
@@ -163,11 +188,15 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
163
188
  return {
164
189
  disconnect: () => {
165
190
  intentionalClose = true;
191
+ clearPing();
166
192
  if (retryTimer !== null) {
167
193
  clearTimeout(retryTimer);
168
194
  retryTimer = null;
169
195
  }
170
196
  if (ws) {
197
+ if (ws.readyState === WebSocket.OPEN) {
198
+ ws.send(JSON.stringify({ type: "leave", session_id: sessionId }));
199
+ }
171
200
  ws.close();
172
201
  ws = null;
173
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arthurreira/analytics",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsup",