@faststats/web 0.1.3 → 0.1.4
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/CHANGELOG.md +6 -0
- package/dist/analytics-D0RCcMlv.js +1 -0
- package/dist/error-BNd3BvFI.js +2 -0
- package/dist/module.d.ts +34 -18
- package/dist/module.js +1 -1
- package/dist/replay-D9AlW2Jv.js +1 -0
- package/dist/types-8FXsUqbi.js +1 -0
- package/dist/web-vitals-Bj8FoiTR.js +1 -0
- package/package.json +5 -2
- package/src/analytics.ts +303 -129
- package/src/error.ts +120 -116
- package/src/module.ts +1 -0
- package/src/replay.ts +12 -7
- package/src/utils/api-urls.ts +26 -0
- package/src/web-vitals.ts +25 -20
- package/tests/analytics.test.ts +311 -0
- package/tsdown.config.ts +1 -1
- package/dist/analytics-p8pLmlA-.js +0 -1
- package/dist/error-CMM8PYcW.js +0 -3
- package/dist/replay-DeRZXYG3.js +0 -1
- package/dist/types-B5zbhOWK.js +0 -1
- package/dist/web-vitals-DU_i1Ejx.js +0 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { getInstance, WebAnalytics } from "../src/analytics";
|
|
3
|
+
|
|
4
|
+
class MockStorage {
|
|
5
|
+
private readonly store = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
getItem(key: string): string | null {
|
|
8
|
+
return this.store.get(key) ?? null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
setItem(key: string, value: string): void {
|
|
12
|
+
this.store.set(key, value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
removeItem(key: string): void {
|
|
16
|
+
this.store.delete(key);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class MockNode {
|
|
21
|
+
parentNode: MockNode | null = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class MockAnchorElement extends MockNode {
|
|
25
|
+
constructor(
|
|
26
|
+
public href: string,
|
|
27
|
+
public host: string,
|
|
28
|
+
) {
|
|
29
|
+
super();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Listener = EventListenerOrEventListenerObject;
|
|
34
|
+
|
|
35
|
+
class MockEventTarget {
|
|
36
|
+
readonly listeners = new Map<string, Set<Listener>>();
|
|
37
|
+
|
|
38
|
+
addEventListener(type: string, listener: Listener): void {
|
|
39
|
+
const existing = this.listeners.get(type) ?? new Set<Listener>();
|
|
40
|
+
existing.add(listener);
|
|
41
|
+
this.listeners.set(type, existing);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
removeEventListener(type: string, listener: Listener): void {
|
|
45
|
+
this.listeners.get(type)?.delete(listener);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
listenerCount(type: string): number {
|
|
49
|
+
return this.listeners.get(type)?.size ?? 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type FetchCall = {
|
|
54
|
+
url: string;
|
|
55
|
+
body?: string;
|
|
56
|
+
headers?: Record<string, string>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type BrowserHarness = {
|
|
60
|
+
windowTarget: MockEventTarget;
|
|
61
|
+
documentTarget: MockEventTarget;
|
|
62
|
+
fetchCalls: FetchCall[];
|
|
63
|
+
restore(): void;
|
|
64
|
+
location: {
|
|
65
|
+
href: string;
|
|
66
|
+
pathname: string;
|
|
67
|
+
hash: string;
|
|
68
|
+
search: string;
|
|
69
|
+
};
|
|
70
|
+
history: History;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function wait(ms: number): Promise<void> {
|
|
74
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function setGlobal(name: keyof typeof globalThis, value: unknown): void {
|
|
78
|
+
Object.defineProperty(globalThis, name, {
|
|
79
|
+
configurable: true,
|
|
80
|
+
writable: true,
|
|
81
|
+
value,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setupBrowser(): BrowserHarness {
|
|
86
|
+
const originalValues = {
|
|
87
|
+
window: globalThis.window,
|
|
88
|
+
document: globalThis.document,
|
|
89
|
+
location: globalThis.location,
|
|
90
|
+
history: globalThis.history,
|
|
91
|
+
localStorage: globalThis.localStorage,
|
|
92
|
+
sessionStorage: globalThis.sessionStorage,
|
|
93
|
+
navigator: globalThis.navigator,
|
|
94
|
+
fetch: globalThis.fetch,
|
|
95
|
+
Node: globalThis.Node,
|
|
96
|
+
HTMLAnchorElement: globalThis.HTMLAnchorElement,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const fetchCalls: FetchCall[] = [];
|
|
100
|
+
const windowTarget = new MockEventTarget();
|
|
101
|
+
const documentTarget = new MockEventTarget();
|
|
102
|
+
const localStorage = new MockStorage();
|
|
103
|
+
const sessionStorage = new MockStorage();
|
|
104
|
+
const locationState = {
|
|
105
|
+
href: "https://example.com/",
|
|
106
|
+
pathname: "/",
|
|
107
|
+
hash: "",
|
|
108
|
+
search: "",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const updateHref = (): void => {
|
|
112
|
+
locationState.href = `https://example.com${locationState.pathname}${locationState.search}${locationState.hash}`;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const history = {
|
|
116
|
+
pushState(_data: unknown, _unused: string, url?: string | URL | null) {
|
|
117
|
+
if (typeof url === "string") {
|
|
118
|
+
const parsed = new URL(url, locationState.href);
|
|
119
|
+
locationState.pathname = parsed.pathname;
|
|
120
|
+
locationState.search = parsed.search;
|
|
121
|
+
locationState.hash = parsed.hash;
|
|
122
|
+
updateHref();
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
replaceState(_data: unknown, _unused: string, url?: string | URL | null) {
|
|
126
|
+
if (typeof url === "string") {
|
|
127
|
+
const parsed = new URL(url, locationState.href);
|
|
128
|
+
locationState.pathname = parsed.pathname;
|
|
129
|
+
locationState.search = parsed.search;
|
|
130
|
+
locationState.hash = parsed.hash;
|
|
131
|
+
updateHref();
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
} as unknown as History;
|
|
135
|
+
|
|
136
|
+
const document = {
|
|
137
|
+
addEventListener: documentTarget.addEventListener.bind(documentTarget),
|
|
138
|
+
removeEventListener:
|
|
139
|
+
documentTarget.removeEventListener.bind(documentTarget),
|
|
140
|
+
referrer: "",
|
|
141
|
+
title: "FastStats",
|
|
142
|
+
visibilityState: "visible",
|
|
143
|
+
documentElement: {
|
|
144
|
+
scrollHeight: 2000,
|
|
145
|
+
scrollTop: 0,
|
|
146
|
+
},
|
|
147
|
+
body: {
|
|
148
|
+
scrollHeight: 2000,
|
|
149
|
+
},
|
|
150
|
+
} as unknown as Document;
|
|
151
|
+
|
|
152
|
+
const windowObject = {
|
|
153
|
+
addEventListener: windowTarget.addEventListener.bind(windowTarget),
|
|
154
|
+
removeEventListener: windowTarget.removeEventListener.bind(windowTarget),
|
|
155
|
+
innerHeight: 1000,
|
|
156
|
+
scrollY: 0,
|
|
157
|
+
setTimeout,
|
|
158
|
+
clearTimeout,
|
|
159
|
+
setInterval,
|
|
160
|
+
clearInterval,
|
|
161
|
+
history,
|
|
162
|
+
location: locationState,
|
|
163
|
+
} as unknown as Window & typeof globalThis;
|
|
164
|
+
|
|
165
|
+
const navigator = {
|
|
166
|
+
sendBeacon: () => false,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
setGlobal("window", windowObject);
|
|
170
|
+
setGlobal("document", document);
|
|
171
|
+
setGlobal("location", locationState as Location);
|
|
172
|
+
setGlobal("history", history);
|
|
173
|
+
setGlobal("localStorage", localStorage);
|
|
174
|
+
setGlobal("sessionStorage", sessionStorage);
|
|
175
|
+
setGlobal("navigator", navigator);
|
|
176
|
+
setGlobal("fetch", (async (
|
|
177
|
+
url: string | URL | Request,
|
|
178
|
+
init?: RequestInit,
|
|
179
|
+
): Promise<Response> => {
|
|
180
|
+
let body: string | undefined;
|
|
181
|
+
if (typeof init?.body === "string") {
|
|
182
|
+
body = init.body;
|
|
183
|
+
} else if (init?.body instanceof Blob) {
|
|
184
|
+
body = await init.body.text();
|
|
185
|
+
}
|
|
186
|
+
fetchCalls.push({
|
|
187
|
+
url: String(url),
|
|
188
|
+
body,
|
|
189
|
+
headers: init?.headers as Record<string, string> | undefined,
|
|
190
|
+
});
|
|
191
|
+
return new Response("", { status: 204 });
|
|
192
|
+
}) as typeof fetch);
|
|
193
|
+
setGlobal("Node", MockNode as unknown as typeof Node);
|
|
194
|
+
setGlobal(
|
|
195
|
+
"HTMLAnchorElement",
|
|
196
|
+
MockAnchorElement as unknown as typeof HTMLAnchorElement,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
windowTarget,
|
|
201
|
+
documentTarget,
|
|
202
|
+
fetchCalls,
|
|
203
|
+
location: locationState,
|
|
204
|
+
history,
|
|
205
|
+
restore() {
|
|
206
|
+
setGlobal("window", originalValues.window);
|
|
207
|
+
setGlobal("document", originalValues.document);
|
|
208
|
+
setGlobal("location", originalValues.location);
|
|
209
|
+
setGlobal("history", originalValues.history);
|
|
210
|
+
setGlobal("localStorage", originalValues.localStorage);
|
|
211
|
+
setGlobal("sessionStorage", originalValues.sessionStorage);
|
|
212
|
+
setGlobal("navigator", originalValues.navigator);
|
|
213
|
+
setGlobal("fetch", originalValues.fetch);
|
|
214
|
+
setGlobal("Node", originalValues.Node);
|
|
215
|
+
setGlobal("HTMLAnchorElement", originalValues.HTMLAnchorElement);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let harness: BrowserHarness;
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
harness = setupBrowser();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
afterEach(() => {
|
|
227
|
+
getInstance()?.destroy();
|
|
228
|
+
harness.restore();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("WebAnalytics lifecycle", () => {
|
|
232
|
+
test("start installs listeners and destroy removes them", async () => {
|
|
233
|
+
const analytics = new WebAnalytics({
|
|
234
|
+
siteKey: "site_test",
|
|
235
|
+
autoTrack: false,
|
|
236
|
+
});
|
|
237
|
+
await analytics.start();
|
|
238
|
+
|
|
239
|
+
expect(getInstance()).toBe(analytics);
|
|
240
|
+
expect(harness.windowTarget.listenerCount("pagehide")).toBe(1);
|
|
241
|
+
expect(harness.windowTarget.listenerCount("popstate")).toBe(1);
|
|
242
|
+
expect(harness.documentTarget.listenerCount("visibilitychange")).toBe(1);
|
|
243
|
+
expect(harness.documentTarget.listenerCount("click")).toBe(1);
|
|
244
|
+
|
|
245
|
+
analytics.destroy();
|
|
246
|
+
|
|
247
|
+
expect(getInstance()).toBeNull();
|
|
248
|
+
expect(harness.windowTarget.listenerCount("pagehide")).toBe(0);
|
|
249
|
+
expect(harness.windowTarget.listenerCount("popstate")).toBe(0);
|
|
250
|
+
expect(harness.documentTarget.listenerCount("visibilitychange")).toBe(0);
|
|
251
|
+
expect(harness.documentTarget.listenerCount("click")).toBe(0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("consent changes unblock identify payloads", async () => {
|
|
255
|
+
const analytics = new WebAnalytics({
|
|
256
|
+
siteKey: "site_test",
|
|
257
|
+
autoTrack: false,
|
|
258
|
+
consent: {
|
|
259
|
+
mode: "pending",
|
|
260
|
+
cookielessWhilePending: true,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await analytics.start();
|
|
265
|
+
analytics.identify("user_1", "user@example.com");
|
|
266
|
+
await wait(0);
|
|
267
|
+
expect(
|
|
268
|
+
harness.fetchCalls.some((call) => call.url.endsWith("/v1/identify")),
|
|
269
|
+
).toBe(false);
|
|
270
|
+
|
|
271
|
+
analytics.setConsentMode("granted");
|
|
272
|
+
analytics.identify("user_1", "user@example.com", {
|
|
273
|
+
name: "User One",
|
|
274
|
+
});
|
|
275
|
+
await wait(0);
|
|
276
|
+
|
|
277
|
+
const identifyCall = harness.fetchCalls.find((call) =>
|
|
278
|
+
call.url.endsWith("/v1/identify"),
|
|
279
|
+
);
|
|
280
|
+
expect(identifyCall).toBeTruthy();
|
|
281
|
+
expect(identifyCall?.body).toContain('"externalId":"user_1"');
|
|
282
|
+
expect(identifyCall?.body).toContain('"email":"user@example.com"');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("navigation sends page leave and navigation pageview", async () => {
|
|
286
|
+
const analytics = new WebAnalytics({
|
|
287
|
+
siteKey: "site_test",
|
|
288
|
+
autoTrack: false,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await analytics.start();
|
|
292
|
+
harness.fetchCalls.length = 0;
|
|
293
|
+
|
|
294
|
+
harness.history.pushState({}, "", "/about");
|
|
295
|
+
await wait(350);
|
|
296
|
+
await wait(0);
|
|
297
|
+
|
|
298
|
+
const payloads = harness.fetchCalls.map((call) => call.body ?? "");
|
|
299
|
+
expect(payloads.some((body) => body.includes('"event":"page_leave"'))).toBe(
|
|
300
|
+
true,
|
|
301
|
+
);
|
|
302
|
+
expect(
|
|
303
|
+
payloads.some(
|
|
304
|
+
(body) =>
|
|
305
|
+
body.includes('"event":"pageview"') &&
|
|
306
|
+
body.includes('"trigger":"navigation"') &&
|
|
307
|
+
body.includes('"page":"/about"'),
|
|
308
|
+
),
|
|
309
|
+
).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
});
|
package/tsdown.config.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as e,c as t,i as n,n as r,o as i,r as a,s as o,t as s}from"./types-B5zbhOWK.js";let c=null;function l(){return c}function u(e,t){typeof window>`u`||g()||c?.track(e,t??{})}function d(e,t,n){typeof window>`u`||g()||c?.identify(e,t,n??{})}function f(e=!0){typeof window>`u`||g()||c?.logout(e)}function p(e){c?.setConsentMode(e)}function m(){p(`granted`)}function h(){p(`denied`)}function g(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function _(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`}=e,s=n instanceof Blob?n:new Blob([n],{type:r});if(navigator.sendBeacon?.(t,s))return a&&console.log(`${o} ✓ Sent via beacon`),!0;try{let e=await fetch(t,{method:`POST`,body:n,headers:{"Content-Type":r,...i},keepalive:!0}),s=e.ok;return a&&(s?console.log(`${o} ✓ Sent via fetch`):console.warn(`${o} ✗ Failed: ${e.status}`)),s}catch{return a&&console.warn(`${o} ✗ Failed to send`),!1}}function v(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function y(){let e={};if(!location.search)return e;let t=new URLSearchParams(location.search);for(let n of[`utm_source`,`utm_medium`,`utm_campaign`,`utm_term`,`utm_content`]){let r=t.get(n);r&&(e[n]=r)}return e}var b=class{endpoint;debug;started=!1;pageKey=``;navTimer=null;heartbeatTimer=null;scrollDepth=0;pageEntryTime=0;pagePath=``;pageUrl=``;pageHash=``;hasLeftCurrentPage=!1;scrollHandler=null;consentMode;cookielessWhilePending;constructor(e){this.options=e,this.endpoint=e.endpoint??`https://metrics.faststats.dev/v1/web`,this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(g()){this.log(`disabled`);return}c=this,setTimeout(()=>void this.start(),0)}}async start(){if(this.started||typeof window>`u`)return;if(c&&c!==this){this.log(`already started by another instance`);return}if(g()){this.log(`disabled`);return}this.started=!0,c=this,t(this.isCookielessMode());let e=this.options;if(e.errorTracking?.enabled??e.trackErrors){let{default:t}=await import(`./error-CMM8PYcW.js`);new t({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug}).start(),this.log(`error loaded`)}if(e.trackWebVitals){let{default:t}=await import(`./web-vitals-DU_i1Ejx.js`);new t({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,samplingPercentage:s(e.webVitals?.sampling?.percentage)}).start(),this.log(`web-vitals loaded`)}if(e.trackReplay){let{default:t}=await import(`./replay-DeRZXYG3.js`);new t({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,...e.replayOptions}).start(),this.log(`replay loaded`)}this.enterPage(),this.pageview({trigger:`load`}),this.links(),this.trackScroll(),this.startHeartbeat(),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`?this.leavePage():this.startHeartbeat()}),window.addEventListener(`pagehide`,()=>this.leavePage()),window.addEventListener(`popstate`,()=>this.navigate()),e.trackHash&&window.addEventListener(`hashchange`,()=>this.navigate()),this.patch()}pageview(e={}){let t=`${location.pathname}|${this.options.trackHash??!1?location.hash:``}`;t!==this.pageKey&&(this.pageKey=t,this.send(`pageview`,e))}track(e,t={}){this.send(e,t)}identify(e,t,n={}){if(g()||this.isCookielessMode())return;let i=e.trim(),a=t.trim();!i||!a||_({url:this.endpoint.replace(/\/v1\/web$/,`/v1/identify`),data:JSON.stringify({token:this.options.siteKey,identifier:r(!1),externalId:i,email:a,name:n.name?.trim()||void 0,phone:n.phone?.trim()||void 0,avatarUrl:n.avatarUrl?.trim()||void 0,traits:n.traits??{}}),contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] identify`})}logout(e=!0){g()||(e&&i(this.isCookielessMode()),o())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return r(this.isCookielessMode())}getSessionId(){return a()}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,t={}){let n=r(this.isCookielessMode()),i=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:a(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...y(),...t}});this.log(e),_({url:this.endpoint,data:i,contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] ${e}`})}enterPage(){this.pageEntryTime=Date.now(),this.pagePath=location.pathname,this.pageUrl=location.href,this.pageHash=location.hash,this.scrollDepth=0,this.hasLeftCurrentPage=!1}leavePage(){if(this.hasLeftCurrentPage)return;this.hasLeftCurrentPage=!0;let e=Date.now();this.send(`page_leave`,{page:this.pagePath,url:this.pageUrl,time_on_page:e-this.pageEntryTime,scroll_depth:this.scrollDepth,session_duration:e-n()})}trackScroll(){this.scrollHandler&&window.removeEventListener(`scroll`,this.scrollHandler);let e=()=>{let e=document.documentElement,t=document.body,n=window.innerHeight,r=Math.max(e.scrollHeight,t.scrollHeight);if(r<=n){this.scrollDepth=100;return}let i=Math.min(100,Math.round(((window.scrollY||e.scrollTop)+n)/r*100));i>this.scrollDepth&&(this.scrollDepth=i)};this.scrollHandler=e,e(),window.addEventListener(`scroll`,e,{passive:!0})}startHeartbeat(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval(()=>{if(document.visibilityState===`hidden`){clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;return}e()},300*1e3)}navigate(){this.navTimer&&clearTimeout(this.navTimer),this.navTimer=setTimeout(()=>{this.navTimer=null;let e=location.pathname!==this.pagePath,t=(this.options.trackHash??!1)&&location.hash!==this.pageHash;!e&&!t||(this.leavePage(),this.enterPage(),this.trackScroll(),this.pageview({trigger:`navigation`}))},300)}patch(){let e=()=>this.navigate();for(let t of[`pushState`,`replaceState`]){let n=history[t];history[t]=function(...t){let r=n.apply(this,t);return e(),r}}}links(){let e=e=>{let t=v(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};document.addEventListener(`click`,e),document.addEventListener(`auxclick`,e)}};export{f as a,_ as c,g as i,p as l,l as n,m as o,d as r,h as s,b as t,u};
|
package/dist/error-CMM8PYcW.js
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import{n as e,r as t}from"./types-B5zbhOWK.js";import{c as n}from"./analytics-p8pLmlA-.js";var r=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;handledErrors=new WeakSet;errorCounts=new Map;flushTimer=null;started=!1;constructor(e){this.siteKey=e.siteKey,this.endpoint=e.endpoint??`https://metrics.faststats.dev/v1/web`,this.debug=e.debug??!1,this.flushInterval=e.flushInterval??5e3,this.maxQueueSize=e.maxQueueSize??50}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.handleErrorEvent),window.addEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer=setInterval(()=>this.flush(),this.flushInterval),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.flush()}),window.addEventListener(`pagehide`,()=>this.flush()),this.debug&&console.log(`[ErrorTracker] Started listening for errors`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.handleErrorEvent),window.removeEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush(),this.debug&&console.log(`[ErrorTracker] Stopped listening for errors`))}handleErrorEvent=e=>{let t=e.error;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate error:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:e.message||(t?.message??`Unknown error`),filename:e.filename||void 0,lineno:e.lineno||void 0,colno:e.colno||void 0,stack:t?.stack||void 0,type:`error`})};handleRejection=e=>{let t=e.reason;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate rejection:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:t instanceof Error?t.message:typeof t==`string`?t:`Unhandled promise rejection`,stack:t instanceof Error?t.stack:void 0,type:`unhandledrejection`})};isExtensionError(e){return!!(e.filename?.startsWith(`chrome-extension://`)||e.stack&&e.stack.split(`
|
|
2
|
-
`).find(e=>e.trim().startsWith(`at `))?.includes(`chrome-extension://`))}parseStack(e){if(e)return e.split(`
|
|
3
|
-
`).map(e=>e.trim()).filter(e=>e.length>0)}async generateErrorHash(e){let t=[e.type,e.message,e.filename??``,e.lineno??``].join(`:`),n=new TextEncoder().encode(t),r=await crypto.subtle.digest(`SHA-256`,n);return`err_${Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}`}async queueError(e){if(this.isExtensionError(e))return;this.debug&&console.log(`[ErrorTracker] Captured error:`,e);let t=await this.generateErrorHash(e),n=this.errorCounts.get(t);if(n)n.count++,this.debug&&console.log(`[ErrorTracker] Incremented count for ${t} to ${n.count}`);else{let n={error:e.type===`unhandledrejection`?`UnhandledRejection`:`Error`,message:e.message,stack:this.parseStack(e.stack)};this.errorCounts.set(t,{entry:n,hash:t,count:1}),this.debug&&console.log(`[ErrorTracker] Queued new error: ${t}`)}this.errorCounts.size>=this.maxQueueSize&&this.flush()}captureError(e){if(this.handledErrors.has(e)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate manual capture:`,e.message);return}this.handledErrors.add(e),this.queueError({message:e.message,stack:e.stack,type:`error`})}flush(){if(this.errorCounts.size===0)return;let r=[];for(let{entry:e,hash:t,count:n}of this.errorCounts.values())r.push({...e,hash:t,count:n});this.errorCounts.clear(),this.debug&&console.log(`[ErrorTracker] Flushing errors:`,r);let i=e(),a=typeof document<`u`?{url:location.href,page:location.pathname,referrer:document.referrer||null}:{url:``,page:``,referrer:null},o=globalThis.__SOURCEMAPS_BUILD__,s=typeof o?.buildId==`string`&&o.buildId.trim().length>0?o.buildId:void 0,c={token:this.siteKey,...i?{userId:i}:{},sessionId:t(),...s?{buildId:s}:{},data:{url:a.url,page:a.page,referrer:a.referrer,title:typeof document<`u`?document.title:``},errors:r},l=JSON.stringify(c);this.debug&&console.log(`[ErrorTracker] Payload:`,l),n({url:this.endpoint,data:l,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{r as default};
|
package/dist/replay-DeRZXYG3.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{n as e,r as t,t as n}from"./types-B5zbhOWK.js";import{c as r}from"./analytics-p8pLmlA-.js";import{getRecordConsolePlugin as i}from"@rrweb/rrweb-plugin-console-record";import{record as a}from"rrweb";var o=class{endpoint;siteKey;debug;flushInterval;maxEvents;sampling;slimDOMOptions;maskAllInputs;maskInputOptions;blockClass;blockSelector;maskTextClass;maskTextSelector;checkoutEveryNms;checkoutEveryNth;samplingPercentage;recordConsole;events=[];flushTimer=null;stopRecording=void 0;started=!1;startTime=0;minLengthFlushScheduled=!1;sequenceNumber=0;pendingBatches=[];isFlushing=!1;compressionSupported=!1;sessionSamplingSeed;minReplayLengthMs;constructor(e){this.siteKey=e.siteKey,this.endpoint=e.endpoint?.replace(/\/v1\/web$/,`/v1/replay`)??`https://metrics.faststats.dev/v1/replay`,this.debug=e.debug??!1,this.samplingPercentage=n(e.samplingPercentage),this.flushInterval=e.flushInterval??1e4,this.maxEvents=e.maxEvents??500,this.sampling=e.sampling??{mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},this.slimDOMOptions=e.slimDOMOptions??{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0},this.maskAllInputs=e.maskAllInputs??!0,this.maskInputOptions=e.maskInputOptions??{password:!0,email:!0,tel:!0},this.blockClass=e.blockClass,this.blockSelector=e.blockSelector,this.maskTextClass=e.maskTextClass,this.maskTextSelector=e.maskTextSelector,this.checkoutEveryNms=e.checkoutEveryNms??6e4,this.checkoutEveryNth=e.checkoutEveryNth,this.recordConsole=e.recordConsole??!0,this.minReplayLengthMs=e.minReplayLengthMs??3e3,this.sessionSamplingSeed=Math.random()*100,typeof window<`u`&&(this.compressionSupported=`CompressionStream`in window)}start(){if(this.started||typeof window>`u`||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;this.started=!0,this.startTime=Date.now(),this.debug&&console.log(`[Replay] Recording started`);let e={emit:(e,t)=>this.handleEvent(e,t),sampling:this.sampling,slimDOMOptions:this.slimDOMOptions,maskAllInputs:this.maskAllInputs,checkoutEveryNms:this.checkoutEveryNms};this.maskInputOptions&&(e.maskInputOptions=this.maskInputOptions),this.blockClass&&(e.blockClass=this.blockClass),this.blockSelector&&(e.blockSelector=this.blockSelector),this.maskTextClass&&(e.maskTextClass=this.maskTextClass),this.maskTextSelector&&(e.maskTextSelector=this.maskTextSelector),this.checkoutEveryNth&&(e.checkoutEveryNth=this.checkoutEveryNth),this.recordConsole&&(e.plugins=[i()]),this.stopRecording=a(e),this.flushTimer=setInterval(()=>{this.scheduleFlush()},this.flushInterval),window.addEventListener(`beforeunload`,this.handleUnload),window.addEventListener(`pagehide`,this.handleUnload),document.addEventListener(`visibilitychange`,this.handleVisibilityChange)}stop(){if(this.started){if(this.started=!1,this.debug&&console.log(`[Replay] Recording stopped`),this.stopRecording?.(),this.stopRecording=void 0,this.flushTimer&&=(clearInterval(this.flushTimer),null),window.removeEventListener(`beforeunload`,this.handleUnload),window.removeEventListener(`pagehide`,this.handleUnload),document.removeEventListener(`visibilitychange`,this.handleVisibilityChange),!this.hasReachedMinLength()){this.events=[],this.debug&&console.log(`[Replay] Session too short (${Date.now()-this.startTime}ms), discarding events`);return}this.flush()}}handleEvent(e,t){this.events.push(e),t||this.events.length>=this.maxEvents?this.scheduleFlush():!this.minLengthFlushScheduled&&this.hasReachedMinLength()&&(this.minLengthFlushScheduled=!0,this.scheduleFlush())}hasReachedMinLength(){return this.minReplayLengthMs<=0?!0:Date.now()-this.startTime>=this.minReplayLengthMs}scheduleFlush(){this.isFlushing||this.events.length===0||(typeof window<`u`&&`requestIdleCallback`in window?window.requestIdleCallback(()=>this.flush(),{timeout:2e3}):setTimeout(()=>this.flush(),0))}handleUnload=()=>{this.flush()};handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush()):document.visibilityState===`visible`&&this.started&&(this.flushTimer||=setInterval(()=>{this.scheduleFlush()},this.flushInterval))};async flush(){if(this.events.length===0){this.processPendingBatches();return}if(!this.hasReachedMinLength()){this.debug&&console.log(`[Replay] Too short (${Date.now()-this.startTime}ms), skipping`),this.processPendingBatches();return}if(this.isFlushing)return;this.isFlushing=!0;let n=this.events;this.events=[];let r=e(),i={token:this.siteKey,sessionId:t(),...r?{identifier:r}:{},sequence:this.sequenceNumber++,timestamp:Date.now(),url:window.location.href,events:n};this.debug&&console.log(`[Replay] Sending ${n.length} events (seq: ${i.sequence})`);try{let e,t=!1;if(this.compressionSupported)try{e=await this.compress(JSON.stringify(i)),t=!0}catch{this.debug&&console.warn(`[Replay] Compression failed, using uncompressed`),e=new Blob([JSON.stringify(i)],{type:`application/json`})}else e=new Blob([JSON.stringify(i)],{type:`application/json`});await this.send(e,t)||this.pendingBatches.push({batch:i,isCompressed:t,retries:0})}catch(e){this.debug&&console.warn(`[Replay] Flush error:`,e),this.pendingBatches.push({batch:i,isCompressed:!1,retries:0})}finally{this.isFlushing=!1,this.processPendingBatches()}}async processPendingBatches(){if(this.pendingBatches.length===0)return;let e=this.pendingBatches.shift();if(e){if(e.retries>=3){this.debug&&console.warn(`[Replay] Max retries reached, dropping batch ${e.batch.sequence}`),this.processPendingBatches();return}e.retries++;try{let t;if(e.isCompressed&&this.compressionSupported)try{t=await this.compress(JSON.stringify(e.batch))}catch{t=new Blob([JSON.stringify(e.batch)],{type:`application/json`}),e.isCompressed=!1}else t=new Blob([JSON.stringify(e.batch)],{type:`application/json`});await this.send(t,e.isCompressed)?this.processPendingBatches():(this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries))}catch{this.debug&&console.warn(`[Replay] Retry ${e.retries} failed`),this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries)}}}async compress(e){if(!this.compressionSupported)throw Error(`Compression not supported`);let t=new TextEncoder().encode(e),n=new CompressionStream(`gzip`),r=n.writable.getWriter();r.write(t),r.close();let i=[],a=n.readable.getReader();for(;;){let{done:e,value:t}=await a.read();if(e)break;t&&i.push(t)}let o=i.reduce((e,t)=>e+t.length,0),s=new Uint8Array(o),c=0;for(let e of i)s.set(e,c),c+=e.length;if(this.debug){let e=(s.length/t.length*100).toFixed(1);console.log(`[Replay] Compressed: ${t.length} → ${s.length} bytes (${e}%)`)}return new Blob([s],{type:`application/octet-stream`})}async send(e,t){let n=t?`${this.endpoint}?encoding=gzip`:this.endpoint,i=(e.size/1024).toFixed(1);return r({url:n,data:e,contentType:t?`application/octet-stream`:`application/json`,headers:t?{"Content-Encoding":`gzip`}:void 0,debug:this.debug,debugPrefix:`[Replay] ${i}KB`})??Promise.resolve(!1)}getSessionId(){return t()}};export{o as default};
|
package/dist/types-B5zbhOWK.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
let e=!1;function t(t){e=t}function n(t){return t??e?``:r()}function r(){if(typeof localStorage>`u`)return``;let e=localStorage.getItem(`faststats_anon_id`);if(e)return e;let t=crypto.randomUUID();return localStorage.setItem(`faststats_anon_id`,t),t}function i(e){return e||typeof localStorage>`u`?``:(localStorage.removeItem(`faststats_anon_id`),r())}function a(){if(typeof sessionStorage>`u`)return``;let e=sessionStorage.getItem(`session_id`),t=sessionStorage.getItem(`session_timestamp`);if(e&&t){if(Date.now()-Number.parseInt(t,10)<18e5)return sessionStorage.setItem(`session_timestamp`,Date.now().toString()),e;sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`)}let n=Date.now().toString(),r=crypto.randomUUID();return sessionStorage.setItem(`session_id`,r),sessionStorage.setItem(`session_timestamp`,n),sessionStorage.setItem(`session_start`,n),r}function o(){return typeof sessionStorage>`u`?``:(sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`),a())}function s(){typeof sessionStorage>`u`||sessionStorage.getItem(`session_id`)&&sessionStorage.setItem(`session_timestamp`,Date.now().toString())}function c(){if(typeof sessionStorage>`u`)return Date.now();let e=sessionStorage.getItem(`session_start`);if(e)return Number.parseInt(e,10);let t=sessionStorage.getItem(`session_timestamp`);return t?Number.parseInt(t,10):Date.now()}function l(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}export{s as a,t as c,c as i,n,i as o,a as r,o as s,l as t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as e,t}from"./types-B5zbhOWK.js";import{onCLS as n,onFCP as r,onINP as i,onLCP as a,onTTFB as o}from"web-vitals/attribution";var s=class{endpoint;siteKey;debug;samplingPercentage;started=!1;sessionSamplingSeed;metricsMap=new Map;flushed=!1;constructor(e){this.siteKey=e.siteKey,this.endpoint=this.getVitalsEndpoint(e.endpoint??`https://metrics.faststats.dev/v1/web`),this.debug=e.debug??!1,this.samplingPercentage=t(e.samplingPercentage),this.sessionSamplingSeed=Math.random()*100}getVitalsEndpoint(e){let t=new URL(e),n=t.pathname.split(`/`);return n[n.length-1]=`vitals`,t.pathname=n.join(`/`),t.toString()}start(){this.started||typeof window>`u`||(this.started=!0,document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.finalizeAndFlush()}),window.addEventListener(`pagehide`,()=>{this.finalizeAndFlush()}),this.debug&&console.log(`[WebVitals] Tracking started`),n(e=>this.captureMetric(e)),i(e=>this.captureMetric(e)),a(e=>this.captureMetric(e)),r(e=>this.captureMetric(e)),o(e=>this.captureMetric(e)))}captureMetric(e){if(this.flushed||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;let t=e.name,n={id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...e.attribution??{}},r=t===`FCP`||t===`TTFB`;this.metricsMap.set(t,{value:e.value,attributes:n,final:r}),this.debug&&console.log(`[WebVitals] ${t} captured: ${e.value}`+(r?` (final)`:``))}finalizeAndFlush(){if(!(this.flushed||this.metricsMap.size===0)){for(let[e,t]of this.metricsMap.entries())t.final||(t.final=!0,this.debug&&console.log(`[WebVitals] ${e} finalized: ${t.value}`));this.flushWithBeacon()}}buildPayload(){if(this.metricsMap.size===0)return null;let t=Array.from(this.metricsMap.entries()).map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes}));return{body:JSON.stringify({sessionId:e(),vitals:t,metadata:{url:window.location.href}}),count:t.length}}flushWithBeacon(){let e=this.buildPayload();if(e){if(this.flushed=!0,this.debug){let t=Array.from(this.metricsMap.keys()).join(`, `);console.log(`[WebVitals] Sending final metrics (${e.count}): ${t}`)}fetch(this.endpoint,{method:`POST`,body:e.body,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.siteKey}`},keepalive:!0}).catch(()=>{this.debug&&console.warn(`[WebVitals] Failed to send metrics`)})}}};export{s as default};
|