@arthurreira/analytics 0.17.0 → 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]");
@@ -110,6 +113,49 @@ var AfAnalytics = (() => {
110
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;
@@ -151,9 +197,17 @@ var AfAnalytics = (() => {
151
197
  touchSession();
152
198
  return stored;
153
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
+ }
154
206
  const id = await createRemoteSession(apiUrl, apiKey, getVisitorId());
155
- localStorage.setItem("af_session_id", id);
156
- 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
+ }
157
211
  return id;
158
212
  }
159
213
  function trackPageview(apiUrl, apiKey, sessionId) {
@@ -188,6 +242,9 @@ var AfAnalytics = (() => {
188
242
  const pending = [];
189
243
  let sessionEnded = false;
190
244
  let lastScrollDepth = 0;
245
+ let closeResponder = () => {
246
+ };
247
+ let idleTimer = null;
191
248
  function enqueue(fn) {
192
249
  if (sessionId) {
193
250
  touchSession();
@@ -196,9 +253,43 @@ var AfAnalytics = (() => {
196
253
  pending.push(fn);
197
254
  }
198
255
  }
199
- getOrCreateSession(apiUrl, apiKey).then((id) => {
256
+ function startSession(id) {
200
257
  sessionId = id;
258
+ closeResponder = openSessionResponder(id);
259
+ idleTimer = setInterval(touchSession, IDLE_TOUCH_INTERVAL_MS);
201
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);
202
293
  }).catch(() => {
203
294
  });
204
295
  enqueue((id) => trackPageview(apiUrl, apiKey, id));
@@ -263,12 +354,11 @@ var AfAnalytics = (() => {
263
354
  });
264
355
  });
265
356
  });
266
- const endSession = () => {
267
- if (!sessionId || sessionEnded) return;
268
- sessionEnded = true;
269
- navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end`);
270
- clearSession();
271
- };
357
+ window.addEventListener("storage", (e) => {
358
+ if (e.key === "af_session_id" && e.newValue === null && e.oldValue !== null) {
359
+ restartSession();
360
+ }
361
+ });
272
362
  let visHideTimer = null;
273
363
  document.addEventListener("visibilitychange", () => {
274
364
  if (document.visibilityState === "hidden") {
@@ -280,7 +370,17 @@ var AfAnalytics = (() => {
280
370
  }
281
371
  }
282
372
  });
283
- window.addEventListener("pagehide", endSession);
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
+ }
383
+ });
284
384
  window.addEventListener("beforeunload", endSession);
285
385
  }
286
386
  if (document.readyState === "loading") {
package/dist/client.js CHANGED
@@ -216,7 +216,51 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
216
216
 
217
217
  // src/hooks/useAnalytics.ts
218
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;
219
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
+ }
220
264
  async function getOrCreateSession(apiUrl, apiKey, visitorId) {
221
265
  const storedSessionId = localStorage.getItem("af_session_id");
222
266
  const lastActivity = localStorage.getItem("af_session_last_activity");
@@ -231,11 +275,20 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
231
275
  localStorage.removeItem("af_session_last_activity");
232
276
  }
233
277
  if (_sessionFlight) return _sessionFlight;
234
- _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);
235
286
  if (id) {
236
287
  localStorage.setItem("af_session_id", id);
237
288
  localStorage.setItem("af_session_last_activity", String(Date.now()));
238
289
  }
290
+ return id;
291
+ })().then((id) => {
239
292
  _sessionFlight = null;
240
293
  return id;
241
294
  }).catch((err) => {
@@ -249,6 +302,13 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
249
302
  const pendingEvents = useRef([]);
250
303
  const hasSentEnd = useRef(false);
251
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]);
252
312
  useEffect(() => {
253
313
  if (typeof window === "undefined") return;
254
314
  const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
@@ -257,6 +317,12 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
257
317
  getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
258
318
  if (cancelled || !id) return;
259
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);
260
326
  if (wsUrl) {
261
327
  presenceRef.current = connectPresence(apiKey, id, wsUrl);
262
328
  }
@@ -272,11 +338,61 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
272
338
  const sendEnd = () => {
273
339
  if (!sessionId.current || hasSentEnd.current) return;
274
340
  hasSentEnd.current = true;
341
+ if (idleTimerRef.current !== null) {
342
+ clearInterval(idleTimerRef.current);
343
+ idleTimerRef.current = null;
344
+ }
345
+ closeResponderRef.current();
275
346
  presenceRef.current?.disconnect();
347
+ presenceRef.current = null;
276
348
  navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end`);
277
349
  localStorage.removeItem("af_session_id");
278
350
  localStorage.removeItem("af_session_last_activity");
279
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
+ };
280
396
  let visHideTimer = null;
281
397
  const handleVisibility = () => {
282
398
  if (document.visibilityState === "hidden") {
@@ -288,13 +404,17 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
288
404
  }
289
405
  }
290
406
  };
407
+ window.addEventListener("storage", handleStorage);
291
408
  document.addEventListener("visibilitychange", handleVisibility);
292
- window.addEventListener("pagehide", sendEnd);
409
+ window.addEventListener("pagehide", handlePageHide);
410
+ window.addEventListener("pageshow", handlePageShow);
293
411
  window.addEventListener("beforeunload", sendEnd);
294
412
  return () => {
295
413
  if (visHideTimer !== null) clearTimeout(visHideTimer);
414
+ window.removeEventListener("storage", handleStorage);
296
415
  document.removeEventListener("visibilitychange", handleVisibility);
297
- window.removeEventListener("pagehide", sendEnd);
416
+ window.removeEventListener("pagehide", handlePageHide);
417
+ window.removeEventListener("pageshow", handlePageShow);
298
418
  window.removeEventListener("beforeunload", sendEnd);
299
419
  };
300
420
  }, [apiUrl]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arthurreira/analytics",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsup",