@faststats/web 0.5.0 → 0.7.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.
@@ -0,0 +1,46 @@
1
+ //#region src/utils/api-urls.ts
2
+ const ANALYTICS_BASE = "https://metrics.faststats.dev";
3
+ const FEATURE_FLAGS_BASE = "https://flags.faststats.dev";
4
+ function resolveBaseUrl(input, fallback) {
5
+ return input?.replace(/\/+$/, "") || fallback;
6
+ }
7
+ const URLS = {
8
+ events: "/v1/web",
9
+ identify: "/v1/identify",
10
+ replay: "/v1/replay",
11
+ vitals: "/v1/vitals",
12
+ flags: "/v1/check"
13
+ };
14
+ function sanitizeUrl(url, includeHash = false) {
15
+ try {
16
+ const parsed = new URL(url, location.href);
17
+ return `${parsed.origin}${parsed.pathname}${includeHash ? parsed.hash : ""}`;
18
+ } catch {
19
+ return url.split(includeHash ? "?" : /[?#]/, 1)[0] ?? "";
20
+ }
21
+ }
22
+ const UTM_KEYS = [
23
+ "utm_source",
24
+ "utm_medium",
25
+ "utm_campaign",
26
+ "utm_term",
27
+ "utm_content"
28
+ ];
29
+ function getPageContext(includeHash = false) {
30
+ const hash = includeHash ? location.hash : "";
31
+ const ctx = {
32
+ page: `${location.pathname}${hash}`,
33
+ url: sanitizeUrl(location.href, includeHash),
34
+ referrer: document.referrer ? sanitizeUrl(document.referrer) : null,
35
+ title: document.title || ""
36
+ };
37
+ if (!location.search) return ctx;
38
+ const sp = new URLSearchParams(location.search);
39
+ for (const key of UTM_KEYS) {
40
+ const value = sp.get(key);
41
+ if (value) ctx[key] = value;
42
+ }
43
+ return ctx;
44
+ }
45
+ //#endregion
46
+ export { resolveBaseUrl as a, getPageContext as i, FEATURE_FLAGS_BASE as n, sanitizeUrl as o, URLS as r, ANALYTICS_BASE as t };
@@ -0,0 +1,21 @@
1
+ import { d as setStorageItem, i as isCookielessMode, l as getStorageItem, n as createId, u as removeStorageItem } from "./send-data-DR1JUXka.js";
2
+ //#region src/utils/identifiers.ts
3
+ const ANONYMOUS_ID_KEY = "faststats_anon_id_";
4
+ function getOrCreateAnonymousId(siteKey) {
5
+ const key = `${ANONYMOUS_ID_KEY}${siteKey}`;
6
+ const existingId = getStorageItem("localStorage", key);
7
+ if (existingId) return existingId;
8
+ const newId = createId();
9
+ return setStorageItem("localStorage", key, newId) ? newId : "";
10
+ }
11
+ function getAnonymousId(siteKey, cookieless) {
12
+ if (cookieless ?? isCookielessMode()) return "";
13
+ return getOrCreateAnonymousId(siteKey);
14
+ }
15
+ function resetAnonymousId(siteKey, cookieless) {
16
+ if (cookieless) return "";
17
+ if (!removeStorageItem("localStorage", `${ANONYMOUS_ID_KEY}${siteKey}`)) return "";
18
+ return getOrCreateAnonymousId(siteKey);
19
+ }
20
+ //#endregion
21
+ export { resetAnonymousId as n, getAnonymousId as t };
@@ -1,12 +1,27 @@
1
1
  import { record } from "@rrweb/record";
2
2
  import { LogLevel } from "@rrweb/rrweb-plugin-console-record";
3
3
 
4
+ //#region ../../node_modules/.bun/@rrweb+types@2.1.0/node_modules/@rrweb/types/dist/index.d.ts
5
+ declare global {
6
+ interface Window {
7
+ FontFace: typeof FontFace;
8
+ }
9
+ }
10
+ //#endregion
11
+ //#region src/replay-mutation-throttler.d.ts
12
+ type ReplayMutationThrottleOptions = {
13
+ enabled?: boolean;
14
+ bucketSize?: number;
15
+ refillRate?: number;
16
+ };
17
+ //#endregion
4
18
  //#region src/replay.d.ts
5
19
  type RecordOptions = NonNullable<Parameters<typeof record>[0]>;
6
20
  interface ReplayTrackerOptions {
7
21
  siteKey: string;
8
22
  baseUrl?: string;
9
23
  debug?: boolean;
24
+ trackHash?: boolean;
10
25
  compress?: boolean;
11
26
  flushInterval?: number;
12
27
  maxEvents?: number;
@@ -29,6 +44,7 @@ interface ReplayTrackerOptions {
29
44
  consoleLevel?: LogLevel[];
30
45
  consoleLengthThreshold?: number;
31
46
  consoleStringLengthLimit?: number;
47
+ mutationThrottle?: ReplayMutationThrottleOptions;
32
48
  }
33
49
  declare class ReplayTracker {
34
50
  private readonly options;
@@ -43,43 +59,52 @@ declare class ReplayTracker {
43
59
  private readonly minReplayLengthMs;
44
60
  private readonly events;
45
61
  private readonly pending;
62
+ private readonly minLengthBlockedBatches;
46
63
  private queuedEventsSizeBytes;
47
64
  private pendingSizeBytes;
48
65
  private viewId;
66
+ private viewUrl;
49
67
  private chunkStartedAt;
50
68
  private started;
69
+ private disposed;
51
70
  private startGeneration;
52
71
  private startTime;
53
72
  private sequence;
54
73
  private intervalId;
55
74
  private flushTask;
56
75
  private retryTask;
76
+ private retryAttempt;
57
77
  private minLengthFlushTask;
58
78
  private stopRecording?;
59
79
  private sending;
60
- private flushQueued;
61
- private flushQueuedLowLatency;
80
+ private inFlightBatch;
81
+ private queuedFlush;
62
82
  private unsubscribeRotation?;
63
83
  private lastCookielessMode;
84
+ private terminalFlushRequested;
85
+ private overflowed;
86
+ private readonly mutationThrottler;
64
87
  constructor(options: ReplayTrackerOptions);
65
88
  private log;
66
89
  private clearFlushTimers;
67
90
  private clearQueues;
68
- private storage;
69
- private counterKey;
70
- private readCounter;
71
- private writeCounter;
91
+ private dropMinLengthBlockedBatches;
92
+ private unblockSessionBatches;
93
+ private finalizeSessionPending;
72
94
  private nextBatchSequence;
73
95
  setCookielessMode(cookieless: boolean): void;
74
96
  start(): void;
75
97
  trackPageChange(url?: string): void;
76
- onPageHidden(persisted?: boolean): void;
98
+ onPageHidden(persisted?: boolean, terminal?: boolean): void;
77
99
  onPageShow(persisted?: boolean): void;
78
100
  private beginRecording;
79
- stop(): void;
101
+ stop(discard?: boolean): void;
80
102
  private onSessionRotated;
103
+ private applySessionRotation;
104
+ private resetSessionTiming;
81
105
  private onEvent;
82
106
  private onUnload;
107
+ private requestTerminalFlush;
83
108
  private hasReachedMinLength;
84
109
  private requestFlush;
85
110
  private scheduleMinLengthFlush;
@@ -88,10 +113,11 @@ declare class ReplayTracker {
88
113
  private createBatch;
89
114
  private eventSizeBytes;
90
115
  private batchSizeBytes;
91
- private dropOldestPending;
116
+ private removePending;
92
117
  private enqueueBatch;
93
118
  private flush;
94
119
  private scheduleRetry;
120
+ private retryDelay;
95
121
  private sendBatch;
96
122
  }
97
123
  //#endregion
@@ -0,0 +1,201 @@
1
+ //#region src/utils/storage.ts
2
+ function getStorageItem(name, key) {
3
+ try {
4
+ return globalThis[name]?.getItem(key) ?? null;
5
+ } catch {
6
+ return null;
7
+ }
8
+ }
9
+ function setStorageItem(name, key, value) {
10
+ try {
11
+ const storage = globalThis[name];
12
+ if (!storage) return false;
13
+ storage.setItem(key, value);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ function removeStorageItem(name, key) {
20
+ try {
21
+ const storage = globalThis[name];
22
+ if (!storage) return false;
23
+ storage.removeItem(key);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ //#endregion
30
+ //#region src/utils/session-manager.ts
31
+ const IDLE_MS = 1800 * 1e3;
32
+ const MAX_AGE_MS = 1440 * 60 * 1e3;
33
+ const SESSION_KEY = "faststats_session_";
34
+ let cookielessMode = false;
35
+ const memorySessions = /* @__PURE__ */ new Map();
36
+ const rotationListeners = /* @__PURE__ */ new Set();
37
+ function createId() {
38
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
39
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
40
+ }
41
+ function readSession(siteKey) {
42
+ const memory = memorySessions.get(siteKey) ?? null;
43
+ const raw = getStorageItem("localStorage", `${SESSION_KEY}${siteKey}`);
44
+ if (!raw) return memory;
45
+ try {
46
+ const session = JSON.parse(raw);
47
+ return session.id ? session : memory;
48
+ } catch {}
49
+ return memory;
50
+ }
51
+ function writeSession(siteKey, session) {
52
+ if (!setStorageItem("localStorage", `${SESSION_KEY}${siteKey}`, JSON.stringify(session))) memorySessions.set(siteKey, session);
53
+ }
54
+ function clearSession(siteKey) {
55
+ memorySessions.delete(siteKey);
56
+ removeStorageItem("localStorage", `${SESSION_KEY}${siteKey}`);
57
+ }
58
+ function expired(session, now) {
59
+ return now - session.activity >= IDLE_MS || now - session.start >= MAX_AGE_MS;
60
+ }
61
+ function emitRotation(prev, next) {
62
+ for (const listener of rotationListeners) listener(prev, next);
63
+ }
64
+ function resolveSession(siteKey, cookieless, touch) {
65
+ const now = Date.now();
66
+ if (cookieless) {
67
+ let session = memorySessions.get(siteKey);
68
+ if (!session) {
69
+ session = {
70
+ id: createId(),
71
+ start: now,
72
+ activity: now
73
+ };
74
+ memorySessions.set(siteKey, session);
75
+ } else if (touch) session.activity = now;
76
+ return {
77
+ session,
78
+ rotatedFrom: null
79
+ };
80
+ }
81
+ const existing = readSession(siteKey);
82
+ if (existing && !expired(existing, now)) {
83
+ if (touch) {
84
+ existing.activity = now;
85
+ writeSession(siteKey, existing);
86
+ }
87
+ return {
88
+ session: existing,
89
+ rotatedFrom: null
90
+ };
91
+ }
92
+ const session = {
93
+ id: createId(),
94
+ start: now,
95
+ activity: now
96
+ };
97
+ writeSession(siteKey, session);
98
+ return {
99
+ session,
100
+ rotatedFrom: existing
101
+ };
102
+ }
103
+ function windowId(siteKey, cookieless, sessionId) {
104
+ if (cookieless) return sessionId;
105
+ const key = `faststats_window_id_${siteKey}`;
106
+ const existing = getStorageItem("sessionStorage", key);
107
+ if (existing) return existing;
108
+ const id = createId();
109
+ return setStorageItem("sessionStorage", key, id) ? id : sessionId;
110
+ }
111
+ function toContext(siteKey, session, cookieless, windowIdOverride) {
112
+ return {
113
+ sessionId: session.id,
114
+ windowId: windowIdOverride ?? windowId(siteKey, cookieless, session.id),
115
+ sessionStart: session.start
116
+ };
117
+ }
118
+ function setCookielessMode(mode) {
119
+ if (cookielessMode === mode) return;
120
+ cookielessMode = mode;
121
+ memorySessions.clear();
122
+ }
123
+ function isCookielessMode() {
124
+ return cookielessMode;
125
+ }
126
+ function getSessionContext(siteKey, cookieless) {
127
+ const useCookieless = cookieless ?? cookielessMode;
128
+ const { session, rotatedFrom } = resolveSession(siteKey, useCookieless, false);
129
+ const next = toContext(siteKey, session, useCookieless);
130
+ if (rotatedFrom) emitRotation(toContext(siteKey, rotatedFrom, useCookieless, next.windowId), next);
131
+ return next;
132
+ }
133
+ function touchActivity(siteKey, cookieless) {
134
+ const useCookieless = cookieless ?? cookielessMode;
135
+ const { session, rotatedFrom } = resolveSession(siteKey, useCookieless, true);
136
+ if (!rotatedFrom) return null;
137
+ const wid = windowId(siteKey, useCookieless, session.id);
138
+ const rotation = {
139
+ prev: toContext(siteKey, rotatedFrom, useCookieless, wid),
140
+ next: toContext(siteKey, session, useCookieless, wid)
141
+ };
142
+ emitRotation(rotation.prev, rotation.next);
143
+ return rotation;
144
+ }
145
+ function resetSession(siteKey) {
146
+ const useCookieless = cookielessMode;
147
+ const previous = getSessionContext(siteKey, useCookieless);
148
+ clearSession(siteKey);
149
+ if (useCookieless) {
150
+ const now = Date.now();
151
+ memorySessions.set(siteKey, {
152
+ id: createId(),
153
+ start: now,
154
+ activity: now
155
+ });
156
+ } else removeStorageItem("sessionStorage", `faststats_window_id_${siteKey}`);
157
+ const next = getSessionContext(siteKey, useCookieless);
158
+ if (previous.sessionId !== next.sessionId) emitRotation(previous, next);
159
+ return next;
160
+ }
161
+ function onSessionRotated(listener) {
162
+ rotationListeners.add(listener);
163
+ return () => rotationListeners.delete(listener);
164
+ }
165
+ //#endregion
166
+ //#region src/utils/send-data.ts
167
+ async function sendData(options) {
168
+ const { url, data, contentType = "application/json", headers = {}, debug = false, debugPrefix = "[Analytics]", useBeacon = true, keepalive = true, signal } = options;
169
+ if (signal?.aborted) return false;
170
+ if (useBeacon && typeof globalThis.navigator?.sendBeacon === "function") try {
171
+ const beaconBody = data instanceof Blob || typeof Blob === "undefined" ? data : new Blob([data], { type: contentType });
172
+ if (globalThis.navigator.sendBeacon(url, beaconBody)) {
173
+ if (debug) console.log(`${debugPrefix} Sent via beacon`);
174
+ return true;
175
+ }
176
+ } catch {}
177
+ try {
178
+ const response = await fetch(url, {
179
+ method: "POST",
180
+ body: data,
181
+ headers: {
182
+ "Content-Type": contentType,
183
+ ...headers
184
+ },
185
+ keepalive,
186
+ credentials: "omit",
187
+ signal
188
+ });
189
+ const success = response.ok;
190
+ if (debug) {
191
+ const message = success ? `${debugPrefix} Sent via fetch` : `${debugPrefix} Failed: ${response.status}`;
192
+ console[success ? "log" : "warn"](message);
193
+ }
194
+ return success;
195
+ } catch {
196
+ if (debug) console.warn(`${debugPrefix} Failed to send`);
197
+ return false;
198
+ }
199
+ }
200
+ //#endregion
201
+ export { onSessionRotated as a, touchActivity as c, setStorageItem as d, isCookielessMode as i, getStorageItem as l, createId as n, resetSession as o, getSessionContext as r, setCookielessMode as s, sendData as t, removeStorageItem as u };
package/dist/error.d.ts CHANGED
@@ -9,6 +9,7 @@ interface ErrorTrackingOptions {
9
9
  siteKey: string;
10
10
  baseUrl?: string;
11
11
  debug?: boolean;
12
+ trackHash?: boolean;
12
13
  flushInterval?: number;
13
14
  maxQueueSize?: number;
14
15
  sdkName?: string;
@@ -24,7 +25,7 @@ declare class ErrorTracker {
24
25
  constructor(options: ErrorTrackingOptions);
25
26
  private log;
26
27
  start(): void;
27
- stop(): void;
28
+ stop(discard?: boolean): void;
28
29
  onPageHidden(persisted?: boolean): void;
29
30
  captureError(error: Error): void;
30
31
  private onError;
package/dist/error.js CHANGED
@@ -1,2 +1,135 @@
1
- import{a as e,i as t,r as n,t as r}from"./chunks/api-urls-Bh77rElT.js";import{r as i,t as a}from"./chunks/send-data-CIbuTxPh.js";import{t as o}from"./chunks/identifiers-DcGHdsnG.js";const s=/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;function c(e,t,n,r){return t instanceof Error?{error:e,message:t.message||r,handled:n,stack:t.stack?.split(`
2
- `).map(e=>e.trim()).filter(Boolean)}:{error:e,message:typeof t==`string`?t:r,handled:n}}function l({error:e,message:t,handled:n}){return`${e}\0${t??``}\0${n}`}function u(e,t){let n=t instanceof Error?t.stack??``:``;return s.test(`${e}\n${n}`)}var d=class{options;endpoint;queue=new Map;timer=null;started=!1;flushing=!1;constructor(t){this.options=t,this.endpoint=`${e(t.baseUrl,r)}${n.events}`}log(...e){this.options.debug&&console.log(`[ErrorTracker]`,...e)}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.onError),window.addEventListener(`unhandledrejection`,this.onRejection),this.timer=setInterval(()=>void this.flush(),this.options.flushInterval??5e3),this.log(`Started`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.onError),window.removeEventListener(`unhandledrejection`,this.onRejection),this.timer&&clearInterval(this.timer),this.timer=null,this.flush(),this.log(`Stopped`))}onPageHidden(e=!1){e||this.flush()}captureError(e){this.enqueue(c(`Error`,e,!0,e.message))}onError=e=>{u(e.filename??``,e.error)||this.enqueue(c(`Error`,e.error,!1,e.message||`Unknown error`))};onRejection=e=>{u(``,e.reason)||this.enqueue(c(`UnhandledRejection`,e.reason,!1,`Unhandled promise rejection`))};enqueue(e){let t=l(e),n=this.queue.get(t);if(n){n.count++;return}this.queue.set(t,{...e,count:1}),this.log(`Captured:`,e),this.queue.size>=(this.options.maxQueueSize??50)&&this.flush()}async flush(){if(this.flushing||this.queue.size===0)return;this.flushing=!0;let e=this.queue;this.queue=new Map;let n=[...e.values()];this.log(`Flushing:`,n);try{let r=globalThis.__SOURCEMAPS_BUILD__?.buildId,s=o(),c=i(this.options.siteKey);if(!await a({url:this.endpoint,data:JSON.stringify({token:this.options.siteKey,...s?{userId:s}:{},sessionId:c.sessionId,windowId:c.windowId,...typeof r==`string`&&r.trim()?{buildId:r}:{},sdkName:this.options.sdkName??`@faststats/web`,sdkVersion:this.options.sdkVersion??`0.5.0`,event:`error`,...t(),properties:{},errors:n}),debug:this.options.debug,debugPrefix:`[ErrorTracker]`}))for(let[t,n]of e){let e=this.queue.get(t);e?e.count+=n.count:this.queue.set(t,n)}}finally{this.flushing=!1}}};export{d as default};
1
+ import { a as resolveBaseUrl, i as getPageContext, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-Dp2XNjRF.js";
2
+ import { r as getSessionContext, t as sendData } from "./chunks/send-data-DR1JUXka.js";
3
+ import { t as getAnonymousId } from "./chunks/identifiers-BvRNbiwA.js";
4
+ //#region src/error.ts
5
+ const SDK_NAME = "@faststats/web";
6
+ const SDK_VERSION = "0.7.0";
7
+ const EXTENSION_RE = /(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;
8
+ function toEntry(kind, source, handled, fallback) {
9
+ if (source instanceof Error) return {
10
+ error: kind === "UnhandledRejection" ? kind : source.name.trim() || kind,
11
+ message: source.message || fallback,
12
+ handled,
13
+ stack: source.stack?.split("\n").map((line) => line.trim()).filter(Boolean)
14
+ };
15
+ return {
16
+ error: kind,
17
+ message: typeof source === "string" ? source : fallback,
18
+ handled
19
+ };
20
+ }
21
+ function isExtensionError(filename, source) {
22
+ const stack = source instanceof Error ? source.stack ?? "" : "";
23
+ return EXTENSION_RE.test(`${filename}\n${stack}`);
24
+ }
25
+ var ErrorTracker = class {
26
+ options;
27
+ endpoint;
28
+ queue = [];
29
+ timer = null;
30
+ started = false;
31
+ flushing = false;
32
+ constructor(options) {
33
+ this.options = options;
34
+ this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.events}`;
35
+ }
36
+ log(...args) {
37
+ if (this.options.debug) console.log("[ErrorTracker]", ...args);
38
+ }
39
+ start() {
40
+ if (this.started || typeof window === "undefined") return;
41
+ this.started = true;
42
+ window.addEventListener("error", this.onError);
43
+ window.addEventListener("unhandledrejection", this.onRejection);
44
+ this.timer = setInterval(() => void this.flush(), this.options.flushInterval ?? 5e3);
45
+ this.log("Started");
46
+ }
47
+ stop(discard = false) {
48
+ if (!this.started || typeof window === "undefined") return;
49
+ this.started = false;
50
+ window.removeEventListener("error", this.onError);
51
+ window.removeEventListener("unhandledrejection", this.onRejection);
52
+ if (this.timer) clearInterval(this.timer);
53
+ this.timer = null;
54
+ if (discard) this.queue = [];
55
+ else this.flush();
56
+ this.log("Stopped");
57
+ }
58
+ onPageHidden(persisted = false) {
59
+ if (!persisted) this.flush();
60
+ }
61
+ captureError(error) {
62
+ this.enqueue(toEntry("Error", error, true, error.message));
63
+ }
64
+ onError = (event) => {
65
+ if (isExtensionError(event.filename ?? "", event.error)) return;
66
+ this.enqueue(toEntry("Error", event.error, false, event.message || "Unknown error"));
67
+ };
68
+ onRejection = (event) => {
69
+ if (isExtensionError("", event.reason)) return;
70
+ this.enqueue(toEntry("UnhandledRejection", event.reason, false, "Unhandled promise rejection"));
71
+ };
72
+ enqueue(error) {
73
+ const buildId = globalThis.__SOURCEMAPS_BUILD__?.buildId;
74
+ const userId = getAnonymousId(this.options.siteKey);
75
+ const ctx = getSessionContext(this.options.siteKey);
76
+ this.log("Captured:", error);
77
+ this.queue.push({
78
+ error,
79
+ context: {
80
+ token: this.options.siteKey,
81
+ ...userId ? { userId } : {},
82
+ sessionId: ctx.sessionId,
83
+ windowId: ctx.windowId,
84
+ ...typeof buildId === "string" && buildId.trim() ? { buildId } : {},
85
+ sdkName: this.options.sdkName ?? SDK_NAME,
86
+ sdkVersion: this.options.sdkVersion ?? SDK_VERSION,
87
+ event: "error",
88
+ ...getPageContext(this.options.trackHash ?? false),
89
+ properties: {}
90
+ }
91
+ });
92
+ if (this.queue.length >= (this.options.maxQueueSize ?? 50)) this.flush();
93
+ }
94
+ async flush() {
95
+ if (this.flushing || !this.queue.length) return;
96
+ this.flushing = true;
97
+ const batch = this.queue;
98
+ this.queue = [];
99
+ const groups = /* @__PURE__ */ new Map();
100
+ for (const item of batch) {
101
+ const key = JSON.stringify(item.context);
102
+ const group = groups.get(key) ?? {
103
+ context: item.context,
104
+ items: []
105
+ };
106
+ group.items.push(item);
107
+ groups.set(key, group);
108
+ }
109
+ const failed = await Promise.all([...groups.values()].map(async (group) => {
110
+ const errors = /* @__PURE__ */ new Map();
111
+ for (const { error } of group.items) {
112
+ const key = JSON.stringify(error);
113
+ const existing = errors.get(key);
114
+ if (existing) existing.count++;
115
+ else errors.set(key, {
116
+ ...error,
117
+ count: 1
118
+ });
119
+ }
120
+ return await sendData({
121
+ url: this.endpoint,
122
+ data: JSON.stringify({
123
+ ...group.context,
124
+ errors: [...errors.values()]
125
+ }),
126
+ debug: this.options.debug,
127
+ debugPrefix: "[ErrorTracker]"
128
+ }) ? [] : group.items;
129
+ }));
130
+ this.queue.unshift(...failed.flat());
131
+ this.flushing = false;
132
+ }
133
+ };
134
+ //#endregion
135
+ export { ErrorTracker as default };
@@ -1 +1,32 @@
1
- import{a as e,n as t,r as n}from"./chunks/api-urls-Bh77rElT.js";async function r(r,i){if(!i.projectToken)throw Error(`feature flag check requires projectToken`);if(!i.identifier&&!i.externalId)throw Error(`feature flag check requires serverId or externalId`);let{baseUrl:a,projectToken:o,signal:s,attributes:c,...l}=i,u={key:r,...l};c&&Object.keys(c).length>0&&(u.attributes=c);let d={"Content-Type":`application/json`,Authorization:`Bearer ${o}`},f=`${e(a,t)}${n.flags}`,p=await fetch(f,{method:`POST`,headers:d,body:JSON.stringify(u),credentials:`omit`,signal:s});if(!p.ok){let e=await p.json().catch(()=>null),t=typeof e?.error==`string`&&e.error.length>0?` — ${e.error}`:``;throw Error(`feature flag check failed: HTTP ${p.status}${t}`)}return await p.json()}export{r as fetchFeatureFlagEvaluation};
1
+ import { a as resolveBaseUrl, n as FEATURE_FLAGS_BASE, r as URLS } from "./chunks/api-urls-Dp2XNjRF.js";
2
+ //#region src/feature-flags.ts
3
+ async function fetchFeatureFlagEvaluation(flagKey, context) {
4
+ if (!context.projectToken) throw new Error("feature flag check requires projectToken");
5
+ if (!context.identifier && !context.externalId) throw new Error("feature flag check requires serverId or externalId");
6
+ const { baseUrl, projectToken, signal, attributes, ...bodyFields } = context;
7
+ const body = {
8
+ key: flagKey,
9
+ ...bodyFields
10
+ };
11
+ if (attributes && Object.keys(attributes).length > 0) body.attributes = attributes;
12
+ const headers = {
13
+ "Content-Type": "application/json",
14
+ Authorization: `Bearer ${projectToken}`
15
+ };
16
+ const url = `${resolveBaseUrl(baseUrl, FEATURE_FLAGS_BASE)}${URLS.flags}`;
17
+ const res = await fetch(url, {
18
+ method: "POST",
19
+ headers,
20
+ body: JSON.stringify(body),
21
+ credentials: "omit",
22
+ signal
23
+ });
24
+ if (!res.ok) {
25
+ const err = await res.json().catch(() => null);
26
+ const detail = typeof err?.error === "string" && err.error.length > 0 ? ` — ${err.error}` : "";
27
+ throw new Error(`feature flag check failed: HTTP ${res.status}${detail}`);
28
+ }
29
+ return await res.json();
30
+ }
31
+ //#endregion
32
+ export { fetchFeatureFlagEvaluation };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FeatureFlagEvaluation } from "./feature-flags.js";
2
- import { n as ReplayTrackerOptions } from "./chunks/replay-CLfrGjqj.js";
2
+ import { n as ReplayTrackerOptions } from "./chunks/replay-BeC2XA4N.js";
3
3
 
4
4
  //#region src/analytics.d.ts
5
5
  type ConsentMode = "pending" | "granted" | "denied";
@@ -58,8 +58,9 @@ declare class WebAnalytics {
58
58
  private pageKey;
59
59
  private entry;
60
60
  private left;
61
- private initialPageviewPending;
62
- private initialPageviewTrigger;
61
+ private visitDuration;
62
+ private visitStartedAt;
63
+ private pendingPageviewTrigger;
63
64
  private consentMode;
64
65
  private readonly pendingBehavior;
65
66
  private readonly cleanup;
@@ -74,8 +75,10 @@ declare class WebAnalytics {
74
75
  private cookieless;
75
76
  private touch;
76
77
  private bind;
78
+ private stopHeartbeat;
77
79
  private loadChild;
78
80
  private loadOptionalTrackers;
81
+ private enterPage;
79
82
  private beginTracking;
80
83
  private readonly onLinkClick;
81
84
  private readonly onVisibilityChange;
@@ -99,6 +102,9 @@ declare class WebAnalytics {
99
102
  reportError(error: Error): void;
100
103
  private send;
101
104
  private leavePage;
105
+ private startVisit;
106
+ private pauseVisit;
107
+ private resumeVisit;
102
108
  private startHeartbeat;
103
109
  private navigate;
104
110
  }