@faststats/web 0.2.14 → 0.3.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.
Files changed (53) hide show
  1. package/dist/chunks/api-urls-nKmJnyjD.js +1 -0
  2. package/dist/chunks/identifiers-DrjjHcKB.js +1 -0
  3. package/dist/chunks/{replay-DvJYurEC.d.ts → replay-DR_T0Iz4.d.ts} +12 -17
  4. package/dist/chunks/send-data-Cb4xlYF0.js +1 -0
  5. package/dist/error.d.ts +4 -12
  6. package/dist/error.js +2 -1
  7. package/dist/feature-flags.d.ts +19 -2
  8. package/dist/feature-flags.js +1 -1
  9. package/dist/index.d.ts +28 -59
  10. package/dist/index.js +1 -1
  11. package/dist/replay.d.ts +2 -2
  12. package/dist/replay.js +1 -1
  13. package/dist/web-vitals.d.ts +5 -8
  14. package/dist/web-vitals.js +1 -1
  15. package/package.json +7 -6
  16. package/CHANGELOG.md +0 -184
  17. package/REPLAY_PAYLOAD.md +0 -64
  18. package/dist/chunks/api-urls-DaeYkG0_.js +0 -1
  19. package/dist/chunks/error-Cd9PTS5v.js +0 -2
  20. package/dist/chunks/feature-flags-BClx56v5.d.ts +0 -19
  21. package/dist/chunks/feature-flags-DSOCIZHK.js +0 -1
  22. package/dist/chunks/replay-rTqcjOo2.js +0 -1
  23. package/dist/chunks/rolldown-runtime-MP-BAFHD.js +0 -1
  24. package/dist/chunks/send-data-B2fYGj6v.js +0 -1
  25. package/dist/chunks/session-manager-Cy63ptPF.js +0 -1
  26. package/dist/chunks/types-CYzR5xtT.js +0 -1
  27. package/dist/chunks/web-vitals-Be-Cg4Po.js +0 -1
  28. package/scripts/check-bundle-size.mjs +0 -96
  29. package/src/analytics.ts +0 -787
  30. package/src/entries/error.ts +0 -6
  31. package/src/entries/feature-flags.ts +0 -5
  32. package/src/entries/main.ts +0 -21
  33. package/src/entries/replay.ts +0 -1
  34. package/src/entries/web-vitals.ts +0 -1
  35. package/src/env.d.ts +0 -2
  36. package/src/error.ts +0 -257
  37. package/src/feature-flags.ts +0 -84
  38. package/src/replay.ts +0 -596
  39. package/src/sdk.ts +0 -8
  40. package/src/utils/api-urls.ts +0 -24
  41. package/src/utils/identifiers.ts +0 -47
  42. package/src/utils/send-data.ts +0 -52
  43. package/src/utils/session-manager.ts +0 -416
  44. package/src/utils/types.ts +0 -15
  45. package/src/web-vitals.ts +0 -159
  46. package/tests/analytics.test.ts +0 -453
  47. package/tests/identifiers.test.ts +0 -78
  48. package/tests/replay.test.ts +0 -582
  49. package/tests/session-manager.test.ts +0 -161
  50. package/tests/web-vitals.test.ts +0 -208
  51. package/tsconfig.json +0 -30
  52. package/tsdown.config.ts +0 -27
  53. package/worker/index.ts +0 -22
@@ -1,52 +0,0 @@
1
- import type { SendDataOptions } from "./types";
2
-
3
- export async function sendData(options: SendDataOptions): Promise<boolean> {
4
- const {
5
- url,
6
- data,
7
- contentType = "application/json",
8
- headers = {},
9
- debug = false,
10
- debugPrefix = "[Analytics]",
11
- useBeacon = true,
12
- keepalive = true,
13
- } = options;
14
-
15
- if (useBeacon && typeof globalThis.navigator?.sendBeacon === "function") {
16
- try {
17
- const beaconBody =
18
- data instanceof Blob
19
- ? data
20
- : typeof Blob !== "undefined"
21
- ? new Blob([data as BlobPart], { type: contentType })
22
- : data;
23
- if (globalThis.navigator.sendBeacon(url, beaconBody as BodyInit)) {
24
- if (debug) console.log(`${debugPrefix} Sent via beacon`);
25
- return true;
26
- }
27
- } catch {}
28
- }
29
-
30
- try {
31
- const response = await fetch(url, {
32
- method: "POST",
33
- body: data as BodyInit,
34
- headers: {
35
- "Content-Type": contentType,
36
- ...headers,
37
- },
38
- keepalive,
39
- });
40
- const success = response.ok;
41
- if (debug) {
42
- const message = success
43
- ? `${debugPrefix} Sent via fetch`
44
- : `${debugPrefix} Failed: ${response.status}`;
45
- console[success ? "log" : "warn"](message);
46
- }
47
- return success;
48
- } catch {
49
- if (debug) console.warn(`${debugPrefix} Failed to send`);
50
- return false;
51
- }
52
- }
@@ -1,416 +0,0 @@
1
- export type SessionContext = {
2
- sessionId: string;
3
- windowId: string;
4
- sessionStart: number;
5
- };
6
-
7
- export type SessionRotation = {
8
- prev: SessionContext;
9
- next: SessionContext;
10
- };
11
-
12
- const SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
13
- const SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000;
14
-
15
- const LS_SESSION_ID = "faststats_session_id";
16
- const LS_SESSION_ACTIVITY = "faststats_session_activity";
17
- const LS_SESSION_START = "faststats_session_start";
18
-
19
- const LEGACY_SESSION_ID = "session_id";
20
- const LEGACY_SESSION_TIMESTAMP = "session_timestamp";
21
- const LEGACY_SESSION_START = "session_start";
22
-
23
- let cookielessMode = false;
24
- let defaultSiteKey = "";
25
- let ephemeralSession: {
26
- sessionId: string;
27
- sessionStart: number;
28
- activity: number;
29
- } | null = null;
30
-
31
- type RotationListener = (prev: SessionContext, next: SessionContext) => void;
32
- const rotationListeners = new Set<RotationListener>();
33
-
34
- function storageGet(storage: Storage | undefined, key: string): string | null {
35
- try {
36
- return storage?.getItem(key) ?? null;
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
- function storageSet(
43
- storage: Storage | undefined,
44
- key: string,
45
- value: string,
46
- ): boolean {
47
- if (!storage) return false;
48
- try {
49
- storage.setItem(key, value);
50
- return true;
51
- } catch {
52
- return false;
53
- }
54
- }
55
-
56
- function storageRemove(storage: Storage | undefined, key: string): void {
57
- try {
58
- storage?.removeItem(key);
59
- } catch {}
60
- }
61
-
62
- function getLocalStorage(): Storage | undefined {
63
- try {
64
- return globalThis.localStorage;
65
- } catch {
66
- return undefined;
67
- }
68
- }
69
-
70
- function getSessionStorage(): Storage | undefined {
71
- try {
72
- return globalThis.sessionStorage;
73
- } catch {
74
- return undefined;
75
- }
76
- }
77
-
78
- function notifyRotation(prev: SessionContext, next: SessionContext): void {
79
- for (const listener of rotationListeners) {
80
- listener(prev, next);
81
- }
82
- }
83
-
84
- export function createId(): string {
85
- if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
86
- return crypto.randomUUID();
87
- }
88
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
89
- }
90
-
91
- export function setCookielessMode(mode: boolean): void {
92
- cookielessMode = mode;
93
- if (mode) ephemeralSession = null;
94
- }
95
-
96
- export function isCookielessMode(): boolean {
97
- return cookielessMode;
98
- }
99
-
100
- export function setDefaultSiteKey(siteKey: string): void {
101
- defaultSiteKey = siteKey;
102
- }
103
-
104
- function windowIdKey(siteKey: string): string {
105
- return `faststats_window_id_${siteKey}`;
106
- }
107
-
108
- function parseTimestamp(value: string | null): number | null {
109
- if (!value) return null;
110
- const parsed = Number.parseInt(value, 10);
111
- return Number.isFinite(parsed) ? parsed : null;
112
- }
113
-
114
- function migrateLegacySession(): void {
115
- const session = getSessionStorage();
116
- const local = getLocalStorage();
117
- const legacyId = storageGet(session, LEGACY_SESSION_ID);
118
- if (!legacyId || storageGet(local, LS_SESSION_ID)) {
119
- if (legacyId) {
120
- storageRemove(session, LEGACY_SESSION_ID);
121
- storageRemove(session, LEGACY_SESSION_TIMESTAMP);
122
- storageRemove(session, LEGACY_SESSION_START);
123
- }
124
- return;
125
- }
126
-
127
- const legacyActivity = parseTimestamp(
128
- storageGet(session, LEGACY_SESSION_TIMESTAMP),
129
- );
130
- const legacyStart = parseTimestamp(storageGet(session, LEGACY_SESSION_START));
131
- const now = Date.now();
132
-
133
- const migrated = storageSet(local, LS_SESSION_ID, legacyId);
134
- storageSet(local, LS_SESSION_ACTIVITY, String(legacyActivity ?? now));
135
- storageSet(
136
- local,
137
- LS_SESSION_START,
138
- String(legacyStart ?? legacyActivity ?? now),
139
- );
140
-
141
- if (migrated) {
142
- storageRemove(session, LEGACY_SESSION_ID);
143
- storageRemove(session, LEGACY_SESSION_TIMESTAMP);
144
- storageRemove(session, LEGACY_SESSION_START);
145
- }
146
- }
147
-
148
- function readPersistedSession(): {
149
- sessionId: string;
150
- activity: number;
151
- sessionStart: number;
152
- } | null {
153
- migrateLegacySession();
154
-
155
- const local = getLocalStorage();
156
- const sessionId = storageGet(local, LS_SESSION_ID);
157
- const activity = parseTimestamp(storageGet(local, LS_SESSION_ACTIVITY));
158
- const sessionStart = parseTimestamp(storageGet(local, LS_SESSION_START));
159
-
160
- if (!sessionId || activity === null) return null;
161
-
162
- return {
163
- sessionId,
164
- activity,
165
- sessionStart: sessionStart ?? activity,
166
- };
167
- }
168
-
169
- function writePersistedSession(
170
- sessionId: string,
171
- activity: number,
172
- sessionStart: number,
173
- ): void {
174
- const local = getLocalStorage();
175
- const wroteId = storageSet(local, LS_SESSION_ID, sessionId);
176
- storageSet(local, LS_SESSION_ACTIVITY, activity.toString());
177
- storageSet(local, LS_SESSION_START, sessionStart.toString());
178
-
179
- if (!wroteId) {
180
- ephemeralSession = { sessionId, activity, sessionStart };
181
- }
182
- }
183
-
184
- function clearPersistedSession(): void {
185
- const local = getLocalStorage();
186
- storageRemove(local, LS_SESSION_ID);
187
- storageRemove(local, LS_SESSION_ACTIVITY);
188
- storageRemove(local, LS_SESSION_START);
189
- }
190
-
191
- function isSessionExpired(
192
- activity: number,
193
- sessionStart: number,
194
- now: number,
195
- ): boolean {
196
- if (now - activity >= SESSION_IDLE_TIMEOUT_MS) return true;
197
- return now - sessionStart >= SESSION_MAX_AGE_MS;
198
- }
199
-
200
- function ephemeralRecord(now: number): {
201
- sessionId: string;
202
- sessionStart: number;
203
- activity: number;
204
- } {
205
- if (!ephemeralSession) {
206
- ephemeralSession = {
207
- sessionId: createId(),
208
- sessionStart: now,
209
- activity: now,
210
- };
211
- }
212
- return ephemeralSession;
213
- }
214
-
215
- function resolveSessionRecord(
216
- cookieless: boolean,
217
- touch: boolean,
218
- ): {
219
- sessionId: string;
220
- sessionStart: number;
221
- rotated: boolean;
222
- previous: { sessionId: string; sessionStart: number } | null;
223
- } {
224
- const now = Date.now();
225
-
226
- if (cookieless) {
227
- const record = ephemeralRecord(now);
228
- if (touch) record.activity = now;
229
- return {
230
- sessionId: record.sessionId,
231
- sessionStart: record.sessionStart,
232
- rotated: false,
233
- previous: null,
234
- };
235
- }
236
-
237
- const existing = readPersistedSession() ?? ephemeralSession;
238
- if (
239
- existing &&
240
- !isSessionExpired(existing.activity, existing.sessionStart, now)
241
- ) {
242
- if (touch) {
243
- writePersistedSession(existing.sessionId, now, existing.sessionStart);
244
- }
245
- return {
246
- sessionId: existing.sessionId,
247
- sessionStart: existing.sessionStart,
248
- rotated: false,
249
- previous: null,
250
- };
251
- }
252
-
253
- const previous =
254
- existing !== null
255
- ? {
256
- sessionId: existing.sessionId,
257
- sessionStart: existing.sessionStart,
258
- }
259
- : null;
260
-
261
- const sessionId = createId();
262
- writePersistedSession(sessionId, now, now);
263
-
264
- return {
265
- sessionId,
266
- sessionStart: now,
267
- rotated: previous !== null,
268
- previous,
269
- };
270
- }
271
-
272
- export function getOrCreateWindowId(
273
- siteKey: string,
274
- cookieless?: boolean,
275
- ): string {
276
- if (cookieless ?? cookielessMode) {
277
- return resolveSessionRecord(true, false).sessionId;
278
- }
279
-
280
- const key = windowIdKey(siteKey);
281
- const session = getSessionStorage();
282
- if (!session) return resolveSessionRecord(false, false).sessionId;
283
-
284
- const existing = storageGet(session, key);
285
- if (existing) return existing;
286
-
287
- const windowId = createId();
288
- storageSet(session, key, windowId);
289
- return windowId;
290
- }
291
-
292
- function clearWindowId(siteKey: string): void {
293
- storageRemove(getSessionStorage(), windowIdKey(siteKey));
294
- }
295
-
296
- export function getSessionContext(
297
- siteKey: string,
298
- cookieless?: boolean,
299
- ): SessionContext {
300
- const useCookieless = cookieless ?? cookielessMode;
301
- const record = resolveSessionRecord(useCookieless, false);
302
- const windowId = getOrCreateWindowId(siteKey, useCookieless);
303
-
304
- const context: SessionContext = {
305
- sessionId: record.sessionId,
306
- windowId,
307
- sessionStart: record.sessionStart,
308
- };
309
-
310
- if (record.rotated && record.previous) {
311
- const prev: SessionContext = {
312
- sessionId: record.previous.sessionId,
313
- windowId,
314
- sessionStart: record.previous.sessionStart,
315
- };
316
- notifyRotation(prev, context);
317
- }
318
-
319
- return context;
320
- }
321
-
322
- export function getOrCreateSessionId(cookieless?: boolean): string {
323
- return resolveSessionRecord(cookieless ?? cookielessMode, true).sessionId;
324
- }
325
-
326
- export function getSessionStart(cookieless?: boolean): number {
327
- return resolveSessionRecord(cookieless ?? cookielessMode, false).sessionStart;
328
- }
329
-
330
- export function touchActivity(cookieless?: boolean): SessionRotation | null {
331
- const useCookieless = cookieless ?? cookielessMode;
332
- const record = resolveSessionRecord(useCookieless, true);
333
- if (record.rotated && record.previous) {
334
- const siteKey = defaultSiteKey || "_default";
335
- const rotation = {
336
- prev: {
337
- sessionId: record.previous.sessionId,
338
- windowId: getOrCreateWindowId(siteKey, useCookieless),
339
- sessionStart: record.previous.sessionStart,
340
- },
341
- next: {
342
- sessionId: record.sessionId,
343
- windowId: getOrCreateWindowId(siteKey, useCookieless),
344
- sessionStart: record.sessionStart,
345
- },
346
- };
347
- notifyRotation(rotation.prev, rotation.next);
348
- return rotation;
349
- }
350
- return null;
351
- }
352
-
353
- export function refreshSessionTimestamp(cookieless?: boolean): void {
354
- const useCookieless = cookieless ?? cookielessMode;
355
- const now = Date.now();
356
-
357
- if (useCookieless) {
358
- if (ephemeralSession) ephemeralSession.activity = now;
359
- return;
360
- }
361
-
362
- const existing = readPersistedSession() ?? ephemeralSession;
363
- if (!existing) return;
364
-
365
- writePersistedSession(existing.sessionId, now, existing.sessionStart);
366
- }
367
-
368
- export function resetSession(siteKey?: string): SessionContext {
369
- const key = siteKey ?? defaultSiteKey ?? "_default";
370
- const now = Date.now();
371
- let previous: SessionContext | null = null;
372
-
373
- if (cookielessMode) {
374
- if (ephemeralSession) {
375
- previous = {
376
- sessionId: ephemeralSession.sessionId,
377
- windowId: ephemeralSession.sessionId,
378
- sessionStart: ephemeralSession.sessionStart,
379
- };
380
- }
381
- ephemeralSession = {
382
- sessionId: createId(),
383
- sessionStart: now,
384
- activity: now,
385
- };
386
- } else {
387
- const existing = readPersistedSession();
388
- if (existing) {
389
- previous = {
390
- sessionId: existing.sessionId,
391
- windowId: getOrCreateWindowId(key, false),
392
- sessionStart: existing.sessionStart,
393
- };
394
- }
395
- clearPersistedSession();
396
- ephemeralSession = null;
397
- }
398
-
399
- clearWindowId(key);
400
- const next = getSessionContext(key, cookielessMode);
401
-
402
- if (previous && previous.sessionId !== next.sessionId) {
403
- notifyRotation(previous, next);
404
- }
405
-
406
- return next;
407
- }
408
-
409
- export function onSessionRotated(
410
- listener: (prev: SessionContext, next: SessionContext) => void,
411
- ): () => void {
412
- rotationListeners.add(listener);
413
- return () => {
414
- rotationListeners.delete(listener);
415
- };
416
- }
@@ -1,15 +0,0 @@
1
- export interface SendDataOptions {
2
- url: string;
3
- data: string | Blob | Uint8Array;
4
- contentType?: string;
5
- headers?: Record<string, string>;
6
- debug?: boolean;
7
- debugPrefix?: string;
8
- useBeacon?: boolean;
9
- keepalive?: boolean;
10
- }
11
-
12
- export function normalizeSamplingPercentage(value: number | undefined): number {
13
- if (typeof value !== "number" || !Number.isFinite(value)) return 100;
14
- return Math.max(0, Math.min(100, value));
15
- }
package/src/web-vitals.ts DELETED
@@ -1,159 +0,0 @@
1
- import type { Metric, MetricWithAttribution } from "web-vitals";
2
- import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
3
- import { getOrCreateSessionId } from "./utils/session-manager";
4
- import { normalizeSamplingPercentage } from "./utils/types";
5
-
6
- export interface WebVitalsOptions {
7
- siteKey: string;
8
- baseUrl?: string;
9
- debug?: boolean;
10
- samplingPercentage?: number;
11
- attribution?: boolean;
12
- }
13
-
14
- type MetricName = "CLS" | "INP" | "LCP" | "FCP" | "TTFB";
15
- type AnyMetric = Metric | MetricWithAttribution;
16
-
17
- type MetricPayload = {
18
- metric: MetricName;
19
- value: number;
20
- attributes: Record<string, unknown>;
21
- };
22
-
23
- export default class WebVitalsTracker {
24
- private readonly endpoint: string;
25
- private readonly sampled: boolean;
26
- private readonly metrics = new Map<MetricName, MetricPayload>();
27
- private started = false;
28
- private url = "";
29
-
30
- constructor(private readonly options: WebVitalsOptions) {
31
- this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.vitals}`;
32
- this.sampled =
33
- Math.random() * 100 <
34
- normalizeSamplingPercentage(options.samplingPercentage);
35
- }
36
-
37
- private get debug(): boolean {
38
- return this.options.debug ?? false;
39
- }
40
-
41
- private log(...args: unknown[]): void {
42
- if (this.debug) console.log("[WebVitals]", ...args);
43
- }
44
-
45
- async start(): Promise<void> {
46
- if (this.started || typeof window === "undefined") return;
47
-
48
- this.started = true;
49
- this.url = window.location.href;
50
-
51
- document.addEventListener("visibilitychange", this.onHidden, {
52
- passive: true,
53
- });
54
- window.addEventListener("pagehide", this.onPageHide, { passive: true });
55
-
56
- const mod = this.options.attribution
57
- ? await import("web-vitals/attribution")
58
- : await import("web-vitals");
59
-
60
- if (!this.started) return;
61
-
62
- mod.onCLS(this.capture, { reportAllChanges: true });
63
- mod.onINP(this.capture, { reportAllChanges: true });
64
- mod.onLCP(this.capture, { reportAllChanges: true });
65
- mod.onFCP(this.capture);
66
- mod.onTTFB(this.capture);
67
-
68
- this.log("Tracking started");
69
- }
70
-
71
- stop(): void {
72
- if (!this.started || typeof window === "undefined") return;
73
-
74
- this.started = false;
75
- this.removeListeners();
76
- void this.flush();
77
- }
78
-
79
- trackPageChange(url?: string): void {
80
- if (!this.started || typeof window === "undefined") return;
81
-
82
- const nextUrl = url ?? window.location.href;
83
- if (nextUrl === this.url) return;
84
-
85
- void this.flush();
86
- this.url = nextUrl;
87
- }
88
-
89
- private onHidden = (): void => {
90
- if (document.visibilityState === "hidden") {
91
- void this.flush();
92
- }
93
- };
94
-
95
- private onPageHide = (): void => {
96
- void this.flush();
97
- };
98
-
99
- private capture = (metric: AnyMetric): void => {
100
- if (!this.started || !this.sampled) return;
101
-
102
- const name = metric.name as MetricName;
103
- const attribution = (metric as MetricWithAttribution).attribution;
104
-
105
- this.metrics.set(name, {
106
- metric: name,
107
- value: metric.value,
108
- attributes: {
109
- id: metric.id,
110
- rating: metric.rating,
111
- delta: metric.delta,
112
- navigationType: metric.navigationType,
113
- ...(attribution ?? {}),
114
- },
115
- });
116
- };
117
-
118
- private async flush(): Promise<void> {
119
- if (typeof window === "undefined" || typeof fetch !== "function") return;
120
- if (!this.metrics.size) return;
121
-
122
- const vitals = Array.from(this.metrics.values());
123
- const url = this.url;
124
- this.metrics.clear();
125
-
126
- try {
127
- const controller = new AbortController();
128
- const timeoutId = window.setTimeout(() => controller.abort(), 3000);
129
-
130
- const response = await fetch(this.endpoint, {
131
- method: "POST",
132
- body: JSON.stringify({
133
- sessionId: getOrCreateSessionId(),
134
- vitals,
135
- metadata: { url },
136
- }),
137
- headers: {
138
- "Content-Type": "application/json",
139
- Authorization: `Bearer ${this.options.siteKey}`,
140
- },
141
- keepalive: true,
142
- signal: controller.signal,
143
- });
144
-
145
- clearTimeout(timeoutId);
146
-
147
- if (!response.ok) {
148
- throw new Error(`HTTP ${response.status}`);
149
- }
150
- } catch (error) {
151
- this.log("Failed to send metrics", error);
152
- }
153
- }
154
-
155
- private removeListeners(): void {
156
- document.removeEventListener("visibilitychange", this.onHidden);
157
- window.removeEventListener("pagehide", this.onPageHide);
158
- }
159
- }