@faststats/web 0.6.0 → 0.8.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 +67 -0
- package/dist/chunks/api-urls-DWk43fu6.js +53 -0
- package/dist/chunks/extensions-DW7tJY0T.d.ts +51 -0
- package/dist/chunks/identifiers-KgQDX74Q.js +21 -0
- package/dist/chunks/send-data-DFZnsaL2.js +219 -0
- package/dist/error.d.ts +9 -4
- package/dist/error.js +89 -59
- package/dist/index.d.ts +54 -72
- package/dist/index.js +349 -353
- package/dist/outbound-links.d.ts +10 -0
- package/dist/outbound-links.js +62 -0
- package/dist/replay.d.ts +128 -2
- package/dist/replay.js +154 -62
- package/dist/web-vitals.d.ts +10 -5
- package/dist/web-vitals.js +56 -50
- package/package.json +11 -12
- package/dist/chunks/api-urls-CaNa8upY.js +0 -45
- package/dist/chunks/identifiers-cUymVK5H.js +0 -20
- package/dist/chunks/replay-DgodPHzA.d.ts +0 -104
- package/dist/chunks/send-data-DjdbGDDc.js +0 -196
- package/dist/feature-flags.d.ts +0 -18
- package/dist/feature-flags.js +0 -32
package/dist/web-vitals.js
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { r as getSessionContext, t as sendData } from "./chunks/send-data-
|
|
1
|
+
import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
|
|
2
|
+
import { r as getSessionContext, t as sendData } from "./chunks/send-data-DFZnsaL2.js";
|
|
3
3
|
//#region src/web-vitals.ts
|
|
4
|
+
function webVitals(options = {}) {
|
|
5
|
+
return {
|
|
6
|
+
name: "web-vitals",
|
|
7
|
+
async setup(context) {
|
|
8
|
+
const tracker = new WebVitalsTracker({
|
|
9
|
+
...options,
|
|
10
|
+
siteKey: context.siteKey,
|
|
11
|
+
baseUrl: context.baseUrl,
|
|
12
|
+
debug: context.debug,
|
|
13
|
+
trackHash: context.trackHash,
|
|
14
|
+
cookieless: context.isCookieless()
|
|
15
|
+
});
|
|
16
|
+
context.on("pageHide", ({ persisted }) => tracker.onPageHidden(persisted));
|
|
17
|
+
context.on("consentChange", ({ cookieless }) => tracker.setCookielessMode(cookieless));
|
|
18
|
+
await tracker.start();
|
|
19
|
+
return ({ discard }) => tracker.stop(discard);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
4
23
|
const METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
5
24
|
"CLS",
|
|
6
25
|
"INP",
|
|
@@ -15,50 +34,51 @@ var WebVitalsTracker = class {
|
|
|
15
34
|
started = false;
|
|
16
35
|
flushing = false;
|
|
17
36
|
initialUrl = "";
|
|
18
|
-
|
|
37
|
+
session = null;
|
|
38
|
+
cookieless;
|
|
19
39
|
constructor(options) {
|
|
20
40
|
this.options = options;
|
|
21
41
|
this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.vitals}`;
|
|
42
|
+
this.cookieless = options.cookieless ?? false;
|
|
43
|
+
}
|
|
44
|
+
setCookielessMode(cookieless) {
|
|
45
|
+
if (this.cookieless === cookieless) return;
|
|
46
|
+
this.cookieless = cookieless;
|
|
47
|
+
this.metrics.clear();
|
|
48
|
+
this.session = this.started ? getSessionContext(this.options.siteKey, cookieless) : null;
|
|
22
49
|
}
|
|
23
50
|
async start() {
|
|
24
51
|
if (this.started || typeof window === "undefined") return;
|
|
25
52
|
this.started = true;
|
|
26
|
-
this.initialUrl = sanitizeUrl(window.location.href);
|
|
27
|
-
this.
|
|
28
|
-
const
|
|
53
|
+
this.initialUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
|
|
54
|
+
this.session = getSessionContext(this.options.siteKey, this.cookieless);
|
|
55
|
+
const vitals = this.options.attribution ? await import("web-vitals/attribution") : await import("web-vitals");
|
|
29
56
|
if (!this.started) return;
|
|
30
57
|
const changes = { reportAllChanges: true };
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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);
|
|
36
63
|
}
|
|
37
|
-
stop() {
|
|
64
|
+
stop(discard = false) {
|
|
38
65
|
if (!this.started) return;
|
|
39
66
|
this.started = false;
|
|
40
|
-
this.
|
|
67
|
+
if (discard) this.metrics.clear();
|
|
68
|
+
else this.flush();
|
|
41
69
|
}
|
|
42
70
|
onPageHidden(persisted = false) {
|
|
43
71
|
if (persisted) return;
|
|
44
|
-
this.flush();
|
|
45
|
-
}
|
|
46
|
-
trackPageChange(url) {
|
|
47
|
-
if (!this.started) return;
|
|
48
|
-
const next = sanitizeUrl(url ?? window.location.href);
|
|
49
|
-
if (next === this.currentUrl) return;
|
|
50
|
-
this.currentUrl = next;
|
|
72
|
+
queueMicrotask(() => void this.flush());
|
|
51
73
|
}
|
|
52
74
|
onMetric = (metric) => {
|
|
53
75
|
if (!this.started) return;
|
|
54
76
|
const name = metric.name;
|
|
55
77
|
if (!METRIC_NAMES.has(name) || !Number.isFinite(metric.value) || metric.value < 0) return;
|
|
56
|
-
const url = this.getMetricUrl(name);
|
|
57
78
|
const { id, rating, delta, navigationType } = metric;
|
|
58
|
-
const attribution = "attribution" in metric && metric.attribution ? metric.attribution : void 0;
|
|
59
|
-
this.metrics.set(
|
|
79
|
+
const attribution = this.options.attribution && "attribution" in metric && metric.attribution ? metric.attribution : void 0;
|
|
80
|
+
this.metrics.set(name, {
|
|
60
81
|
metric: name,
|
|
61
|
-
url,
|
|
62
82
|
value: metric.value,
|
|
63
83
|
attributes: {
|
|
64
84
|
id,
|
|
@@ -69,47 +89,33 @@ var WebVitalsTracker = class {
|
|
|
69
89
|
}
|
|
70
90
|
});
|
|
71
91
|
};
|
|
72
|
-
getMetricUrl(metric) {
|
|
73
|
-
if (metric === "FCP" || metric === "LCP" || metric === "TTFB") return this.initialUrl;
|
|
74
|
-
return this.currentUrl || this.initialUrl;
|
|
75
|
-
}
|
|
76
92
|
async flush() {
|
|
77
93
|
if (!this.metrics.size || this.flushing) return;
|
|
78
94
|
this.flushing = true;
|
|
79
95
|
const batch = this.metrics;
|
|
80
96
|
this.metrics = /* @__PURE__ */ new Map();
|
|
81
|
-
const session = getSessionContext(this.options.siteKey);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
vitals.push(vital);
|
|
86
|
-
byUrl.set(vital.url, vitals);
|
|
87
|
-
}
|
|
88
|
-
const results = await Promise.all([...byUrl].map(async ([url, vitals]) => ({
|
|
89
|
-
ok: await sendData({
|
|
97
|
+
const session = this.session ?? getSessionContext(this.options.siteKey, this.cookieless);
|
|
98
|
+
let sent = false;
|
|
99
|
+
try {
|
|
100
|
+
sent = await sendData({
|
|
90
101
|
url: this.endpoint,
|
|
91
102
|
data: JSON.stringify({
|
|
92
103
|
token: this.options.siteKey,
|
|
93
104
|
sessionId: session.sessionId,
|
|
94
105
|
windowId: session.windowId,
|
|
95
|
-
vitals:
|
|
96
|
-
metadata: { url }
|
|
106
|
+
vitals: [...batch.values()],
|
|
107
|
+
metadata: { url: this.initialUrl }
|
|
97
108
|
}),
|
|
98
109
|
debug: this.options.debug,
|
|
99
110
|
debugPrefix: "[WebVitals]"
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for (const result of results) {
|
|
105
|
-
if (result.ok) continue;
|
|
106
|
-
for (const vital of result.vitals) {
|
|
107
|
-
const key = `${result.url}\n${vital.metric}`;
|
|
108
|
-
if (!this.metrics.has(key)) this.metrics.set(key, vital);
|
|
111
|
+
});
|
|
112
|
+
} finally {
|
|
113
|
+
if (!sent) {
|
|
114
|
+
for (const [name, vital] of batch) if (!this.metrics.has(name)) this.metrics.set(name, vital);
|
|
109
115
|
}
|
|
116
|
+
this.flushing = false;
|
|
110
117
|
}
|
|
111
|
-
this.flushing = false;
|
|
112
118
|
}
|
|
113
119
|
};
|
|
114
120
|
//#endregion
|
|
115
|
-
export { WebVitalsTracker as default };
|
|
121
|
+
export { WebVitalsTracker as default, webVitals };
|
package/package.json
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"import": "./dist/index.js",
|
|
14
14
|
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
|
-
"./
|
|
17
|
-
"types": "./dist/
|
|
18
|
-
"import": "./dist/
|
|
19
|
-
"default": "./dist/
|
|
16
|
+
"./outbound-links": {
|
|
17
|
+
"types": "./dist/outbound-links.d.ts",
|
|
18
|
+
"import": "./dist/outbound-links.js",
|
|
19
|
+
"default": "./dist/outbound-links.js"
|
|
20
20
|
},
|
|
21
21
|
"./replay": {
|
|
22
22
|
"types": "./dist/replay.d.ts",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
|
-
"version": "0.
|
|
45
|
+
"version": "0.8.0",
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsdown && bun run check-size",
|
|
48
48
|
"dev": "bun run build",
|
|
@@ -51,15 +51,14 @@
|
|
|
51
51
|
"check-size": "node scripts/check-bundle-size.mjs"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@rrweb/types": "^2.
|
|
55
|
-
"@types/bun": "
|
|
56
|
-
"tsdown": "^0.22.
|
|
54
|
+
"@rrweb/types": "^2.1.1",
|
|
55
|
+
"@types/bun": "^1.4.0",
|
|
56
|
+
"tsdown": "^0.22.14",
|
|
57
57
|
"typescript": "^6.0.3"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@rrweb/record": "^2.1.
|
|
61
|
-
"@rrweb/rrweb-plugin-console-record": "^2.1.
|
|
62
|
-
"
|
|
63
|
-
"web-vitals": "^6.0.0"
|
|
60
|
+
"@rrweb/record": "^2.1.1",
|
|
61
|
+
"@rrweb/rrweb-plugin-console-record": "^2.1.1",
|
|
62
|
+
"web-vitals": "^6.2.1"
|
|
64
63
|
}
|
|
65
64
|
}
|
|
@@ -1,45 +0,0 @@
|
|
|
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) {
|
|
15
|
-
try {
|
|
16
|
-
const parsed = new URL(url, location.href);
|
|
17
|
-
return `${parsed.origin}${parsed.pathname}`;
|
|
18
|
-
} catch {
|
|
19
|
-
return url.split(/[?#]/, 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() {
|
|
30
|
-
const ctx = {
|
|
31
|
-
page: location.pathname,
|
|
32
|
-
url: sanitizeUrl(location.href),
|
|
33
|
-
referrer: document.referrer ? sanitizeUrl(document.referrer) : null,
|
|
34
|
-
title: document.title || ""
|
|
35
|
-
};
|
|
36
|
-
if (!location.search) return ctx;
|
|
37
|
-
const sp = new URLSearchParams(location.search);
|
|
38
|
-
for (const key of UTM_KEYS) {
|
|
39
|
-
const value = sp.get(key);
|
|
40
|
-
if (value) ctx[key] = value;
|
|
41
|
-
}
|
|
42
|
-
return ctx;
|
|
43
|
-
}
|
|
44
|
-
//#endregion
|
|
45
|
-
export { resolveBaseUrl as a, getPageContext as i, FEATURE_FLAGS_BASE as n, sanitizeUrl as o, URLS as r, ANALYTICS_BASE as t };
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { d as setStorageItem, i as isCookielessMode, l as getStorageItem, n as createId, u as removeStorageItem } from "./send-data-DjdbGDDc.js";
|
|
2
|
-
//#region src/utils/identifiers.ts
|
|
3
|
-
const ANONYMOUS_ID_KEY = "faststats_anon_id";
|
|
4
|
-
function getOrCreateAnonymousId() {
|
|
5
|
-
const existingId = getStorageItem("localStorage", ANONYMOUS_ID_KEY);
|
|
6
|
-
if (existingId) return existingId;
|
|
7
|
-
const newId = createId();
|
|
8
|
-
return setStorageItem("localStorage", ANONYMOUS_ID_KEY, newId) ? newId : "";
|
|
9
|
-
}
|
|
10
|
-
function getAnonymousId(cookieless) {
|
|
11
|
-
if (cookieless ?? isCookielessMode()) return "";
|
|
12
|
-
return getOrCreateAnonymousId();
|
|
13
|
-
}
|
|
14
|
-
function resetAnonymousId(cookieless) {
|
|
15
|
-
if (cookieless) return "";
|
|
16
|
-
if (!removeStorageItem("localStorage", ANONYMOUS_ID_KEY)) return "";
|
|
17
|
-
return getOrCreateAnonymousId();
|
|
18
|
-
}
|
|
19
|
-
//#endregion
|
|
20
|
-
export { resetAnonymousId as n, getAnonymousId as t };
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { record } from "@rrweb/record";
|
|
2
|
-
import { LogLevel } from "@rrweb/rrweb-plugin-console-record";
|
|
3
|
-
|
|
4
|
-
//#region src/replay.d.ts
|
|
5
|
-
type RecordOptions = NonNullable<Parameters<typeof record>[0]>;
|
|
6
|
-
interface ReplayTrackerOptions {
|
|
7
|
-
siteKey: string;
|
|
8
|
-
baseUrl?: string;
|
|
9
|
-
debug?: boolean;
|
|
10
|
-
compress?: boolean;
|
|
11
|
-
flushInterval?: number;
|
|
12
|
-
maxEvents?: number;
|
|
13
|
-
maxBatchSizeBytes?: number;
|
|
14
|
-
maxLowLatencyBatchSizeBytes?: number;
|
|
15
|
-
maxPendingBatches?: number;
|
|
16
|
-
maxQueueSizeBytes?: number;
|
|
17
|
-
minReplayLengthMs?: number;
|
|
18
|
-
sampling?: RecordOptions["sampling"];
|
|
19
|
-
slimDOMOptions?: RecordOptions["slimDOMOptions"];
|
|
20
|
-
maskAllInputs?: boolean;
|
|
21
|
-
maskInputOptions?: RecordOptions["maskInputOptions"];
|
|
22
|
-
blockClass?: string;
|
|
23
|
-
blockSelector?: string;
|
|
24
|
-
maskTextClass?: string;
|
|
25
|
-
maskTextSelector?: string;
|
|
26
|
-
checkoutEveryNms?: number;
|
|
27
|
-
checkoutEveryNth?: number;
|
|
28
|
-
recordConsole?: boolean;
|
|
29
|
-
consoleLevel?: LogLevel[];
|
|
30
|
-
consoleLengthThreshold?: number;
|
|
31
|
-
consoleStringLengthLimit?: number;
|
|
32
|
-
}
|
|
33
|
-
declare class ReplayTracker {
|
|
34
|
-
private readonly options;
|
|
35
|
-
private readonly endpoint;
|
|
36
|
-
private readonly compressionSupported;
|
|
37
|
-
private readonly flushInterval;
|
|
38
|
-
private readonly maxEvents;
|
|
39
|
-
private readonly maxBatchSizeBytes;
|
|
40
|
-
private readonly maxLowLatencyBatchSizeBytes;
|
|
41
|
-
private readonly maxPendingBatches;
|
|
42
|
-
private readonly maxQueueSizeBytes;
|
|
43
|
-
private readonly minReplayLengthMs;
|
|
44
|
-
private readonly events;
|
|
45
|
-
private readonly pending;
|
|
46
|
-
private readonly minLengthBlockedBatches;
|
|
47
|
-
private queuedEventsSizeBytes;
|
|
48
|
-
private pendingSizeBytes;
|
|
49
|
-
private viewId;
|
|
50
|
-
private chunkStartedAt;
|
|
51
|
-
private started;
|
|
52
|
-
private disposed;
|
|
53
|
-
private startGeneration;
|
|
54
|
-
private startTime;
|
|
55
|
-
private sequence;
|
|
56
|
-
private intervalId;
|
|
57
|
-
private flushTask;
|
|
58
|
-
private retryTask;
|
|
59
|
-
private retryAttempt;
|
|
60
|
-
private minLengthFlushTask;
|
|
61
|
-
private stopRecording?;
|
|
62
|
-
private sending;
|
|
63
|
-
private inFlightBatch;
|
|
64
|
-
private queuedFlush;
|
|
65
|
-
private unsubscribeRotation?;
|
|
66
|
-
private lastCookielessMode;
|
|
67
|
-
constructor(options: ReplayTrackerOptions);
|
|
68
|
-
private log;
|
|
69
|
-
private clearFlushTimers;
|
|
70
|
-
private clearQueues;
|
|
71
|
-
private dropMinLengthBlockedBatches;
|
|
72
|
-
private unblockSessionBatches;
|
|
73
|
-
private finalizeSessionPending;
|
|
74
|
-
private nextBatchSequence;
|
|
75
|
-
setCookielessMode(cookieless: boolean): void;
|
|
76
|
-
start(): void;
|
|
77
|
-
trackPageChange(url?: string): void;
|
|
78
|
-
onPageHidden(persisted?: boolean): void;
|
|
79
|
-
onPageShow(persisted?: boolean): void;
|
|
80
|
-
private beginRecording;
|
|
81
|
-
stop(): void;
|
|
82
|
-
private onSessionRotated;
|
|
83
|
-
private applySessionRotation;
|
|
84
|
-
private resetSessionTiming;
|
|
85
|
-
private onEvent;
|
|
86
|
-
private onUnload;
|
|
87
|
-
private hasReachedMinLength;
|
|
88
|
-
private requestFlush;
|
|
89
|
-
private scheduleMinLengthFlush;
|
|
90
|
-
private enqueueEventsIfReady;
|
|
91
|
-
private takeEventBatches;
|
|
92
|
-
private createBatch;
|
|
93
|
-
private eventSizeBytes;
|
|
94
|
-
private batchSizeBytes;
|
|
95
|
-
private dropOldestPending;
|
|
96
|
-
private removePending;
|
|
97
|
-
private enqueueBatch;
|
|
98
|
-
private flush;
|
|
99
|
-
private scheduleRetry;
|
|
100
|
-
private retryDelay;
|
|
101
|
-
private sendBatch;
|
|
102
|
-
}
|
|
103
|
-
//#endregion
|
|
104
|
-
export { ReplayTrackerOptions as n, ReplayTracker as t };
|
|
@@ -1,196 +0,0 @@
|
|
|
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
|
-
let memorySession = null;
|
|
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() {
|
|
42
|
-
const raw = getStorageItem("localStorage", SESSION_KEY);
|
|
43
|
-
if (!raw) return memorySession;
|
|
44
|
-
try {
|
|
45
|
-
const session = JSON.parse(raw);
|
|
46
|
-
return session.id ? session : memorySession;
|
|
47
|
-
} catch {}
|
|
48
|
-
return memorySession;
|
|
49
|
-
}
|
|
50
|
-
function writeSession(session) {
|
|
51
|
-
if (!setStorageItem("localStorage", SESSION_KEY, JSON.stringify(session))) memorySession = session;
|
|
52
|
-
}
|
|
53
|
-
function clearSession() {
|
|
54
|
-
memorySession = null;
|
|
55
|
-
removeStorageItem("localStorage", SESSION_KEY);
|
|
56
|
-
}
|
|
57
|
-
function expired(session, now) {
|
|
58
|
-
return now - session.activity >= IDLE_MS || now - session.start >= MAX_AGE_MS;
|
|
59
|
-
}
|
|
60
|
-
function emitRotation(prev, next) {
|
|
61
|
-
for (const listener of rotationListeners) listener(prev, next);
|
|
62
|
-
}
|
|
63
|
-
function resolveSession(cookieless, touch) {
|
|
64
|
-
const now = Date.now();
|
|
65
|
-
if (cookieless) {
|
|
66
|
-
if (!memorySession) memorySession = {
|
|
67
|
-
id: createId(),
|
|
68
|
-
start: now,
|
|
69
|
-
activity: now
|
|
70
|
-
};
|
|
71
|
-
else if (touch) memorySession.activity = now;
|
|
72
|
-
return {
|
|
73
|
-
session: memorySession,
|
|
74
|
-
rotatedFrom: null
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
const existing = readSession();
|
|
78
|
-
if (existing && !expired(existing, now)) {
|
|
79
|
-
if (touch) {
|
|
80
|
-
existing.activity = now;
|
|
81
|
-
writeSession(existing);
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
session: existing,
|
|
85
|
-
rotatedFrom: null
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
const session = {
|
|
89
|
-
id: createId(),
|
|
90
|
-
start: now,
|
|
91
|
-
activity: now
|
|
92
|
-
};
|
|
93
|
-
writeSession(session);
|
|
94
|
-
return {
|
|
95
|
-
session,
|
|
96
|
-
rotatedFrom: existing
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function windowId(siteKey, cookieless, sessionId) {
|
|
100
|
-
if (cookieless) return sessionId;
|
|
101
|
-
const key = `faststats_window_id_${siteKey}`;
|
|
102
|
-
const existing = getStorageItem("sessionStorage", key);
|
|
103
|
-
if (existing) return existing;
|
|
104
|
-
const id = createId();
|
|
105
|
-
return setStorageItem("sessionStorage", key, id) ? id : sessionId;
|
|
106
|
-
}
|
|
107
|
-
function toContext(siteKey, session, cookieless, windowIdOverride) {
|
|
108
|
-
return {
|
|
109
|
-
sessionId: session.id,
|
|
110
|
-
windowId: windowIdOverride ?? windowId(siteKey, cookieless, session.id),
|
|
111
|
-
sessionStart: session.start
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
function setCookielessMode(mode) {
|
|
115
|
-
cookielessMode = mode;
|
|
116
|
-
memorySession = null;
|
|
117
|
-
}
|
|
118
|
-
function isCookielessMode() {
|
|
119
|
-
return cookielessMode;
|
|
120
|
-
}
|
|
121
|
-
function getSessionContext(siteKey, cookieless) {
|
|
122
|
-
const useCookieless = cookieless ?? cookielessMode;
|
|
123
|
-
const { session, rotatedFrom } = resolveSession(useCookieless, false);
|
|
124
|
-
const next = toContext(siteKey, session, useCookieless);
|
|
125
|
-
if (rotatedFrom) emitRotation(toContext(siteKey, rotatedFrom, useCookieless, next.windowId), next);
|
|
126
|
-
return next;
|
|
127
|
-
}
|
|
128
|
-
function touchActivity(siteKey, cookieless) {
|
|
129
|
-
const useCookieless = cookieless ?? cookielessMode;
|
|
130
|
-
const { session, rotatedFrom } = resolveSession(useCookieless, true);
|
|
131
|
-
if (!rotatedFrom) return null;
|
|
132
|
-
const wid = windowId(siteKey, useCookieless, session.id);
|
|
133
|
-
const rotation = {
|
|
134
|
-
prev: toContext(siteKey, rotatedFrom, useCookieless, wid),
|
|
135
|
-
next: toContext(siteKey, session, useCookieless, wid)
|
|
136
|
-
};
|
|
137
|
-
emitRotation(rotation.prev, rotation.next);
|
|
138
|
-
return rotation;
|
|
139
|
-
}
|
|
140
|
-
function resetSession(siteKey) {
|
|
141
|
-
const useCookieless = cookielessMode;
|
|
142
|
-
const previous = getSessionContext(siteKey, useCookieless);
|
|
143
|
-
clearSession();
|
|
144
|
-
if (useCookieless) {
|
|
145
|
-
const now = Date.now();
|
|
146
|
-
memorySession = {
|
|
147
|
-
id: createId(),
|
|
148
|
-
start: now,
|
|
149
|
-
activity: now
|
|
150
|
-
};
|
|
151
|
-
} else removeStorageItem("sessionStorage", `faststats_window_id_${siteKey}`);
|
|
152
|
-
const next = getSessionContext(siteKey, useCookieless);
|
|
153
|
-
if (previous.sessionId !== next.sessionId) emitRotation(previous, next);
|
|
154
|
-
return next;
|
|
155
|
-
}
|
|
156
|
-
function onSessionRotated(listener) {
|
|
157
|
-
rotationListeners.add(listener);
|
|
158
|
-
return () => rotationListeners.delete(listener);
|
|
159
|
-
}
|
|
160
|
-
//#endregion
|
|
161
|
-
//#region src/utils/send-data.ts
|
|
162
|
-
async function sendData(options) {
|
|
163
|
-
const { url, data, contentType = "application/json", headers = {}, debug = false, debugPrefix = "[Analytics]", useBeacon = true, keepalive = true, signal } = options;
|
|
164
|
-
if (signal?.aborted) return false;
|
|
165
|
-
if (useBeacon && typeof globalThis.navigator?.sendBeacon === "function") try {
|
|
166
|
-
const beaconBody = data instanceof Blob || typeof Blob === "undefined" ? data : new Blob([data], { type: contentType });
|
|
167
|
-
if (globalThis.navigator.sendBeacon(url, beaconBody)) {
|
|
168
|
-
if (debug) console.log(`${debugPrefix} Sent via beacon`);
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
} catch {}
|
|
172
|
-
try {
|
|
173
|
-
const response = await fetch(url, {
|
|
174
|
-
method: "POST",
|
|
175
|
-
body: data,
|
|
176
|
-
headers: {
|
|
177
|
-
"Content-Type": contentType,
|
|
178
|
-
...headers
|
|
179
|
-
},
|
|
180
|
-
keepalive,
|
|
181
|
-
credentials: "omit",
|
|
182
|
-
signal
|
|
183
|
-
});
|
|
184
|
-
const success = response.ok;
|
|
185
|
-
if (debug) {
|
|
186
|
-
const message = success ? `${debugPrefix} Sent via fetch` : `${debugPrefix} Failed: ${response.status}`;
|
|
187
|
-
console[success ? "log" : "warn"](message);
|
|
188
|
-
}
|
|
189
|
-
return success;
|
|
190
|
-
} catch {
|
|
191
|
-
if (debug) console.warn(`${debugPrefix} Failed to send`);
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
//#endregion
|
|
196
|
-
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/feature-flags.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
//#region src/feature-flags.d.ts
|
|
2
|
-
type FeatureFlagEvaluation = {
|
|
3
|
-
value: string;
|
|
4
|
-
variantKey?: string;
|
|
5
|
-
variantValue?: string;
|
|
6
|
-
};
|
|
7
|
-
type FeatureFlagCheckContext = {
|
|
8
|
-
baseUrl?: string;
|
|
9
|
-
projectToken?: string;
|
|
10
|
-
identifier?: string;
|
|
11
|
-
externalId?: string;
|
|
12
|
-
sessionId?: string;
|
|
13
|
-
attributes?: Record<string, unknown>;
|
|
14
|
-
signal?: AbortSignal;
|
|
15
|
-
};
|
|
16
|
-
declare function fetchFeatureFlagEvaluation(flagKey: string, context: FeatureFlagCheckContext): Promise<FeatureFlagEvaluation>;
|
|
17
|
-
//#endregion
|
|
18
|
-
export { FeatureFlagCheckContext, FeatureFlagEvaluation, fetchFeatureFlagEvaluation };
|
package/dist/feature-flags.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { a as resolveBaseUrl, n as FEATURE_FLAGS_BASE, r as URLS } from "./chunks/api-urls-CaNa8upY.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 };
|