@faststats/web 0.2.11 → 0.2.13
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 +12 -0
- package/dist/chunks/api-urls-DaeYkG0_.js +1 -0
- package/dist/chunks/error-CDVOMiI9.js +2 -0
- package/dist/chunks/{feature-flags-BxZz_lNm.d.ts → feature-flags-BClx56v5.d.ts} +1 -0
- package/dist/chunks/feature-flags-DSOCIZHK.js +1 -0
- package/dist/chunks/identifiers-CQeWm7wi.js +1 -0
- package/dist/chunks/replay-CHFW2q4E.js +1 -0
- package/dist/chunks/web-vitals-GwPlL7Zy.js +1 -0
- package/dist/error.d.ts +5 -8
- package/dist/error.js +1 -1
- package/dist/feature-flags.d.ts +1 -1
- package/dist/feature-flags.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/replay.js +1 -1
- package/dist/web-vitals.d.ts +7 -15
- package/dist/web-vitals.js +1 -1
- package/package.json +1 -1
- package/src/analytics.ts +55 -52
- package/src/error.ts +104 -157
- package/src/feature-flags.ts +5 -2
- package/src/replay.ts +2 -2
- package/src/utils/api-urls.ts +7 -20
- package/src/utils/identifiers.ts +38 -26
- package/src/web-vitals.ts +65 -167
- package/tests/analytics.test.ts +36 -0
- package/tests/identifiers.test.ts +18 -0
- package/dist/chunks/api-urls-BrkcoElX.js +0 -1
- package/dist/chunks/error-3yIt0PgC.js +0 -2
- package/dist/chunks/feature-flags-CjnLZGxp.js +0 -1
- package/dist/chunks/identifiers-BN-atT_6.js +0 -1
- package/dist/chunks/replay-u8fo4u_R.js +0 -1
- package/dist/chunks/web-vitals-ClET5qGP.js +0 -1
package/src/error.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SDK_NAME, SDK_VERSION } from "./sdk";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
|
|
3
3
|
import { getAnonymousId, getOrCreateSessionId } from "./utils/identifiers";
|
|
4
4
|
import { sendData } from "./utils/send-data";
|
|
5
5
|
|
|
@@ -16,18 +16,6 @@ export type ErrorTracking = ErrorEntry & {
|
|
|
16
16
|
count: number;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
type ErrorKind = "error" | "unhandledrejection";
|
|
20
|
-
|
|
21
|
-
type NormalizedError = {
|
|
22
|
-
kind: ErrorKind;
|
|
23
|
-
message: string;
|
|
24
|
-
stack?: string;
|
|
25
|
-
filename?: string;
|
|
26
|
-
lineno?: number;
|
|
27
|
-
handled: boolean;
|
|
28
|
-
cause?: ErrorEntry;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
19
|
export interface ErrorTrackingOptions {
|
|
32
20
|
siteKey: string;
|
|
33
21
|
baseUrl?: string;
|
|
@@ -38,214 +26,176 @@ export interface ErrorTrackingOptions {
|
|
|
38
26
|
sdkVersion?: string;
|
|
39
27
|
}
|
|
40
28
|
|
|
41
|
-
|
|
29
|
+
type Kind = "error" | "unhandledrejection";
|
|
30
|
+
|
|
31
|
+
const EXTENSION_RE =
|
|
42
32
|
/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;
|
|
43
33
|
|
|
44
34
|
const parseStack = (stack?: string): string[] | undefined =>
|
|
45
35
|
stack
|
|
46
36
|
?.split("\n")
|
|
47
|
-
.map((
|
|
37
|
+
.map((l) => l.trim())
|
|
48
38
|
.filter(Boolean);
|
|
49
39
|
|
|
50
|
-
function serializeCause(
|
|
51
|
-
if (
|
|
40
|
+
function serializeCause(value: unknown): ErrorEntry | undefined {
|
|
41
|
+
if (value instanceof Error) {
|
|
52
42
|
return {
|
|
53
|
-
error:
|
|
54
|
-
message:
|
|
55
|
-
stack: parseStack(
|
|
56
|
-
cause: serializeCause(
|
|
43
|
+
error: value.name?.trim() || "Error",
|
|
44
|
+
message: value.message,
|
|
45
|
+
stack: parseStack(value.stack),
|
|
46
|
+
cause: serializeCause(value.cause),
|
|
57
47
|
};
|
|
58
48
|
}
|
|
59
|
-
|
|
60
|
-
if (typeof cause === "string") {
|
|
61
|
-
return { error: "Error", message: cause };
|
|
62
|
-
}
|
|
49
|
+
if (typeof value === "string") return { error: "Error", message: value };
|
|
63
50
|
}
|
|
64
51
|
|
|
65
52
|
function fingerprintCause(cause?: ErrorEntry): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
53
|
+
let s = "";
|
|
54
|
+
for (let c = cause; c; c = c.cause) {
|
|
55
|
+
s += `${c.error}\0${c.message ?? ""}\0`;
|
|
56
|
+
}
|
|
57
|
+
return s;
|
|
69
58
|
}
|
|
70
59
|
|
|
71
|
-
function
|
|
72
|
-
const key = [
|
|
73
|
-
data.kind,
|
|
74
|
-
data.handled ? "handled" : "unhandled",
|
|
75
|
-
data.message,
|
|
76
|
-
data.filename ?? "",
|
|
77
|
-
data.lineno ?? "",
|
|
78
|
-
fingerprintCause(data.cause),
|
|
79
|
-
].join("\0");
|
|
80
|
-
|
|
60
|
+
function hashKey(key: string): string {
|
|
81
61
|
let a = 2166136261;
|
|
82
62
|
let b = 3598710387;
|
|
83
|
-
|
|
84
63
|
for (let i = 0; i < key.length; i++) {
|
|
85
64
|
const c = key.charCodeAt(i);
|
|
86
65
|
a = Math.imul(a ^ c, 16777619);
|
|
87
66
|
b = Math.imul(b ^ c, 2246822519);
|
|
88
67
|
}
|
|
68
|
+
return `err_${(a >>> 0).toString(16).padStart(8, "0")}${(b >>> 0).toString(16).padStart(8, "0")}`;
|
|
69
|
+
}
|
|
89
70
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
71
|
+
function readBuildId(): string | undefined {
|
|
72
|
+
const v = (globalThis as { __SOURCEMAPS_BUILD__?: { buildId?: unknown } })
|
|
73
|
+
.__SOURCEMAPS_BUILD__?.buildId;
|
|
74
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
93
75
|
}
|
|
94
76
|
|
|
95
77
|
export default class ErrorTracker {
|
|
96
78
|
private readonly endpoint: string;
|
|
97
|
-
private readonly
|
|
79
|
+
private readonly seen = new WeakSet<object>();
|
|
98
80
|
private readonly queue = new Map<string, ErrorTracking>();
|
|
99
81
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
100
82
|
private started = false;
|
|
101
83
|
private flushing = false;
|
|
102
84
|
|
|
103
85
|
constructor(private readonly options: ErrorTrackingOptions) {
|
|
104
|
-
this.endpoint =
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private get debug(): boolean {
|
|
108
|
-
return this.options.debug ?? false;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private get flushInterval(): number {
|
|
112
|
-
return this.options.flushInterval ?? 5000;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private get maxQueueSize(): number {
|
|
116
|
-
return this.options.maxQueueSize ?? 50;
|
|
86
|
+
this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.events}`;
|
|
117
87
|
}
|
|
118
88
|
|
|
119
89
|
private log(...args: unknown[]): void {
|
|
120
|
-
if (this.debug) console.log("[ErrorTracker]", ...args);
|
|
90
|
+
if (this.options.debug) console.log("[ErrorTracker]", ...args);
|
|
121
91
|
}
|
|
122
92
|
|
|
123
93
|
start(): void {
|
|
124
94
|
if (this.started || typeof window === "undefined") return;
|
|
125
95
|
this.started = true;
|
|
126
|
-
|
|
127
96
|
window.addEventListener("error", this.onError);
|
|
128
97
|
window.addEventListener("unhandledrejection", this.onRejection);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
98
|
+
window.addEventListener("pagehide", this.scheduleFlush);
|
|
99
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
100
|
+
this.timer = setInterval(
|
|
101
|
+
this.scheduleFlush,
|
|
102
|
+
this.options.flushInterval ?? 5000,
|
|
103
|
+
);
|
|
104
|
+
this.log("Started");
|
|
134
105
|
}
|
|
135
106
|
|
|
136
107
|
stop(): void {
|
|
137
108
|
if (!this.started || typeof window === "undefined") return;
|
|
138
109
|
this.started = false;
|
|
139
|
-
|
|
140
110
|
window.removeEventListener("error", this.onError);
|
|
141
111
|
window.removeEventListener("unhandledrejection", this.onRejection);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
112
|
+
window.removeEventListener("pagehide", this.scheduleFlush);
|
|
113
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
145
114
|
if (this.timer) clearInterval(this.timer);
|
|
146
115
|
this.timer = null;
|
|
147
|
-
|
|
148
|
-
this.
|
|
149
|
-
this.log("Stopped listening for errors");
|
|
116
|
+
this.scheduleFlush();
|
|
117
|
+
this.log("Stopped");
|
|
150
118
|
}
|
|
151
119
|
|
|
152
120
|
captureError(error: Error): void {
|
|
153
|
-
this.
|
|
154
|
-
{
|
|
155
|
-
kind: "error",
|
|
156
|
-
message: error.message,
|
|
157
|
-
stack: error.stack,
|
|
158
|
-
handled: true,
|
|
159
|
-
cause: serializeCause(error.cause),
|
|
160
|
-
},
|
|
161
|
-
error,
|
|
162
|
-
);
|
|
121
|
+
this.capture("error", error.message, true, error);
|
|
163
122
|
}
|
|
164
123
|
|
|
165
124
|
private onError = (event: ErrorEvent): void => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
lineno: event.lineno || undefined,
|
|
174
|
-
stack: err instanceof Error ? err.stack : undefined,
|
|
175
|
-
handled: false,
|
|
176
|
-
cause: err instanceof Error ? serializeCause(err.cause) : undefined,
|
|
177
|
-
},
|
|
178
|
-
err,
|
|
125
|
+
this.capture(
|
|
126
|
+
"error",
|
|
127
|
+
event.message || "Unknown error",
|
|
128
|
+
false,
|
|
129
|
+
event.error,
|
|
130
|
+
event.filename || "",
|
|
131
|
+
event.lineno,
|
|
179
132
|
);
|
|
180
133
|
};
|
|
181
134
|
|
|
182
135
|
private onRejection = (event: PromiseRejectionEvent): void => {
|
|
183
136
|
const reason = event.reason;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
cause: serializeCause(reason.cause),
|
|
192
|
-
},
|
|
193
|
-
reason,
|
|
194
|
-
);
|
|
137
|
+
const message =
|
|
138
|
+
reason instanceof Error
|
|
139
|
+
? reason.message
|
|
140
|
+
: typeof reason === "string"
|
|
141
|
+
? reason
|
|
142
|
+
: "Unhandled promise rejection";
|
|
143
|
+
this.capture("unhandledrejection", message, false, reason);
|
|
195
144
|
};
|
|
196
145
|
|
|
197
|
-
private
|
|
198
|
-
if (document.visibilityState === "hidden") this.
|
|
146
|
+
private onVisibility = (): void => {
|
|
147
|
+
if (document.visibilityState === "hidden") this.scheduleFlush();
|
|
199
148
|
};
|
|
200
149
|
|
|
201
|
-
private
|
|
150
|
+
private scheduleFlush = (): void => {
|
|
202
151
|
void this.flush();
|
|
203
152
|
};
|
|
204
153
|
|
|
205
|
-
private
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
154
|
+
private capture(
|
|
155
|
+
kind: Kind,
|
|
156
|
+
message: string,
|
|
157
|
+
handled: boolean,
|
|
158
|
+
source: unknown,
|
|
159
|
+
filename = "",
|
|
160
|
+
lineno: number | "" = "",
|
|
161
|
+
): void {
|
|
162
|
+
const stackText = source instanceof Error ? (source.stack ?? "") : "";
|
|
163
|
+
if (EXTENSION_RE.test(`${filename}\n${stackText}`)) return;
|
|
164
|
+
|
|
165
|
+
if (typeof source === "object" && source !== null) {
|
|
166
|
+
if (this.seen.has(source)) {
|
|
167
|
+
this.log("Skipping duplicate:", message);
|
|
218
168
|
return;
|
|
219
169
|
}
|
|
220
|
-
this.
|
|
170
|
+
this.seen.add(source);
|
|
221
171
|
}
|
|
222
172
|
|
|
223
|
-
|
|
224
|
-
|
|
173
|
+
const entry: ErrorEntry = {
|
|
174
|
+
error: kind === "unhandledrejection" ? "UnhandledRejection" : "Error",
|
|
175
|
+
message,
|
|
176
|
+
handled,
|
|
177
|
+
};
|
|
178
|
+
if (source instanceof Error) {
|
|
179
|
+
entry.stack = parseStack(source.stack);
|
|
180
|
+
const cause = serializeCause(source.cause);
|
|
181
|
+
if (cause) entry.cause = cause;
|
|
225
182
|
}
|
|
226
183
|
|
|
227
|
-
const
|
|
228
|
-
|
|
184
|
+
const key = hashKey(
|
|
185
|
+
`${kind}\0${handled ? "handled" : "unhandled"}\0${message}\0${filename}\0${lineno}\0${fingerprintCause(entry.cause)}`,
|
|
186
|
+
);
|
|
229
187
|
|
|
188
|
+
const existing = this.queue.get(key);
|
|
230
189
|
if (existing) {
|
|
231
|
-
existing.count
|
|
190
|
+
existing.count++;
|
|
232
191
|
} else {
|
|
233
|
-
this.queue.set(hash,
|
|
234
|
-
hash,
|
|
235
|
-
count: 1,
|
|
236
|
-
error:
|
|
237
|
-
data.kind === "unhandledrejection" ? "UnhandledRejection" : "Error",
|
|
238
|
-
message: data.message,
|
|
239
|
-
stack: parseStack(data.stack),
|
|
240
|
-
handled: data.handled,
|
|
241
|
-
...(data.cause ? { cause: data.cause } : {}),
|
|
242
|
-
});
|
|
192
|
+
this.queue.set(key, { hash: key, count: 1, ...entry });
|
|
243
193
|
}
|
|
244
194
|
|
|
245
|
-
this.log("Captured
|
|
195
|
+
this.log("Captured:", entry);
|
|
246
196
|
|
|
247
|
-
if (this.queue.size >= this.maxQueueSize) {
|
|
248
|
-
this.
|
|
197
|
+
if (this.queue.size >= (this.options.maxQueueSize ?? 50)) {
|
|
198
|
+
this.scheduleFlush();
|
|
249
199
|
}
|
|
250
200
|
}
|
|
251
201
|
|
|
@@ -256,22 +206,12 @@ export default class ErrorTracker {
|
|
|
256
206
|
this.queue.clear();
|
|
257
207
|
this.flushing = true;
|
|
258
208
|
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
globalThis as {
|
|
262
|
-
__SOURCEMAPS_BUILD__?: { buildId?: unknown };
|
|
263
|
-
}
|
|
264
|
-
).__SOURCEMAPS_BUILD__;
|
|
265
|
-
|
|
266
|
-
const buildId =
|
|
267
|
-
typeof sourcemapsBuild?.buildId === "string" &&
|
|
268
|
-
sourcemapsBuild.buildId.trim()
|
|
269
|
-
? sourcemapsBuild.buildId
|
|
270
|
-
: undefined;
|
|
209
|
+
const userId = getAnonymousId();
|
|
210
|
+
const buildId = readBuildId();
|
|
271
211
|
|
|
272
212
|
const payload = JSON.stringify({
|
|
273
213
|
token: this.options.siteKey,
|
|
274
|
-
...(
|
|
214
|
+
...(userId ? { userId } : {}),
|
|
275
215
|
sessionId: getOrCreateSessionId(),
|
|
276
216
|
...(buildId ? { buildId } : {}),
|
|
277
217
|
sdkName: this.options.sdkName ?? SDK_NAME,
|
|
@@ -285,21 +225,28 @@ export default class ErrorTracker {
|
|
|
285
225
|
errors,
|
|
286
226
|
});
|
|
287
227
|
|
|
288
|
-
this.log("Flushing
|
|
289
|
-
this.log("Payload:", payload);
|
|
228
|
+
this.log("Flushing:", errors);
|
|
290
229
|
|
|
291
230
|
try {
|
|
292
231
|
const sent = await sendData({
|
|
293
232
|
url: this.endpoint,
|
|
294
233
|
data: payload,
|
|
295
|
-
debug: this.debug,
|
|
234
|
+
debug: this.options.debug,
|
|
296
235
|
debugPrefix: "[ErrorTracker]",
|
|
297
236
|
});
|
|
298
|
-
if (!sent) this.
|
|
237
|
+
if (!sent) this.requeue(errors);
|
|
299
238
|
} catch {
|
|
300
|
-
this.
|
|
239
|
+
this.requeue(errors);
|
|
301
240
|
} finally {
|
|
302
241
|
this.flushing = false;
|
|
303
242
|
}
|
|
304
243
|
}
|
|
244
|
+
|
|
245
|
+
private requeue(errors: ErrorTracking[]): void {
|
|
246
|
+
for (const e of errors) {
|
|
247
|
+
const existing = this.queue.get(e.hash);
|
|
248
|
+
if (existing) existing.count += e.count;
|
|
249
|
+
else this.queue.set(e.hash, e);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
305
252
|
}
|
package/src/feature-flags.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeFeatureFlagsBaseUrl, URLS } from "./utils/api-urls";
|
|
2
2
|
|
|
3
3
|
export type FeatureFlagEvaluation = {
|
|
4
4
|
value: string;
|
|
@@ -12,6 +12,7 @@ export type FeatureFlagCheckContext = {
|
|
|
12
12
|
projectId?: string;
|
|
13
13
|
identifier?: string;
|
|
14
14
|
externalId?: string;
|
|
15
|
+
sessionId?: string;
|
|
15
16
|
attributes?: Record<string, unknown>;
|
|
16
17
|
signal?: AbortSignal;
|
|
17
18
|
};
|
|
@@ -28,6 +29,7 @@ export async function fetchFeatureFlagEvaluation(
|
|
|
28
29
|
const projectId = context.projectId;
|
|
29
30
|
const identifier = context.identifier;
|
|
30
31
|
const externalId = context.externalId;
|
|
32
|
+
const sessionId = context.sessionId;
|
|
31
33
|
|
|
32
34
|
if (!projectToken && !projectId) {
|
|
33
35
|
throw new Error("feature flag check requires projectToken or projectId");
|
|
@@ -41,6 +43,7 @@ export async function fetchFeatureFlagEvaluation(
|
|
|
41
43
|
...(projectId ? { projectId } : {}),
|
|
42
44
|
...(identifier ? { identifier } : {}),
|
|
43
45
|
...(externalId ? { externalId } : {}),
|
|
46
|
+
...(sessionId ? { sessionId } : {}),
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
if (context.attributes && Object.keys(context.attributes).length > 0) {
|
|
@@ -54,7 +57,7 @@ export async function fetchFeatureFlagEvaluation(
|
|
|
54
57
|
headers.Authorization = `Bearer ${projectToken}`;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
const url =
|
|
60
|
+
const url = `${normalizeFeatureFlagsBaseUrl(context.baseUrl)}${URLS.flags}`;
|
|
58
61
|
const res = await fetch(url, {
|
|
59
62
|
method: "POST",
|
|
60
63
|
headers,
|
package/src/replay.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { record } from "rrweb";
|
|
7
7
|
import type { recordOptions } from "rrweb/typings/types";
|
|
8
8
|
import type { SlimDOMOptions } from "rrweb-snapshot";
|
|
9
|
-
import {
|
|
9
|
+
import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
|
|
10
10
|
import {
|
|
11
11
|
getAnonymousId,
|
|
12
12
|
getOrCreateSessionId,
|
|
@@ -96,7 +96,7 @@ export default class ReplayTracker {
|
|
|
96
96
|
private sending = false;
|
|
97
97
|
|
|
98
98
|
constructor(private readonly options: ReplayTrackerOptions) {
|
|
99
|
-
this.endpoint =
|
|
99
|
+
this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.replay}`;
|
|
100
100
|
this.sampled =
|
|
101
101
|
Math.random() * 100 <
|
|
102
102
|
normalizeSamplingPercentage(options.samplingPercentage);
|
package/src/utils/api-urls.ts
CHANGED
|
@@ -10,28 +10,15 @@ export function normalizeAnalyticsBaseUrl(input?: string): string {
|
|
|
10
10
|
return stripTrailingSlashes(input) || DEFAULT_BASE;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function replayEventsUrl(baseUrl?: string): string {
|
|
22
|
-
return `${normalizeAnalyticsBaseUrl(baseUrl)}/v1/replay`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function webVitalsEventsUrl(baseUrl?: string): string {
|
|
26
|
-
return `${normalizeAnalyticsBaseUrl(baseUrl)}/v1/vitals`;
|
|
27
|
-
}
|
|
13
|
+
export const URLS = {
|
|
14
|
+
events: "/v1/web",
|
|
15
|
+
identify: "/v1/identify",
|
|
16
|
+
replay: "/v1/replay",
|
|
17
|
+
vitals: "/v1/vitals",
|
|
18
|
+
flags: "/v1/check",
|
|
19
|
+
} as const;
|
|
28
20
|
|
|
29
21
|
export function normalizeFeatureFlagsBaseUrl(input?: string): string {
|
|
30
22
|
if (input === undefined || input === "") return DEFAULT_FEATURE_FLAGS_BASE;
|
|
31
23
|
return stripTrailingSlashes(input) || DEFAULT_FEATURE_FLAGS_BASE;
|
|
32
24
|
}
|
|
33
|
-
|
|
34
|
-
export function featureFlagsCheckUrl(baseUrl?: string): string {
|
|
35
|
-
const base = normalizeFeatureFlagsBaseUrl(baseUrl);
|
|
36
|
-
return `${base}/v1/check`;
|
|
37
|
-
}
|
package/src/utils/identifiers.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
2
|
+
const ANONYMOUS_ID_KEY = "faststats_anon_id";
|
|
3
|
+
const SESSION_ID_KEY = "session_id";
|
|
4
|
+
const SESSION_TIMESTAMP_KEY = "session_timestamp";
|
|
5
|
+
const SESSION_START_KEY = "session_start";
|
|
2
6
|
|
|
3
7
|
let cookielessMode = false;
|
|
4
8
|
|
|
@@ -13,65 +17,73 @@ export function getAnonymousId(cookieless?: boolean): string {
|
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export function getOrCreateAnonymousId(): string {
|
|
16
|
-
if (
|
|
17
|
-
const existingId = localStorage.getItem(
|
|
20
|
+
if (!localStorage) return "";
|
|
21
|
+
const existingId = localStorage.getItem(ANONYMOUS_ID_KEY);
|
|
18
22
|
if (existingId) return existingId;
|
|
19
23
|
|
|
20
24
|
const newId = crypto.randomUUID();
|
|
21
|
-
localStorage.setItem(
|
|
25
|
+
localStorage.setItem(ANONYMOUS_ID_KEY, newId);
|
|
22
26
|
return newId;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export function resetAnonymousId(cookieless?: boolean): string {
|
|
26
|
-
if (cookieless
|
|
27
|
-
|
|
30
|
+
if (cookieless) return "";
|
|
31
|
+
|
|
32
|
+
if (!localStorage) return "";
|
|
33
|
+
|
|
34
|
+
localStorage.removeItem(ANONYMOUS_ID_KEY);
|
|
28
35
|
return getOrCreateAnonymousId();
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export function getOrCreateSessionId(): string {
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
const
|
|
39
|
+
if (!sessionStorage) return "";
|
|
40
|
+
|
|
41
|
+
const existingId = sessionStorage.getItem(SESSION_ID_KEY);
|
|
42
|
+
const sessionTimestamp = sessionStorage.getItem(SESSION_TIMESTAMP_KEY);
|
|
35
43
|
|
|
36
44
|
if (existingId && sessionTimestamp) {
|
|
37
45
|
const sessionAge = Date.now() - Number.parseInt(sessionTimestamp, 10);
|
|
38
46
|
if (sessionAge < SESSION_TIMEOUT) {
|
|
39
|
-
sessionStorage.setItem(
|
|
47
|
+
sessionStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
|
|
40
48
|
return existingId;
|
|
41
49
|
}
|
|
42
|
-
sessionStorage
|
|
43
|
-
sessionStorage.removeItem("session_timestamp");
|
|
44
|
-
sessionStorage.removeItem("session_start");
|
|
50
|
+
clearSession(sessionStorage);
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
const now = Date.now().toString();
|
|
48
54
|
const newId = crypto.randomUUID();
|
|
49
|
-
sessionStorage.setItem(
|
|
50
|
-
sessionStorage.setItem(
|
|
51
|
-
sessionStorage.setItem(
|
|
55
|
+
sessionStorage.setItem(SESSION_ID_KEY, newId);
|
|
56
|
+
sessionStorage.setItem(SESSION_TIMESTAMP_KEY, now);
|
|
57
|
+
sessionStorage.setItem(SESSION_START_KEY, now);
|
|
52
58
|
return newId;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
export function resetSessionId(): string {
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
sessionStorage
|
|
59
|
-
sessionStorage.removeItem("session_start");
|
|
62
|
+
if (!sessionStorage) return "";
|
|
63
|
+
|
|
64
|
+
clearSession(sessionStorage);
|
|
60
65
|
return getOrCreateSessionId();
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
export function refreshSessionTimestamp(): void {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
sessionStorage.setItem(
|
|
69
|
+
if (!sessionStorage) return;
|
|
70
|
+
|
|
71
|
+
if (sessionStorage.getItem(SESSION_ID_KEY)) {
|
|
72
|
+
sessionStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
export function getSessionStart(): number {
|
|
72
|
-
if (
|
|
73
|
-
|
|
77
|
+
if (!sessionStorage) return Date.now();
|
|
78
|
+
|
|
79
|
+
const start = sessionStorage.getItem(SESSION_START_KEY);
|
|
74
80
|
if (start) return Number.parseInt(start, 10);
|
|
75
|
-
const ts = sessionStorage.getItem(
|
|
81
|
+
const ts = sessionStorage.getItem(SESSION_TIMESTAMP_KEY);
|
|
76
82
|
return ts ? Number.parseInt(ts, 10) : Date.now();
|
|
77
83
|
}
|
|
84
|
+
|
|
85
|
+
function clearSession(storage: Storage): void {
|
|
86
|
+
storage.removeItem(SESSION_ID_KEY);
|
|
87
|
+
storage.removeItem(SESSION_TIMESTAMP_KEY);
|
|
88
|
+
storage.removeItem(SESSION_START_KEY);
|
|
89
|
+
}
|