@faststats/web 0.12.1 → 0.13.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/README.md CHANGED
@@ -48,8 +48,8 @@ Consent remains client-local: use `"anonymous"` for cookieless collection,
48
48
 
49
49
  ### Visit duration
50
50
 
51
- `page_leave.properties.time_on_page` is the accumulated time in milliseconds
52
- that the page was visible and its document had focus. It pauses when the tab is
51
+ `properties.time_on_page` on `page_ping` and `page_leave` is the accumulated
52
+ time in milliseconds that the page was visible and its document had focus. It pauses when the tab is
53
53
  hidden, the window loses focus, or the page enters the back-forward cache, and
54
54
  resets on navigation, logout, or session rotation. A monotonic clock keeps
55
55
  system-clock changes from affecting this duration. It also pauses after 60
@@ -60,11 +60,24 @@ the device spent asleep or the tab frozen is not counted.
60
60
  Pageviews wait until the page becomes visible. Pages replaced or closed before
61
61
  being shown do not emit pageviews or page-leave events.
62
62
 
63
- The SDK sends cumulative `page_leave` checkpoints every 15 seconds while active
64
- and when activity pauses or the visit ends. Unchanged checkpoints are suppressed.
65
- Pageviews and their checkpoints share `properties.visit_id`; consumers should use
66
- the maximum `time_on_page` per visit, never sum its checkpoints. The existing
67
- session-duration and bounce-rate queries already use maximum values.
63
+ The SDK sends a cumulative `page_ping` when activity pauses and periodically
64
+ while active: after 15 and 45 seconds, then every 60 seconds. A ping that adds no
65
+ time is suppressed. A visit ends with
66
+ exactly one `page_leave`, on navigation, a terminal `pagehide`, logout, a consent
67
+ change, or session rotation. A page the browser discards without notice never
68
+ sends one, so its last ping holds its duration. A pageview, its pings, and its
69
+ leave share `properties.visit_id`; consumers should use the maximum
70
+ `time_on_page` per visit across both events, never sum them.
71
+
72
+ Before 0.13 the pings were sent as `page_leave` every 15 seconds. The collector
73
+ and dashboards read both shapes; deploy them before releasing this SDK.
74
+
75
+ ### Default context
76
+
77
+ Every event carries `timezone` (IANA name), `language`, and `screen`
78
+ (`1920x1080`, CSS pixels) beside the page context, separate from its properties.
79
+ Custom events and outbound links also carry the `visit_id` of the page view they
80
+ happened during.
68
81
 
69
82
  Each visit retains its original identity, session, and page metadata. A session
70
83
  or identity change starts a new visit, even on the same route. Both persistent
@@ -76,7 +89,7 @@ session, so it is separate from active time on the page. An idle visitor stops
76
89
  extending the session, which then expires after 30 minutes like any other.
77
90
 
78
91
  Delivery is best effort: a sudden process kill or network failure can lose time
79
- since the last delivered checkpoint. The collector must accept successive events
92
+ since the last delivered ping. The collector must accept successive events
80
93
  for the same visitor and URL; deploy the removal of its five-second debounce
81
94
  before releasing this SDK.
82
95
 
@@ -31,6 +31,19 @@ function getReferrer() {
31
31
  } catch {}
32
32
  return sanitizeUrl(raw);
33
33
  }
34
+ /** Visitor environment sent with every event; unavailable values are omitted. */
35
+ function getClientContext() {
36
+ let timezone;
37
+ try {
38
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
39
+ } catch {}
40
+ const { width, height } = globalThis.screen ?? {};
41
+ return {
42
+ timezone,
43
+ language: globalThis.navigator?.language,
44
+ screen: width && height ? `${width}x${height}` : void 0
45
+ };
46
+ }
34
47
  function getPageContext(includeHash = false) {
35
48
  const hash = includeHash ? location.hash : "";
36
49
  const ctx = {
@@ -80,4 +93,4 @@ async function sendData(options) {
80
93
  }
81
94
  }
82
95
  //#endregion
83
- export { sanitizeUrl as a, resolveBaseUrl as i, URLS as n, getPageContext as r, sendData as t };
96
+ export { resolveBaseUrl as a, getPageContext as i, URLS as n, sanitizeUrl as o, getClientContext as r, sendData as t };
package/dist/error.js CHANGED
@@ -1,9 +1,9 @@
1
- import { i as resolveBaseUrl, n as URLS, r as getPageContext, t as sendData } from "./chunks/send-data-gBIF6DTb.js";
1
+ import { a as resolveBaseUrl, i as getPageContext, n as URLS, t as sendData } from "./chunks/send-data-C76HbZ8d.js";
2
2
  import { n as getSessionContext } from "./chunks/session-manager-DUpF3mi3.js";
3
3
  import { t as getAnonymousId } from "./chunks/identifiers-DOtve3E5.js";
4
4
  //#region src/error.ts
5
5
  const SDK_NAME = "@faststats/web";
6
- const SDK_VERSION = "0.12.1";
6
+ const SDK_VERSION = "0.13.0";
7
7
  function errorTracking(options = {}) {
8
8
  return {
9
9
  name: "error-tracking",
package/dist/index.d.ts CHANGED
@@ -69,6 +69,10 @@ export declare class WebAnalytics {
69
69
  private readonly onActivity;
70
70
  private isIdle;
71
71
  private leavePage;
72
+ /**
73
+ * Reports the visit's running totals: as a `page_ping` while it may still
74
+ * continue, and as its single `page_leave` once it has ended.
75
+ */
72
76
  private checkpoint;
73
77
  private pauseVisit;
74
78
  private resumeVisit;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, r as getPageContext, t as sendData } from "./chunks/send-data-gBIF6DTb.js";
1
+ import { a as resolveBaseUrl, i as getPageContext, n as URLS, o as sanitizeUrl, r as getClientContext, t as sendData } from "./chunks/send-data-C76HbZ8d.js";
2
2
  import { a as touchActivity, i as resetSession, n as getSessionContext, t as createId } from "./chunks/session-manager-DUpF3mi3.js";
3
3
  import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-DOtve3E5.js";
4
4
  //#region src/extensions.ts
@@ -125,6 +125,7 @@ function observeHistory(listener) {
125
125
  }
126
126
  const RESERVED_EVENTS = /* @__PURE__ */ new Set([
127
127
  "pageview",
128
+ "page_ping",
128
129
  "page_leave",
129
130
  "error",
130
131
  "outbound_link"
@@ -190,7 +191,9 @@ var WebAnalytics = class {
190
191
  duration: 0,
191
192
  startedAt: null,
192
193
  checkedAt: Date.now(),
193
- reportedDuration: -1
194
+ activeAt: Date.now(),
195
+ reportedDuration: -1,
196
+ beats: 0
194
197
  };
195
198
  this.send("pageview", {
196
199
  ...properties,
@@ -291,6 +294,7 @@ var WebAnalytics = class {
291
294
  }
292
295
  context(session = this.session()) {
293
296
  return {
297
+ ...getClientContext(),
294
298
  ...getPageContext(this.options.trackHash),
295
299
  userId: this.getAnonymousId() || void 0,
296
300
  sessionId: session.sessionId,
@@ -355,12 +359,12 @@ var WebAnalytics = class {
355
359
  onPageHide = (event) => {
356
360
  if (!this.canTrack()) return;
357
361
  const persisted = event.persisted;
358
- this.pauseVisit();
362
+ if (persisted) this.pauseVisit();
363
+ else this.leavePage();
359
364
  this.extensions?.emit("pageHide", {
360
365
  persisted,
361
366
  terminal: !persisted
362
367
  });
363
- if (!persisted) this.leavePage();
364
368
  };
365
369
  onPageShow = (event) => {
366
370
  if (!this.canTrack()) return;
@@ -383,36 +387,40 @@ var WebAnalytics = class {
383
387
  return !playing;
384
388
  }
385
389
  leavePage() {
386
- this.pauseVisit();
390
+ this.pauseVisit(true);
387
391
  this.visit = null;
388
392
  }
389
- checkpoint() {
393
+ /**
394
+ * Reports the visit's running totals: as a `page_ping` while it may still
395
+ * continue, and as its single `page_leave` once it has ended.
396
+ */
397
+ checkpoint(leaving = false, periodic = false) {
390
398
  const visit = this.visit;
391
399
  if (!visit || !this.isActive()) return;
392
- let activeAt = visit.checkedAt;
393
400
  if (visit.startedAt !== null) {
394
401
  const now = performance.now();
395
402
  if (now - visit.startedAt < IDLE_MS) {
396
403
  const active = Math.min(now, this.lastInputAt + IDLE_MS) - visit.startedAt;
397
404
  visit.duration += active;
398
- activeAt += active;
405
+ visit.activeAt = visit.checkedAt + active;
399
406
  }
400
407
  visit.startedAt = now;
401
408
  visit.checkedAt = Date.now();
402
409
  }
410
+ if (periodic && ++visit.beats !== 1 && visit.beats % 4 !== 3) return;
403
411
  const duration = Math.round(visit.duration);
404
- if (duration === visit.reportedDuration) return;
412
+ if (!leaving && duration === visit.reportedDuration) return;
405
413
  visit.reportedDuration = duration;
406
- this.send("page_leave", {
414
+ this.send(leaving ? "page_leave" : "page_ping", {
407
415
  visit_id: visit.id,
408
416
  time_on_page: duration,
409
- session_duration: Math.max(0, activeAt - visit.session.sessionStart)
417
+ session_duration: Math.max(0, visit.activeAt - visit.session.sessionStart)
410
418
  }, visit.context);
411
419
  }
412
- pauseVisit() {
420
+ pauseVisit(leaving = false) {
413
421
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
414
422
  this.heartbeatTimer = null;
415
- this.checkpoint();
423
+ this.checkpoint(leaving);
416
424
  if (this.visit) this.visit.startedAt = null;
417
425
  }
418
426
  resumeVisit() {
@@ -426,12 +434,15 @@ var WebAnalytics = class {
426
434
  }
427
435
  const visit = this.visit;
428
436
  this.pageview({ trigger: "session" });
429
- if (this.visit === visit) this.checkpoint();
437
+ if (this.visit === visit) this.checkpoint(false, true);
430
438
  }, 15e3);
431
439
  }
432
440
  send(event, properties = {}, context) {
433
441
  if (!this.isActive()) return;
434
- context ??= this.context();
442
+ context ??= {
443
+ ...this.context(),
444
+ visit_id: this.visit?.id
445
+ };
435
446
  let data;
436
447
  try {
437
448
  data = JSON.stringify({
package/dist/replay.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as sendData } from "./chunks/send-data-gBIF6DTb.js";
1
+ import { a as resolveBaseUrl, n as URLS, o as sanitizeUrl, t as sendData } from "./chunks/send-data-C76HbZ8d.js";
2
2
  import { a as touchActivity, c as setStorageItem, n as getSessionContext, o as getStorageItem, r as onSessionRotated, t as createId } from "./chunks/session-manager-DUpF3mi3.js";
3
3
  import { t as getAnonymousId } from "./chunks/identifiers-DOtve3E5.js";
4
4
  import { record } from "@rrweb/record";
@@ -1,4 +1,4 @@
1
- import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as sendData } from "./chunks/send-data-gBIF6DTb.js";
1
+ import { a as resolveBaseUrl, n as URLS, o as sanitizeUrl, t as sendData } from "./chunks/send-data-C76HbZ8d.js";
2
2
  import { n as getSessionContext, r as onSessionRotated } from "./chunks/session-manager-DUpF3mi3.js";
3
3
  import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals/attribution";
4
4
  //#region src/utils/web-vitals-collector.ts
package/package.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "version": "0.12.1",
45
+ "version": "0.13.0",
46
46
  "scripts": {
47
47
  "build": "tsdown && bun run check-size",
48
48
  "dev": "bun run build",