@faststats/web 0.12.1 → 0.13.1

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/dist/index.js CHANGED
@@ -1,62 +1,6 @@
1
- import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, r as getPageContext, t as sendData } from "./chunks/send-data-gBIF6DTb.js";
2
- import { a as touchActivity, i as resetSession, n as getSessionContext, t as createId } from "./chunks/session-manager-DUpF3mi3.js";
3
- import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-DOtve3E5.js";
4
- //#region src/extensions.ts
5
- var ExtensionHost = class {
6
- context;
7
- log;
8
- handlers = /* @__PURE__ */ new Map();
9
- cleanups = [];
10
- stopped = false;
11
- constructor(extensions, context, log) {
12
- this.context = context;
13
- this.log = log;
14
- for (const extension of extensions) this.setup(extension);
15
- }
16
- emit(hook, event) {
17
- if (this.stopped) return;
18
- for (const handler of this.handlers.get(hook) ?? []) try {
19
- handler(event)?.catch((error) => this.log(`${hook} failed`, error));
20
- } catch (error) {
21
- this.log(`${hook} failed`, error);
22
- }
23
- }
24
- stop(discard = false) {
25
- if (this.stopped) return;
26
- this.stopped = true;
27
- for (const cleanup of this.cleanups.reverse()) try {
28
- cleanup({ discard });
29
- } catch (error) {
30
- this.log("cleanup failed", error);
31
- }
32
- this.cleanups.length = 0;
33
- this.handlers.clear();
34
- }
35
- async setup(extension) {
36
- if (this.stopped) return;
37
- const subscriptions = [];
38
- try {
39
- const cleanup = await extension.setup({
40
- ...this.context,
41
- on: (hook, handler) => {
42
- if (this.stopped) return () => {};
43
- const handlers = this.handlers.get(hook) ?? /* @__PURE__ */ new Set();
44
- this.handlers.set(hook, handlers);
45
- handlers.add(handler);
46
- const off = () => handlers.delete(handler);
47
- subscriptions.push(off);
48
- return off;
49
- }
50
- });
51
- if (this.stopped) cleanup?.({ discard: true });
52
- else if (cleanup) this.cleanups.push(cleanup);
53
- } catch (error) {
54
- for (const off of subscriptions) off();
55
- this.log(`${extension.name} setup failed`, error);
56
- }
57
- }
58
- };
59
- //#endregion
1
+ import { a as resolveBaseUrl, i as getPageContext, n as URLS, o as sanitizeUrl, r as getClientContext, s as ExtensionHost, t as sendData } from "./chunks/send-data-B7P3f5PT.js";
2
+ import { a as touchActivity, i as resetSession, n as getSessionContext, t as createId } from "./chunks/session-manager-VM5RZj9H.js";
3
+ import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-3X8XI1pD.js";
60
4
  //#region src/outbound-links.ts
61
5
  const DEDUPE_MS = 500;
62
6
  function outboundHref(event) {
@@ -88,43 +32,36 @@ function trackOutboundLinks(report) {
88
32
  //#endregion
89
33
  //#region src/analytics.ts
90
34
  const navigationListeners = /* @__PURE__ */ new Set();
91
- const historyPatches = {};
92
- function patchHistory(method) {
93
- if (historyPatches[method]?.owner === history) return;
94
- const original = history[method];
95
- const wrapped = function(data, unused, url) {
96
- Reflect.apply(original, this, [
97
- data,
98
- unused,
99
- url
100
- ]);
101
- for (const listener of navigationListeners) listener();
102
- };
103
- history[method] = wrapped;
104
- historyPatches[method] = {
105
- owner: history,
106
- original,
107
- wrapped
108
- };
109
- }
35
+ const historyPatches = /* @__PURE__ */ new Map();
110
36
  function observeHistory(listener) {
111
37
  navigationListeners.add(listener);
112
- patchHistory("pushState");
113
- patchHistory("replaceState");
38
+ for (const method of ["pushState", "replaceState"]) {
39
+ if (historyPatches.get(method)?.target === history) continue;
40
+ const original = history[method];
41
+ const wrapped = function(...args) {
42
+ Reflect.apply(original, this, args);
43
+ for (const listener of navigationListeners) listener();
44
+ };
45
+ history[method] = wrapped;
46
+ historyPatches.set(method, {
47
+ target: history,
48
+ original,
49
+ wrapped
50
+ });
51
+ }
114
52
  return () => {
115
53
  navigationListeners.delete(listener);
116
54
  if (navigationListeners.size) return;
117
- for (const method of ["pushState", "replaceState"]) {
118
- const patch = historyPatches[method];
119
- if (patch && history[method] === patch.wrapped) {
120
- history[method] = patch.original;
121
- delete historyPatches[method];
122
- }
55
+ for (const [method, patch] of historyPatches) {
56
+ if (history[method] !== patch.wrapped) continue;
57
+ history[method] = patch.original;
58
+ historyPatches.delete(method);
123
59
  }
124
60
  };
125
61
  }
126
62
  const RESERVED_EVENTS = /* @__PURE__ */ new Set([
127
63
  "pageview",
64
+ "page_ping",
128
65
  "page_leave",
129
66
  "error",
130
67
  "outbound_link"
@@ -189,8 +126,9 @@ var WebAnalytics = class {
189
126
  context,
190
127
  duration: 0,
191
128
  startedAt: null,
192
- checkedAt: Date.now(),
193
- reportedDuration: -1
129
+ activeAt: Date.now(),
130
+ reportedDuration: 0,
131
+ beats: 0
194
132
  };
195
133
  this.send("pageview", {
196
134
  ...properties,
@@ -254,21 +192,19 @@ var WebAnalytics = class {
254
192
  setConsentMode(mode) {
255
193
  if (mode === this.consentMode) return;
256
194
  const wasTracking = this.canTrack();
257
- const wasCookieless = this.isCookieless();
258
- const willTrack = mode !== "denied";
259
- const willBeCookieless = this.options.cookieless === true || mode === "anonymous";
260
- if (wasTracking && willTrack && wasCookieless !== willBeCookieless) this.leavePage();
195
+ const identityChanges = this.isCookieless() !== this.isCookieless(mode);
196
+ if (mode !== "denied" && identityChanges) this.leavePage();
261
197
  this.consentMode = mode;
262
- if (!willTrack) {
198
+ if (mode === "denied") {
263
199
  this.leavePage();
264
200
  this.pendingPageview = null;
265
201
  this.stopExtensions(true);
266
- return;
202
+ } else if (!wasTracking) {
203
+ if (this.state === "started") this.activate("consent");
204
+ } else {
205
+ this.extensions?.emit("consentChange", { cookieless: this.isCookieless() });
206
+ if (identityChanges) this.pageview({ trigger: "consent" });
267
207
  }
268
- if (wasTracking) {
269
- this.extensions?.emit("consentChange", { cookieless: willBeCookieless });
270
- if (wasCookieless !== willBeCookieless) this.pageview({ trigger: "consent" });
271
- } else if (this.state === "started") this.activate("consent");
272
208
  }
273
209
  getAnonymousId() {
274
210
  return this.canTrack() ? getAnonymousId(this.options.siteKey, this.isCookieless()) : "";
@@ -280,8 +216,8 @@ var WebAnalytics = class {
280
216
  canTrack() {
281
217
  return this.consentMode !== "denied";
282
218
  }
283
- isCookieless() {
284
- return this.options.cookieless === true || this.consentMode === "anonymous";
219
+ isCookieless(mode = this.consentMode) {
220
+ return this.options.cookieless === true || mode === "anonymous";
285
221
  }
286
222
  isActive() {
287
223
  return this.state === "started" && this.canTrack();
@@ -291,6 +227,7 @@ var WebAnalytics = class {
291
227
  }
292
228
  context(session = this.session()) {
293
229
  return {
230
+ ...getClientContext(),
294
231
  ...getPageContext(this.options.trackHash),
295
232
  userId: this.getAnonymousId() || void 0,
296
233
  sessionId: session.sessionId,
@@ -324,7 +261,7 @@ var WebAnalytics = class {
324
261
  this.bind(window, "pagehide", this.onPageHide);
325
262
  this.bind(window, "pageshow", this.onPageShow);
326
263
  this.bind(window, "focus", this.onResume);
327
- this.bind(window, "blur", () => this.pauseVisit());
264
+ this.bind(window, "blur", this.onBlur);
328
265
  this.bind(window, "popstate", this.onNavigate);
329
266
  for (const type of [
330
267
  "pointerdown",
@@ -341,7 +278,6 @@ var WebAnalytics = class {
341
278
  }
342
279
  onNavigate = () => this.pageview({ trigger: "navigation" });
343
280
  onVisibilityChange = () => {
344
- if (!this.canTrack()) return;
345
281
  if (document.visibilityState === "hidden") {
346
282
  this.pauseVisit();
347
283
  this.extensions?.emit("pageHide", {
@@ -353,17 +289,15 @@ var WebAnalytics = class {
353
289
  this.onResume();
354
290
  };
355
291
  onPageHide = (event) => {
356
- if (!this.canTrack()) return;
357
292
  const persisted = event.persisted;
358
- this.pauseVisit();
293
+ if (persisted) this.pauseVisit();
294
+ else this.leavePage();
359
295
  this.extensions?.emit("pageHide", {
360
296
  persisted,
361
297
  terminal: !persisted
362
298
  });
363
- if (!persisted) this.leavePage();
364
299
  };
365
300
  onPageShow = (event) => {
366
- if (!this.canTrack()) return;
367
301
  this.onResume();
368
302
  this.extensions?.emit("pageShow", { persisted: event.persisted });
369
303
  };
@@ -371,6 +305,13 @@ var WebAnalytics = class {
371
305
  this.pageview(this.pendingPageview ?? { trigger: "session" });
372
306
  this.resumeVisit();
373
307
  };
308
+ onBlur = () => {
309
+ const visit = this.visit;
310
+ const blurredAt = performance.now();
311
+ setTimeout(() => {
312
+ if (this.visit === visit && !document.hasFocus()) this.pauseVisit(false, blurredAt);
313
+ });
314
+ };
374
315
  onActivity = () => {
375
316
  this.lastInputAt = performance.now();
376
317
  if (this.visit?.startedAt === null && document.hasFocus()) this.onResume();
@@ -378,47 +319,44 @@ var WebAnalytics = class {
378
319
  isIdle() {
379
320
  const now = performance.now();
380
321
  if (now - this.lastInputAt < IDLE_MS) return false;
381
- const playing = [...document.querySelectorAll("video,audio")].some((media) => !media.paused && !media.ended);
382
- if (playing) this.lastInputAt = now;
383
- return !playing;
322
+ const engaged = document.activeElement?.tagName === "IFRAME" || [...document.querySelectorAll("video,audio")].some((media) => !media.paused && !media.ended);
323
+ if (engaged) this.lastInputAt = now;
324
+ return !engaged;
384
325
  }
385
326
  leavePage() {
386
- this.pauseVisit();
327
+ this.pauseVisit(true);
387
328
  this.visit = null;
388
329
  }
389
- checkpoint() {
330
+ checkpoint(leaving = false, periodic = false, now = performance.now()) {
390
331
  const visit = this.visit;
391
332
  if (!visit || !this.isActive()) return;
392
- let activeAt = visit.checkedAt;
393
333
  if (visit.startedAt !== null) {
394
- const now = performance.now();
395
- if (now - visit.startedAt < IDLE_MS) {
396
- const active = Math.min(now, this.lastInputAt + IDLE_MS) - visit.startedAt;
397
- visit.duration += active;
398
- activeAt += active;
334
+ const activeUntil = Math.min(now, this.lastInputAt + IDLE_MS);
335
+ if (now - visit.startedAt < IDLE_MS && activeUntil > visit.startedAt) {
336
+ visit.duration += activeUntil - visit.startedAt;
337
+ visit.activeAt = Date.now() - (performance.now() - activeUntil);
399
338
  }
400
339
  visit.startedAt = now;
401
- visit.checkedAt = Date.now();
402
340
  }
341
+ if (periodic && ++visit.beats !== 1 && visit.beats % 4 !== 3) return;
403
342
  const duration = Math.round(visit.duration);
404
- if (duration === visit.reportedDuration) return;
343
+ if (!leaving && duration === visit.reportedDuration) return;
405
344
  visit.reportedDuration = duration;
406
- this.send("page_leave", {
345
+ this.send(leaving ? "page_leave" : "page_ping", {
407
346
  visit_id: visit.id,
408
347
  time_on_page: duration,
409
- session_duration: Math.max(0, activeAt - visit.session.sessionStart)
348
+ session_duration: Math.max(0, visit.activeAt - visit.session.sessionStart)
410
349
  }, visit.context);
411
350
  }
412
- pauseVisit() {
351
+ pauseVisit(leaving = false, at) {
413
352
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
414
353
  this.heartbeatTimer = null;
415
- this.checkpoint();
354
+ this.checkpoint(leaving, false, at);
416
355
  if (this.visit) this.visit.startedAt = null;
417
356
  }
418
357
  resumeVisit() {
419
358
  if (!this.isActive() || !this.visit || this.visit.startedAt !== null || document.visibilityState !== "visible" || !document.hasFocus()) return;
420
359
  this.visit.startedAt = this.lastInputAt = performance.now();
421
- this.visit.checkedAt = Date.now();
422
360
  this.heartbeatTimer = setInterval(() => {
423
361
  if (document.visibilityState !== "visible" || !document.hasFocus() || this.isIdle()) {
424
362
  this.pauseVisit();
@@ -426,12 +364,15 @@ var WebAnalytics = class {
426
364
  }
427
365
  const visit = this.visit;
428
366
  this.pageview({ trigger: "session" });
429
- if (this.visit === visit) this.checkpoint();
367
+ if (this.visit === visit) this.checkpoint(false, true);
430
368
  }, 15e3);
431
369
  }
432
370
  send(event, properties = {}, context) {
433
371
  if (!this.isActive()) return;
434
- context ??= this.context();
372
+ context ??= {
373
+ ...this.context(),
374
+ visit_id: this.visit?.id
375
+ };
435
376
  let data;
436
377
  try {
437
378
  data = JSON.stringify({
package/dist/replay.d.ts CHANGED
@@ -57,81 +57,54 @@ export type SessionReplayExtensionOptions = Omit<ReplayTrackerOptions, "siteKey"
57
57
  export declare function sessionReplay(options?: SessionReplayExtensionOptions): AnalyticsExtension;
58
58
  export default class ReplayTracker {
59
59
  private readonly options;
60
- private readonly endpoint;
61
- private readonly compress;
62
- private readonly flushInterval;
63
- private readonly maxEvents;
64
- private readonly maxBatchSizeBytes;
65
- private readonly maxLowLatencyBatchSizeBytes;
66
- private readonly maxPendingBatches;
67
- private readonly maxQueueSizeBytes;
68
- private readonly minReplayLengthMs;
69
- private readonly events;
70
- private readonly pending;
71
- private queuedEventsSizeBytes;
72
- private pendingSizeBytes;
73
- private viewId;
74
- private viewUrl;
75
- private chunkStartedAt;
60
+ private readonly limits;
61
+ private readonly uploader;
62
+ private readonly throttler;
63
+ private cookieless;
76
64
  private started;
77
- private disposed;
78
- private startTime;
79
- private intervalId;
80
- private flushTask;
81
- private retryTask;
82
- private retryAttempt;
83
- private minLengthFlushTask;
65
+ private overflowed;
84
66
  private stopRecording?;
85
- private activeSessionId;
86
- private sending;
87
- private queuedFlush;
88
- private unsubscribeRotation?;
89
- private lastCookielessMode;
90
- private needsFinalization;
91
- private initialFlushPending;
67
+ private stopFollowing?;
68
+ private intervalTimer?;
69
+ private flushTimer?;
70
+ private minLengthTimer?;
71
+ private viewId;
72
+ private viewUrl;
73
+ private session;
74
+ private streamStart;
75
+ private chunkStart;
76
+ private sequence;
77
+ private events;
78
+ private eventBytes;
92
79
  private resumed;
93
- private overflowed;
94
- private replayEventSequence;
95
- private readonly mutationThrottler;
80
+ private unfinalized;
96
81
  constructor(options: ReplayTrackerOptions);
97
- private log;
98
- private sessionContext;
99
- private sanitize;
100
- private clearFlushTimers;
101
- private dropEvents;
102
- private clearQueues;
103
- private finalizeSession;
104
- setCookielessMode(cookieless: boolean): void;
105
82
  start(): void;
83
+ stop(discard?: boolean): void;
84
+ /** Nothing captured under the old identity may be sent under the new one. */
85
+ setCookielessMode(cookieless: boolean): void;
106
86
  trackPageChange(url?: string): void;
107
87
  onPageHidden(persisted?: boolean, terminal?: boolean): void;
108
88
  onPageShow(persisted?: boolean): void;
109
- private recordingPlugins;
89
+ private log;
90
+ private sanitize;
91
+ private currentSession;
92
+ private followSession;
110
93
  private beginRecording;
111
- stop(discard?: boolean): void;
112
- private onSessionRotated;
113
- private subscribeToSessionRotation;
114
- private applySessionRotation;
115
- private beginReplayStream;
116
- private resetSessionTiming;
117
- private onEvent;
118
- private requestTerminalFlush;
119
- private hasReachedMinLength;
120
- private requestFlush;
121
- private scheduleMinLengthFlush;
122
- private enqueueEventsIfReady;
123
- private takeEventBatches;
124
- private createBatch;
125
- private eventSizeBytes;
126
- private batchSizeBytes;
127
- private removePending;
94
+ private recordingPlugins;
95
+ private takeFullSnapshot;
128
96
  private stopCapture;
129
- private enqueueBatch;
97
+ private readonly onEvent;
98
+ private beginStream;
99
+ private rotate;
100
+ private reachedMinLength;
101
+ private dropEvents;
102
+ private requestFlush;
130
103
  private flush;
131
- private sendPending;
132
- private scheduleRetry;
133
- private retryDelay;
134
- private sendBatch;
104
+ private finish;
105
+ private batchEvents;
106
+ private endStream;
107
+ private enqueue;
135
108
  }
136
109
  //#endregion
137
110
  export {};