@arthurreira/analytics 0.16.1 → 0.18.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.
@@ -42,6 +42,9 @@ var AfAnalytics = (() => {
42
42
  });
43
43
  var _currentScript = document.currentScript;
44
44
  var SESSION_EXPIRY_MS = 30 * 60 * 1e3;
45
+ var IDLE_TOUCH_INTERVAL_MS = 5 * 60 * 1e3;
46
+ var BC_CHANNEL = "af_analytics_session";
47
+ var BC_ADOPT_TIMEOUT_MS = 50;
45
48
  function readConfig() {
46
49
  var _a, _b, _c, _d;
47
50
  const script = _currentScript != null ? _currentScript : document.querySelector("script[data-api-key]");
@@ -107,9 +110,52 @@ var AfAnalytics = (() => {
107
110
  session_id: sessionId,
108
111
  event_type: eventType,
109
112
  path: window.location.pathname,
110
- page_url: window.location.href
113
+ page_url: `${window.location.origin}${window.location.pathname}`
111
114
  };
112
115
  }
116
+ function requestSessionFromPeer() {
117
+ if (typeof BroadcastChannel === "undefined") return Promise.resolve(null);
118
+ return new Promise((resolve) => {
119
+ let channel;
120
+ try {
121
+ channel = new BroadcastChannel(BC_CHANNEL);
122
+ } catch (e) {
123
+ resolve(null);
124
+ return;
125
+ }
126
+ const timer = setTimeout(() => {
127
+ channel.close();
128
+ resolve(null);
129
+ }, BC_ADOPT_TIMEOUT_MS);
130
+ channel.onmessage = (e) => {
131
+ var _a;
132
+ if (((_a = e.data) == null ? void 0 : _a.type) === "session_adopt" && typeof e.data.session_id === "string") {
133
+ clearTimeout(timer);
134
+ channel.close();
135
+ resolve(e.data.session_id);
136
+ }
137
+ };
138
+ channel.postMessage({ type: "session_claim" });
139
+ });
140
+ }
141
+ function openSessionResponder(sessionId) {
142
+ if (typeof BroadcastChannel === "undefined") return () => {
143
+ };
144
+ let channel;
145
+ try {
146
+ channel = new BroadcastChannel(BC_CHANNEL);
147
+ } catch (e) {
148
+ return () => {
149
+ };
150
+ }
151
+ channel.onmessage = (e) => {
152
+ var _a;
153
+ if (((_a = e.data) == null ? void 0 : _a.type) === "session_claim") {
154
+ channel.postMessage({ type: "session_adopt", session_id: sessionId });
155
+ }
156
+ };
157
+ return () => channel.close();
158
+ }
113
159
  async function createRemoteSession(apiUrl, apiKey, visitorId) {
114
160
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
115
161
  const nav = navigator;
@@ -136,20 +182,13 @@ var AfAnalytics = (() => {
136
182
  utm_term: params.get("utm_term"),
137
183
  utm_content: params.get("utm_content")
138
184
  };
139
- console.log(`[AF Analytics] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
140
185
  const res = await fetch(`${apiUrl}/sessions`, {
141
186
  method: "POST",
142
187
  headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
143
188
  body: JSON.stringify(body)
144
189
  });
145
- console.log(`[AF Analytics] Session creation response: ${res.status} ${res.statusText}`);
146
- if (!res.ok) {
147
- const errText = await res.text();
148
- console.error(`[AF Analytics] Session creation failed: ${errText}`);
149
- return "";
150
- }
190
+ if (!res.ok) return "";
151
191
  const data = await res.json();
152
- console.log(`[AF Analytics] Session created: ${data.id}`);
153
192
  return data.id;
154
193
  }
155
194
  async function getOrCreateSession(apiUrl, apiKey) {
@@ -158,9 +197,17 @@ var AfAnalytics = (() => {
158
197
  touchSession();
159
198
  return stored;
160
199
  }
200
+ const adopted = await requestSessionFromPeer();
201
+ if (adopted) {
202
+ localStorage.setItem("af_session_id", adopted);
203
+ localStorage.setItem("af_session_last_activity", String(Date.now()));
204
+ return adopted;
205
+ }
161
206
  const id = await createRemoteSession(apiUrl, apiKey, getVisitorId());
162
- localStorage.setItem("af_session_id", id);
163
- localStorage.setItem("af_session_last_activity", String(Date.now()));
207
+ if (id) {
208
+ localStorage.setItem("af_session_id", id);
209
+ localStorage.setItem("af_session_last_activity", String(Date.now()));
210
+ }
164
211
  return id;
165
212
  }
166
213
  function trackPageview(apiUrl, apiKey, sessionId) {
@@ -195,6 +242,9 @@ var AfAnalytics = (() => {
195
242
  const pending = [];
196
243
  let sessionEnded = false;
197
244
  let lastScrollDepth = 0;
245
+ let closeResponder = () => {
246
+ };
247
+ let idleTimer = null;
198
248
  function enqueue(fn) {
199
249
  if (sessionId) {
200
250
  touchSession();
@@ -203,12 +253,68 @@ var AfAnalytics = (() => {
203
253
  pending.push(fn);
204
254
  }
205
255
  }
206
- getOrCreateSession(apiUrl, apiKey).then((id) => {
256
+ function startSession(id) {
207
257
  sessionId = id;
258
+ closeResponder = openSessionResponder(id);
259
+ idleTimer = setInterval(touchSession, IDLE_TOUCH_INTERVAL_MS);
208
260
  pending.splice(0).forEach((fn) => fn(id));
261
+ }
262
+ function endSession() {
263
+ if (!sessionId || sessionEnded) return;
264
+ sessionEnded = true;
265
+ if (idleTimer !== null) {
266
+ clearInterval(idleTimer);
267
+ idleTimer = null;
268
+ }
269
+ closeResponder();
270
+ navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end`);
271
+ clearSession();
272
+ }
273
+ function restartSession() {
274
+ sessionEnded = false;
275
+ sessionId = null;
276
+ if (idleTimer !== null) {
277
+ clearInterval(idleTimer);
278
+ idleTimer = null;
279
+ }
280
+ closeResponder();
281
+ closeResponder = () => {
282
+ };
283
+ clearSession();
284
+ getOrCreateSession(apiUrl, apiKey).then((id) => {
285
+ if (!id) return;
286
+ startSession(id);
287
+ trackPageview(apiUrl, apiKey, id);
288
+ }).catch(() => {
289
+ });
290
+ }
291
+ getOrCreateSession(apiUrl, apiKey).then((id) => {
292
+ if (id) startSession(id);
209
293
  }).catch(() => {
210
294
  });
211
295
  enqueue((id) => trackPageview(apiUrl, apiKey, id));
296
+ let lastPath = window.location.pathname;
297
+ const origPushState = history.pushState.bind(history);
298
+ history.pushState = (...args) => {
299
+ origPushState(...args);
300
+ const newPath = window.location.pathname;
301
+ if (newPath !== lastPath) {
302
+ lastPath = newPath;
303
+ lastScrollDepth = 0;
304
+ enqueue((id) => trackPageview(apiUrl, apiKey, id));
305
+ }
306
+ };
307
+ window.addEventListener("popstate", () => {
308
+ const newPath = window.location.pathname;
309
+ if (newPath !== lastPath) {
310
+ lastPath = newPath;
311
+ lastScrollDepth = 0;
312
+ enqueue((id) => trackPageview(apiUrl, apiKey, id));
313
+ }
314
+ });
315
+ window.addEventListener("hashchange", () => {
316
+ enqueue((id) => trackPageview(apiUrl, apiKey, id));
317
+ });
212
318
  window.addEventListener("click", (e) => {
213
319
  enqueue((id) => trackClick(apiUrl, apiKey, id, e));
214
320
  });
@@ -248,14 +354,32 @@ var AfAnalytics = (() => {
248
354
  });
249
355
  });
250
356
  });
251
- const endSession = () => {
252
- if (!sessionId || sessionEnded) return;
253
- sessionEnded = true;
254
- navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end`);
255
- clearSession();
256
- };
357
+ window.addEventListener("storage", (e) => {
358
+ if (e.key === "af_session_id" && e.newValue === null && e.oldValue !== null) {
359
+ restartSession();
360
+ }
361
+ });
362
+ let visHideTimer = null;
257
363
  document.addEventListener("visibilitychange", () => {
258
- if (document.visibilityState === "hidden") endSession();
364
+ if (document.visibilityState === "hidden") {
365
+ visHideTimer = setTimeout(endSession, 5e3);
366
+ } else {
367
+ if (visHideTimer !== null) {
368
+ clearTimeout(visHideTimer);
369
+ visHideTimer = null;
370
+ }
371
+ }
372
+ });
373
+ window.addEventListener("pagehide", (e) => {
374
+ if (!e.persisted) endSession();
375
+ });
376
+ window.addEventListener("pageshow", (e) => {
377
+ if (!e.persisted) return;
378
+ if (sessionEnded || !sessionId) {
379
+ restartSession();
380
+ } else {
381
+ trackPageview(apiUrl, apiKey, sessionId);
382
+ }
259
383
  });
260
384
  window.addEventListener("beforeunload", endSession);
261
385
  }
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 {
@@ -88,7 +89,6 @@ async function sendEvent(apiUrl, apiKey, payload) {
88
89
  async function trackPageview(apiUrl, apiKey, sessionId, path) {
89
90
  await sendEvent(apiUrl, apiKey, {
90
91
  ...BASE_FIELDS(sessionId, "pageview", path),
91
- page_url: `${typeof window !== "undefined" ? window.location.origin : ""}${path}`,
92
92
  page_title: typeof document !== "undefined" ? document.title || null : null,
93
93
  referrer: typeof document !== "undefined" ? document.referrer || null : null,
94
94
  scroll_depth: 0
@@ -143,9 +143,124 @@ async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
143
143
  });
144
144
  }
145
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
+
146
217
  // src/hooks/useAnalytics.ts
147
218
  var SESSION_EXPIRY_MINUTES = 30;
219
+ var IDLE_TOUCH_INTERVAL_MS = 5 * 60 * 1e3;
220
+ var BC_CHANNEL = "af_analytics_session";
221
+ var BC_ADOPT_TIMEOUT_MS = 50;
148
222
  var _sessionFlight = null;
223
+ function requestSessionFromPeer() {
224
+ if (typeof BroadcastChannel === "undefined") return Promise.resolve(null);
225
+ return new Promise((resolve) => {
226
+ let channel;
227
+ try {
228
+ channel = new BroadcastChannel(BC_CHANNEL);
229
+ } catch {
230
+ resolve(null);
231
+ return;
232
+ }
233
+ const timer = setTimeout(() => {
234
+ channel.close();
235
+ resolve(null);
236
+ }, BC_ADOPT_TIMEOUT_MS);
237
+ channel.onmessage = (e) => {
238
+ if (e.data?.type === "session_adopt" && typeof e.data.session_id === "string") {
239
+ clearTimeout(timer);
240
+ channel.close();
241
+ resolve(e.data.session_id);
242
+ }
243
+ };
244
+ channel.postMessage({ type: "session_claim" });
245
+ });
246
+ }
247
+ function openSessionResponder(sessionId) {
248
+ if (typeof BroadcastChannel === "undefined") return () => {
249
+ };
250
+ let channel;
251
+ try {
252
+ channel = new BroadcastChannel(BC_CHANNEL);
253
+ } catch {
254
+ return () => {
255
+ };
256
+ }
257
+ channel.onmessage = (e) => {
258
+ if (e.data?.type === "session_claim") {
259
+ channel.postMessage({ type: "session_adopt", session_id: sessionId });
260
+ }
261
+ };
262
+ return () => channel.close();
263
+ }
149
264
  async function getOrCreateSession(apiUrl, apiKey, visitorId) {
150
265
  const storedSessionId = localStorage.getItem("af_session_id");
151
266
  const lastActivity = localStorage.getItem("af_session_last_activity");
@@ -160,11 +275,20 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
160
275
  localStorage.removeItem("af_session_last_activity");
161
276
  }
162
277
  if (_sessionFlight) return _sessionFlight;
163
- _sessionFlight = createSession(apiUrl, apiKey, visitorId).then((id) => {
278
+ _sessionFlight = (async () => {
279
+ const adopted = await requestSessionFromPeer();
280
+ if (adopted) {
281
+ localStorage.setItem("af_session_id", adopted);
282
+ localStorage.setItem("af_session_last_activity", String(Date.now()));
283
+ return adopted;
284
+ }
285
+ const id = await createSession(apiUrl, apiKey, visitorId);
164
286
  if (id) {
165
287
  localStorage.setItem("af_session_id", id);
166
288
  localStorage.setItem("af_session_last_activity", String(Date.now()));
167
289
  }
290
+ return id;
291
+ })().then((id) => {
168
292
  _sessionFlight = null;
169
293
  return id;
170
294
  }).catch((err) => {
@@ -173,11 +297,18 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
173
297
  });
174
298
  return _sessionFlight;
175
299
  }
176
- function useAnalytics(apiUrl, apiKey) {
300
+ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
177
301
  const sessionId = useRef(null);
178
302
  const pendingEvents = useRef([]);
179
- const pathname = useRef(typeof window !== "undefined" ? window?.location?.pathname ?? "" : "");
180
303
  const hasSentEnd = useRef(false);
304
+ const presenceRef = useRef(null);
305
+ const closeResponderRef = useRef(() => {
306
+ });
307
+ const idleTimerRef = useRef(null);
308
+ const currentPathRef = useRef(currentPath);
309
+ useEffect(() => {
310
+ currentPathRef.current = currentPath;
311
+ }, [currentPath]);
181
312
  useEffect(() => {
182
313
  if (typeof window === "undefined") return;
183
314
  const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
@@ -186,6 +317,15 @@ function useAnalytics(apiUrl, apiKey) {
186
317
  getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
187
318
  if (cancelled || !id) return;
188
319
  sessionId.current = id;
320
+ closeResponderRef.current = openSessionResponder(id);
321
+ idleTimerRef.current = setInterval(() => {
322
+ if (!hasSentEnd.current) {
323
+ localStorage.setItem("af_session_last_activity", String(Date.now()));
324
+ }
325
+ }, IDLE_TOUCH_INTERVAL_MS);
326
+ if (wsUrl) {
327
+ presenceRef.current = connectPresence(apiKey, id, wsUrl);
328
+ }
189
329
  pendingEvents.current.forEach((fn) => fn());
190
330
  pendingEvents.current = [];
191
331
  });
@@ -198,19 +338,84 @@ function useAnalytics(apiUrl, apiKey) {
198
338
  const sendEnd = () => {
199
339
  if (!sessionId.current || hasSentEnd.current) return;
200
340
  hasSentEnd.current = true;
341
+ if (idleTimerRef.current !== null) {
342
+ clearInterval(idleTimerRef.current);
343
+ idleTimerRef.current = null;
344
+ }
345
+ closeResponderRef.current();
346
+ presenceRef.current?.disconnect();
347
+ presenceRef.current = null;
201
348
  navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end`);
202
349
  localStorage.removeItem("af_session_id");
203
350
  localStorage.removeItem("af_session_last_activity");
204
351
  };
352
+ const restartSession = () => {
353
+ sessionId.current = null;
354
+ hasSentEnd.current = false;
355
+ if (idleTimerRef.current !== null) {
356
+ clearInterval(idleTimerRef.current);
357
+ idleTimerRef.current = null;
358
+ }
359
+ closeResponderRef.current();
360
+ closeResponderRef.current = () => {
361
+ };
362
+ presenceRef.current?.disconnect();
363
+ presenceRef.current = null;
364
+ const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
365
+ getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
366
+ if (!id) return;
367
+ sessionId.current = id;
368
+ closeResponderRef.current = openSessionResponder(id);
369
+ idleTimerRef.current = setInterval(() => {
370
+ if (!hasSentEnd.current) {
371
+ localStorage.setItem("af_session_last_activity", String(Date.now()));
372
+ }
373
+ }, IDLE_TOUCH_INTERVAL_MS);
374
+ if (wsUrl) {
375
+ presenceRef.current = connectPresence(apiKey, id, wsUrl);
376
+ }
377
+ trackPageview(apiUrl, apiKey, id, currentPathRef.current);
378
+ });
379
+ };
380
+ const handleStorage = (e) => {
381
+ if (e.key === "af_session_id" && e.newValue === null && e.oldValue !== null) {
382
+ restartSession();
383
+ }
384
+ };
385
+ const handlePageHide = (e) => {
386
+ if (!e.persisted) sendEnd();
387
+ };
388
+ const handlePageShow = (e) => {
389
+ if (!e.persisted) return;
390
+ if (hasSentEnd.current || !sessionId.current) {
391
+ restartSession();
392
+ } else {
393
+ trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current);
394
+ }
395
+ };
396
+ let visHideTimer = null;
205
397
  const handleVisibility = () => {
206
- if (document.visibilityState === "hidden") sendEnd();
398
+ if (document.visibilityState === "hidden") {
399
+ visHideTimer = setTimeout(sendEnd, 5e3);
400
+ } else {
401
+ if (visHideTimer !== null) {
402
+ clearTimeout(visHideTimer);
403
+ visHideTimer = null;
404
+ }
405
+ }
207
406
  };
208
- const handleUnload = () => sendEnd();
407
+ window.addEventListener("storage", handleStorage);
209
408
  document.addEventListener("visibilitychange", handleVisibility);
210
- window.addEventListener("beforeunload", handleUnload);
409
+ window.addEventListener("pagehide", handlePageHide);
410
+ window.addEventListener("pageshow", handlePageShow);
411
+ window.addEventListener("beforeunload", sendEnd);
211
412
  return () => {
413
+ if (visHideTimer !== null) clearTimeout(visHideTimer);
414
+ window.removeEventListener("storage", handleStorage);
212
415
  document.removeEventListener("visibilitychange", handleVisibility);
213
- window.removeEventListener("beforeunload", handleUnload);
416
+ window.removeEventListener("pagehide", handlePageHide);
417
+ window.removeEventListener("pageshow", handlePageShow);
418
+ window.removeEventListener("beforeunload", sendEnd);
214
419
  };
215
420
  }, [apiUrl]);
216
421
  function enqueueOrRun(fn) {
@@ -226,38 +431,39 @@ function useAnalytics(apiUrl, apiKey) {
226
431
  enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
227
432
  },
228
433
  trackClick: (e, element) => {
229
- enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, pathname.current, e, element));
434
+ enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
230
435
  },
231
436
  trackScroll: (depth) => {
232
- enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current, pathname.current, depth));
437
+ enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current, currentPath, depth));
233
438
  },
234
439
  trackCopy: () => {
235
- enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current, pathname.current));
440
+ enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current, currentPath));
236
441
  },
237
442
  trackException: (fields) => {
238
- enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current, pathname.current, fields));
443
+ enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current, currentPath, fields));
239
444
  },
240
445
  trackError: (error) => {
241
- enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current, pathname.current, error));
446
+ enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current, currentPath, error));
242
447
  },
243
448
  trackCTA: (ctaId, ctaVariant) => {
244
- enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current, pathname.current, ctaId, ctaVariant));
449
+ enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current, currentPath, ctaId, ctaVariant));
245
450
  },
246
451
  trackSearch: (query) => {
247
- enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, pathname.current, query));
452
+ enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
248
453
  }
249
454
  };
250
455
  }
251
456
 
252
457
  // src/components/Analytics.tsx
253
- function Analytics({ apiKey, apiUrl }) {
254
- const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey);
458
+ function Analytics({ apiKey, apiUrl, wsUrl }) {
459
+ const pathname = usePathname();
460
+ const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl);
255
461
  const lastTracked = useRef2(null);
256
462
  const lastScrollDepth = useRef2(0);
257
- const pathname = typeof window !== "undefined" ? window.location.pathname : "";
258
463
  useEffect2(() => {
259
464
  if (lastTracked.current === pathname) return;
260
465
  lastTracked.current = pathname;
466
+ lastScrollDepth.current = 0;
261
467
  trackPageview2(pathname);
262
468
  }, [pathname, trackPageview2]);
263
469
  useEffect2(() => {
@@ -269,12 +475,10 @@ function Analytics({ apiKey, apiUrl }) {
269
475
  useEffect2(() => {
270
476
  if (typeof window === "undefined") return;
271
477
  const handler = () => {
272
- const scrolled = window.scrollY;
273
478
  const total = document.documentElement.scrollHeight - window.innerHeight;
274
479
  if (total <= 0) return;
275
- const depth = Math.round(scrolled / total * 100);
276
- const milestones = [25, 50, 75, 100];
277
- for (const m of milestones) {
480
+ const depth = Math.round(window.scrollY / total * 100);
481
+ for (const m of [25, 50, 75, 100]) {
278
482
  if (depth >= m && lastScrollDepth.current < m) {
279
483
  lastScrollDepth.current = m;
280
484
  trackScroll2(m);
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 {
@@ -79,7 +79,6 @@ async function sendEvent(apiUrl, apiKey, payload) {
79
79
  async function trackPageview(apiUrl, apiKey, sessionId, path) {
80
80
  await sendEvent(apiUrl, apiKey, {
81
81
  ...BASE_FIELDS(sessionId, "pageview", path),
82
- page_url: `${typeof window !== "undefined" ? window.location.origin : ""}${path}`,
83
82
  page_title: typeof document !== "undefined" ? document.title || null : null,
84
83
  referrer: typeof document !== "undefined" ? document.referrer || null : null,
85
84
  scroll_depth: 0
@@ -145,6 +144,7 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
145
144
  let attempts = 0;
146
145
  let intentionalClose = false;
147
146
  let retryTimer = null;
147
+ let pingInterval = null;
148
148
  let targetUrl;
149
149
  try {
150
150
  const u = new URL(wsUrl);
@@ -155,6 +155,12 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
155
155
  return { disconnect: () => {
156
156
  } };
157
157
  }
158
+ function clearPing() {
159
+ if (pingInterval !== null) {
160
+ clearInterval(pingInterval);
161
+ pingInterval = null;
162
+ }
163
+ }
158
164
  function connect() {
159
165
  try {
160
166
  ws = new WebSocket(targetUrl);
@@ -164,8 +170,14 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
164
170
  ws.onopen = () => {
165
171
  attempts = 0;
166
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);
167
178
  };
168
179
  ws.onclose = () => {
180
+ clearPing();
169
181
  if (intentionalClose || attempts >= 3) return;
170
182
  const delay = Math.pow(2, attempts) * 1e3;
171
183
  attempts++;
@@ -176,11 +188,15 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
176
188
  return {
177
189
  disconnect: () => {
178
190
  intentionalClose = true;
191
+ clearPing();
179
192
  if (retryTimer !== null) {
180
193
  clearTimeout(retryTimer);
181
194
  retryTimer = null;
182
195
  }
183
196
  if (ws) {
197
+ if (ws.readyState === WebSocket.OPEN) {
198
+ ws.send(JSON.stringify({ type: "leave", session_id: sessionId }));
199
+ }
184
200
  ws.close();
185
201
  ws = null;
186
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arthurreira/analytics",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsup",