@faststats/web 0.8.2 → 0.9.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
@@ -19,7 +19,7 @@ init({
19
19
  extensions: [
20
20
  outboundLinks(),
21
21
  errorTracking(),
22
- webVitals({ attribution: true }),
22
+ webVitals(),
23
23
  sessionReplay({ recordConsole: true }),
24
24
  ],
25
25
  });
package/dist/error.js CHANGED
@@ -3,7 +3,7 @@ import { r as getSessionContext, t as sendData } from "./chunks/send-data-Dplz7m
3
3
  import { t as getAnonymousId } from "./chunks/identifiers-xX7o7oZz.js";
4
4
  //#region src/error.ts
5
5
  const SDK_NAME = "@faststats/web";
6
- const SDK_VERSION = "0.8.2";
6
+ const SDK_VERSION = "0.9.0";
7
7
  function errorTracking(options = {}) {
8
8
  return {
9
9
  name: "error-tracking",
package/dist/replay.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
2
2
  import { record } from "@rrweb/record";
3
3
  import { LogLevel } from "@rrweb/rrweb-plugin-console-record";
4
- //#region ../../node_modules/.bun/@rrweb+types@2.1.1/node_modules/@rrweb/types/dist/index.d.ts
4
+ //#region ../../node_modules/.bun/@rrweb+types@2.1.4/node_modules/@rrweb/types/dist/index.d.ts
5
5
  declare global {
6
6
  interface Window {
7
7
  FontFace: typeof FontFace;
@@ -4,27 +4,29 @@ export interface WebVitalsOptions {
4
4
  siteKey: string;
5
5
  baseUrl?: string;
6
6
  debug?: boolean;
7
- attribution?: boolean;
8
7
  trackHash?: boolean;
9
8
  cookieless?: boolean;
10
9
  }
11
- export type WebVitalsExtensionOptions = Pick<WebVitalsOptions, "attribution">;
12
- export declare function webVitals(options?: WebVitalsExtensionOptions): AnalyticsExtension;
10
+ export declare function webVitals(): AnalyticsExtension;
13
11
  export default class WebVitalsTracker {
14
12
  private readonly options;
15
13
  private readonly endpoint;
16
14
  private metrics;
17
15
  private started;
18
- private flushing;
16
+ private sending?;
17
+ private generation;
18
+ private unsubscribe?;
19
+ private readonly sentMetrics;
19
20
  private initialUrl;
20
21
  private session;
21
22
  private cookieless;
22
23
  constructor(options: WebVitalsOptions);
23
24
  setCookielessMode(cookieless: boolean): void;
24
- start(): Promise<void>;
25
+ start(): void;
25
26
  stop(discard?: boolean): void;
26
27
  onPageHidden(persisted?: boolean): void;
27
28
  private onMetric;
29
+ private discard;
28
30
  private flush;
29
31
  }
30
32
  //#endregion
@@ -1,12 +1,43 @@
1
1
  import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
2
2
  import { r as getSessionContext, t as sendData } from "./chunks/send-data-Dplz7m4c.js";
3
+ import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals/attribution";
4
+ //#region src/utils/web-vitals-collector.ts
5
+ const collectors = /* @__PURE__ */ new WeakMap();
6
+ function subscribeToVitals(subscriber) {
7
+ let collector = collectors.get(document);
8
+ if (!collector) {
9
+ collector = {
10
+ subscribers: /* @__PURE__ */ new Set(),
11
+ latest: /* @__PURE__ */ new Map()
12
+ };
13
+ collectors.set(document, collector);
14
+ const { latest, subscribers } = collector;
15
+ const report = (metric) => {
16
+ const snapshot = { ...metric };
17
+ snapshot.attribution = { ...metric.attribution };
18
+ latest.set(metric.name, snapshot);
19
+ for (const listener of subscribers) listener(snapshot);
20
+ };
21
+ const changes = { reportAllChanges: true };
22
+ onCLS(report, changes);
23
+ onINP(report, changes);
24
+ onLCP(report, changes);
25
+ onFCP(report);
26
+ onTTFB(report);
27
+ }
28
+ collector.subscribers.add(subscriber);
29
+ for (const metric of collector.latest.values()) subscriber(metric);
30
+ return () => {
31
+ collector.subscribers.delete(subscriber);
32
+ };
33
+ }
34
+ //#endregion
3
35
  //#region src/web-vitals.ts
4
- function webVitals(options = {}) {
36
+ function webVitals() {
5
37
  return {
6
38
  name: "web-vitals",
7
- async setup(context) {
39
+ setup(context) {
8
40
  const tracker = new WebVitalsTracker({
9
- ...options,
10
41
  siteKey: context.siteKey,
11
42
  baseUrl: context.baseUrl,
12
43
  debug: context.debug,
@@ -15,7 +46,7 @@ function webVitals(options = {}) {
15
46
  });
16
47
  context.on("pageHide", ({ persisted }) => tracker.onPageHidden(persisted));
17
48
  context.on("consentChange", ({ cookieless }) => tracker.setCookielessMode(cookieless));
18
- await tracker.start();
49
+ tracker.start();
19
50
  return ({ discard }) => tracker.stop(discard);
20
51
  }
21
52
  };
@@ -32,7 +63,10 @@ var WebVitalsTracker = class {
32
63
  endpoint;
33
64
  metrics = /* @__PURE__ */ new Map();
34
65
  started = false;
35
- flushing = false;
66
+ sending;
67
+ generation = 0;
68
+ unsubscribe;
69
+ sentMetrics = /* @__PURE__ */ new Map();
36
70
  initialUrl = "";
37
71
  session = null;
38
72
  cookieless;
@@ -44,77 +78,83 @@ var WebVitalsTracker = class {
44
78
  setCookielessMode(cookieless) {
45
79
  if (this.cookieless === cookieless) return;
46
80
  this.cookieless = cookieless;
47
- this.metrics.clear();
81
+ this.discard();
48
82
  this.session = this.started ? getSessionContext(this.options.siteKey, cookieless) : null;
49
83
  }
50
- async start() {
84
+ start() {
51
85
  if (this.started || typeof window === "undefined") return;
52
86
  this.started = true;
53
- this.initialUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
87
+ this.initialUrl = sanitizeUrl(window.performance?.getEntriesByType("navigation")[0]?.name || window.location.href, this.options.trackHash ?? false);
54
88
  this.session = getSessionContext(this.options.siteKey, this.cookieless);
55
- const vitals = this.options.attribution ? await import("web-vitals/attribution") : await import("web-vitals");
56
- if (!this.started) return;
57
- const changes = { reportAllChanges: true };
58
- vitals.onCLS(this.onMetric, changes);
59
- vitals.onINP(this.onMetric, changes);
60
- vitals.onLCP(this.onMetric, changes);
61
- vitals.onFCP(this.onMetric);
62
- vitals.onTTFB(this.onMetric);
89
+ this.unsubscribe = subscribeToVitals(this.onMetric);
63
90
  }
64
91
  stop(discard = false) {
65
92
  if (!this.started) return;
66
93
  this.started = false;
67
- if (discard) this.metrics.clear();
94
+ this.unsubscribe?.();
95
+ this.unsubscribe = void 0;
96
+ if (discard) this.discard();
68
97
  else this.flush();
69
98
  }
70
99
  onPageHidden(persisted = false) {
71
100
  if (persisted) return;
72
- queueMicrotask(() => void this.flush());
101
+ const generation = this.generation;
102
+ queueMicrotask(() => {
103
+ if (generation === this.generation) this.flush();
104
+ });
73
105
  }
74
106
  onMetric = (metric) => {
75
107
  if (!this.started) return;
76
108
  const name = metric.name;
77
109
  if (!METRIC_NAMES.has(name) || !Number.isFinite(metric.value) || metric.value < 0) return;
78
- const { id, rating, delta, navigationType } = metric;
79
- const attribution = this.options.attribution && "attribution" in metric && metric.attribution ? metric.attribution : void 0;
80
- this.metrics.set(name, {
81
- metric: name,
82
- value: metric.value,
83
- attributes: {
84
- id,
85
- rating,
86
- delta,
87
- navigationType,
88
- ...attribution
89
- }
90
- });
110
+ this.metrics.set(name, metric);
91
111
  };
112
+ discard() {
113
+ this.generation++;
114
+ this.metrics.clear();
115
+ this.sentMetrics.clear();
116
+ }
92
117
  async flush() {
93
- if (!this.metrics.size || this.flushing) return;
94
- this.flushing = true;
118
+ const generation = this.generation;
119
+ while (this.sending) await this.sending;
120
+ if (generation !== this.generation || !this.metrics.size) return;
95
121
  const batch = this.metrics;
96
- this.metrics = /* @__PURE__ */ new Map();
97
122
  const session = this.session ?? getSessionContext(this.options.siteKey, this.cookieless);
98
- let sent = false;
99
- try {
100
- sent = await sendData({
101
- url: this.endpoint,
102
- data: JSON.stringify({
103
- token: this.options.siteKey,
104
- sessionId: session.sessionId,
105
- windowId: session.windowId,
106
- vitals: [...batch.values()],
107
- metadata: { url: this.initialUrl }
108
- }),
109
- debug: this.options.debug,
110
- debugPrefix: "[WebVitals]"
111
- });
112
- } finally {
113
- if (!sent) {
114
- for (const [name, vital] of batch) if (!this.metrics.has(name)) this.metrics.set(name, vital);
115
- }
116
- this.flushing = false;
117
- }
123
+ const data = JSON.stringify({
124
+ token: this.options.siteKey,
125
+ sessionId: session.sessionId,
126
+ windowId: session.windowId,
127
+ vitals: [...batch.values()].map(({ name, value, id, rating, navigationType, attribution }) => {
128
+ const previous = this.sentMetrics.get(name);
129
+ return {
130
+ metric: name,
131
+ value,
132
+ attributes: {
133
+ ...attribution,
134
+ id,
135
+ rating,
136
+ navigationType,
137
+ delta: value - (previous?.id === id ? previous.value : 0)
138
+ }
139
+ };
140
+ }),
141
+ metadata: { url: this.initialUrl }
142
+ });
143
+ this.metrics = /* @__PURE__ */ new Map();
144
+ this.sending = sendData({
145
+ url: this.endpoint,
146
+ data,
147
+ debug: this.options.debug,
148
+ debugPrefix: "[WebVitals]"
149
+ });
150
+ const sent = await this.sending;
151
+ this.sending = void 0;
152
+ if (generation !== this.generation) return;
153
+ for (const [name, vital] of batch) if (sent) this.sentMetrics.set(name, {
154
+ id: vital.id,
155
+ value: vital.value
156
+ });
157
+ else if (!this.metrics.has(name)) this.metrics.set(name, vital);
118
158
  }
119
159
  };
120
160
  //#endregion
package/package.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "version": "0.8.2",
45
+ "version": "0.9.0",
46
46
  "scripts": {
47
47
  "build": "tsdown && bun run check-size",
48
48
  "dev": "bun run build",
@@ -51,14 +51,14 @@
51
51
  "check-size": "node scripts/check-bundle-size.mjs"
52
52
  },
53
53
  "devDependencies": {
54
- "@rrweb/types": "^2.1.1",
55
- "@types/bun": "^1.4.0",
54
+ "@rrweb/types": "^2.1.4",
55
+ "@types/bun": "^1.4.2",
56
56
  "tsdown": "^0.23.0",
57
- "typescript": "^6.0.3"
57
+ "typescript": "^7.0.2"
58
58
  },
59
59
  "dependencies": {
60
- "@rrweb/record": "^2.1.1",
61
- "@rrweb/rrweb-plugin-console-record": "^2.1.1",
60
+ "@rrweb/record": "^2.1.4",
61
+ "@rrweb/rrweb-plugin-console-record": "^2.1.4",
62
62
  "web-vitals": "^6.2.1"
63
63
  }
64
64
  }