@faststats/web 0.1.9 → 0.2.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/CHANGELOG.md +10 -0
- package/dist/analytics-6gDXSBjL.js +1 -0
- package/dist/error-BWTalC2O.js +2 -0
- package/dist/module.d.ts +29 -2
- package/dist/module.js +1 -1
- package/dist/replay-4Jtqp78w.js +1 -0
- package/dist/types-PBKNFqxj.js +1 -0
- package/dist/web-vitals-BAkrZEHv.js +1 -0
- package/package.json +1 -3
- package/src/analytics.ts +42 -5
- package/src/error.ts +131 -175
- package/src/feature-flags.ts +88 -0
- package/src/module.ts +9 -0
- package/src/replay.ts +242 -467
- package/src/utils/api-urls.ts +15 -0
- package/src/web-vitals.ts +58 -120
- package/dist/analytics-B1kgY_Yf.js +0 -1
- package/dist/error-Cs5wnTbZ.js +0 -2
- package/dist/replay-DWCkIH4r.js +0 -1
- package/dist/types-8FXsUqbi.js +0 -1
- package/dist/web-vitals-C7H69Xv3.js +0 -1
- package/tests/replay-stress.test.ts +0 -24
package/src/error.ts
CHANGED
|
@@ -10,18 +10,19 @@ export type ErrorEntry = {
|
|
|
10
10
|
cause?: ErrorEntry;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export type ErrorTracking = {
|
|
13
|
+
export type ErrorTracking = ErrorEntry & {
|
|
14
14
|
hash: string;
|
|
15
15
|
count: number;
|
|
16
|
-
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ErrorKind = "error" | "unhandledrejection";
|
|
17
19
|
|
|
18
|
-
type
|
|
20
|
+
type NormalizedError = {
|
|
21
|
+
kind: ErrorKind;
|
|
19
22
|
message: string;
|
|
23
|
+
stack?: string;
|
|
20
24
|
filename?: string;
|
|
21
25
|
lineno?: number;
|
|
22
|
-
colno?: number;
|
|
23
|
-
stack?: string;
|
|
24
|
-
type: "error" | "unhandledrejection";
|
|
25
26
|
handled: boolean;
|
|
26
27
|
cause?: ErrorEntry;
|
|
27
28
|
};
|
|
@@ -37,248 +38,205 @@ export interface ErrorTrackingOptions {
|
|
|
37
38
|
const EXTENSION_URL =
|
|
38
39
|
/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.split("\n")
|
|
41
|
+
const parseStack = (stack?: string): string[] | undefined =>
|
|
42
|
+
stack
|
|
43
|
+
?.split("\n")
|
|
44
44
|
.map((line) => line.trim())
|
|
45
|
-
.filter(
|
|
46
|
-
}
|
|
45
|
+
.filter(Boolean);
|
|
47
46
|
|
|
48
47
|
function serializeCause(cause: unknown): ErrorEntry | undefined {
|
|
49
48
|
if (cause instanceof Error) {
|
|
50
|
-
const name = cause.name?.trim() || "Error";
|
|
51
49
|
return {
|
|
52
|
-
error: name,
|
|
50
|
+
error: cause.name?.trim() || "Error",
|
|
53
51
|
message: cause.message,
|
|
54
52
|
stack: parseStack(cause.stack),
|
|
55
53
|
cause: serializeCause(cause.cause),
|
|
56
54
|
};
|
|
57
55
|
}
|
|
56
|
+
|
|
58
57
|
if (typeof cause === "string") {
|
|
59
58
|
return { error: "Error", message: cause };
|
|
60
59
|
}
|
|
61
|
-
return undefined;
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
function fingerprintCause(cause?: ErrorEntry): string {
|
|
63
|
+
return cause
|
|
64
|
+
? `${cause.error}\0${cause.message ?? ""}\0${fingerprintCause(cause.cause)}`
|
|
65
|
+
: "";
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
function
|
|
68
|
+
function hashError(data: NormalizedError): string {
|
|
70
69
|
const key = [
|
|
71
|
-
data.
|
|
70
|
+
data.kind,
|
|
72
71
|
data.handled ? "handled" : "unhandled",
|
|
73
72
|
data.message,
|
|
74
73
|
data.filename ?? "",
|
|
75
74
|
data.lineno ?? "",
|
|
76
|
-
|
|
75
|
+
fingerprintCause(data.cause),
|
|
77
76
|
].join("\0");
|
|
78
77
|
|
|
79
78
|
let a = 2166136261;
|
|
80
79
|
let b = 3598710387;
|
|
80
|
+
|
|
81
81
|
for (let i = 0; i < key.length; i++) {
|
|
82
82
|
const c = key.charCodeAt(i);
|
|
83
83
|
a = Math.imul(a ^ c, 16777619);
|
|
84
84
|
b = Math.imul(b ^ c, 2246822519);
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
|
|
87
|
+
return `err_${(a >>> 0).toString(16).padStart(8, "0")}${(b >>> 0)
|
|
88
|
+
.toString(16)
|
|
89
|
+
.padStart(8, "0")}`;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
class ErrorTracker {
|
|
92
|
+
export default class ErrorTracker {
|
|
90
93
|
private readonly endpoint: string;
|
|
91
|
-
private readonly
|
|
92
|
-
private readonly
|
|
93
|
-
private
|
|
94
|
-
private readonly maxQueueSize: number;
|
|
95
|
-
|
|
96
|
-
private readonly handledErrors = new WeakSet<Error>();
|
|
97
|
-
private readonly errorCounts = new Map<
|
|
98
|
-
string,
|
|
99
|
-
{ entry: ErrorEntry; count: number }
|
|
100
|
-
>();
|
|
101
|
-
private readonly handleVisibilityChange = (): void => {
|
|
102
|
-
if (document.visibilityState === "hidden") this.flush();
|
|
103
|
-
};
|
|
104
|
-
private readonly handlePageHide = (): void => {
|
|
105
|
-
this.flush();
|
|
106
|
-
};
|
|
107
|
-
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
94
|
+
private readonly handled = new WeakSet<Error>();
|
|
95
|
+
private readonly queue = new Map<string, ErrorTracking>();
|
|
96
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
108
97
|
private started = false;
|
|
109
98
|
|
|
110
|
-
constructor(options: ErrorTrackingOptions) {
|
|
111
|
-
this.siteKey = options.siteKey;
|
|
99
|
+
constructor(private readonly options: ErrorTrackingOptions) {
|
|
112
100
|
this.endpoint = webEventsUrl(options.baseUrl);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private get debug(): boolean {
|
|
104
|
+
return this.options.debug ?? false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private get flushInterval(): number {
|
|
108
|
+
return this.options.flushInterval ?? 5000;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private get maxQueueSize(): number {
|
|
112
|
+
return this.options.maxQueueSize ?? 50;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private log(...args: unknown[]): void {
|
|
116
|
+
if (this.debug) console.log("[ErrorTracker]", ...args);
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
start(): void {
|
|
119
120
|
if (this.started || typeof window === "undefined") return;
|
|
120
121
|
this.started = true;
|
|
121
122
|
|
|
122
|
-
window.addEventListener("error", this.
|
|
123
|
-
window.addEventListener("unhandledrejection", this.
|
|
123
|
+
window.addEventListener("error", this.onError);
|
|
124
|
+
window.addEventListener("unhandledrejection", this.onRejection);
|
|
125
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
126
|
+
window.addEventListener("pagehide", this.flush);
|
|
124
127
|
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
window.addEventListener("pagehide", this.handlePageHide);
|
|
128
|
-
|
|
129
|
-
if (this.debug) {
|
|
130
|
-
console.log("[ErrorTracker] Started listening for errors");
|
|
131
|
-
}
|
|
128
|
+
this.timer = setInterval(this.flush, this.flushInterval);
|
|
129
|
+
this.log("Started listening for errors");
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
stop(): void {
|
|
135
133
|
if (!this.started || typeof window === "undefined") return;
|
|
136
134
|
this.started = false;
|
|
137
135
|
|
|
138
|
-
window.removeEventListener("error", this.
|
|
139
|
-
window.removeEventListener("unhandledrejection", this.
|
|
140
|
-
document.removeEventListener(
|
|
141
|
-
|
|
142
|
-
this.handleVisibilityChange,
|
|
143
|
-
);
|
|
144
|
-
window.removeEventListener("pagehide", this.handlePageHide);
|
|
136
|
+
window.removeEventListener("error", this.onError);
|
|
137
|
+
window.removeEventListener("unhandledrejection", this.onRejection);
|
|
138
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
139
|
+
window.removeEventListener("pagehide", this.flush);
|
|
145
140
|
|
|
146
|
-
if (this.
|
|
147
|
-
|
|
148
|
-
this.flushTimer = null;
|
|
149
|
-
}
|
|
141
|
+
if (this.timer) clearInterval(this.timer);
|
|
142
|
+
this.timer = null;
|
|
150
143
|
|
|
151
144
|
this.flush();
|
|
152
|
-
|
|
153
|
-
if (this.debug) {
|
|
154
|
-
console.log("[ErrorTracker] Stopped listening for errors");
|
|
155
|
-
}
|
|
145
|
+
this.log("Stopped listening for errors");
|
|
156
146
|
}
|
|
157
147
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
148
|
+
captureError(error: Error): void {
|
|
149
|
+
this.record(
|
|
150
|
+
{
|
|
151
|
+
kind: "error",
|
|
152
|
+
message: error.message,
|
|
153
|
+
stack: error.stack,
|
|
154
|
+
handled: true,
|
|
155
|
+
cause: serializeCause(error.cause),
|
|
156
|
+
},
|
|
157
|
+
error,
|
|
158
|
+
);
|
|
163
159
|
}
|
|
164
160
|
|
|
165
|
-
private
|
|
161
|
+
private onError = (event: ErrorEvent): void => {
|
|
166
162
|
const err = event.error;
|
|
167
|
-
if (this.skipDuplicate(err)) {
|
|
168
|
-
if (this.debug) {
|
|
169
|
-
console.log("[ErrorTracker] Skipping duplicate error:", err.message);
|
|
170
|
-
}
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
163
|
|
|
174
|
-
this.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
164
|
+
this.record(
|
|
165
|
+
{
|
|
166
|
+
kind: "error",
|
|
167
|
+
message: event.message || "Unknown error",
|
|
168
|
+
filename: event.filename || undefined,
|
|
169
|
+
lineno: event.lineno || undefined,
|
|
170
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
171
|
+
handled: false,
|
|
172
|
+
cause: err instanceof Error ? serializeCause(err.cause) : undefined,
|
|
173
|
+
},
|
|
174
|
+
err,
|
|
175
|
+
);
|
|
185
176
|
};
|
|
186
177
|
|
|
187
|
-
private
|
|
178
|
+
private onRejection = (event: PromiseRejectionEvent): void => {
|
|
188
179
|
const reason = event.reason;
|
|
189
|
-
if (this.skipDuplicate(reason)) {
|
|
190
|
-
if (this.debug) {
|
|
191
|
-
console.log(
|
|
192
|
-
"[ErrorTracker] Skipping duplicate rejection:",
|
|
193
|
-
reason instanceof Error ? reason.message : reason,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
180
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
type: "unhandledrejection",
|
|
210
|
-
handled: false,
|
|
211
|
-
cause: reason instanceof Error ? serializeCause(reason.cause) : undefined,
|
|
212
|
-
});
|
|
181
|
+
this.record(
|
|
182
|
+
{
|
|
183
|
+
kind: "unhandledrejection",
|
|
184
|
+
message: reason.message || "Unhandled promise rejection",
|
|
185
|
+
stack: reason.stack || undefined,
|
|
186
|
+
handled: false,
|
|
187
|
+
cause: serializeCause(reason.cause),
|
|
188
|
+
},
|
|
189
|
+
reason,
|
|
190
|
+
);
|
|
213
191
|
};
|
|
214
192
|
|
|
215
|
-
private
|
|
193
|
+
private onVisibilityChange = (): void => {
|
|
194
|
+
if (document.visibilityState === "hidden") this.flush();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
private record(data: NormalizedError, original?: unknown): void {
|
|
198
|
+
if (original instanceof Error) {
|
|
199
|
+
if (this.handled.has(original)) {
|
|
200
|
+
this.log("Skipping duplicate:", original.message);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.handled.add(original);
|
|
204
|
+
}
|
|
205
|
+
|
|
216
206
|
if (EXTENSION_URL.test(`${data.filename ?? ""}\n${data.stack ?? ""}`)) {
|
|
217
207
|
return;
|
|
218
208
|
}
|
|
219
209
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
210
|
+
const hash = hashError(data);
|
|
211
|
+
const existing = this.queue.get(hash);
|
|
223
212
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (bucket) {
|
|
227
|
-
bucket.count++;
|
|
228
|
-
if (this.debug) {
|
|
229
|
-
console.log(
|
|
230
|
-
`[ErrorTracker] Incremented count for ${hash} to ${bucket.count}`,
|
|
231
|
-
);
|
|
232
|
-
}
|
|
213
|
+
if (existing) {
|
|
214
|
+
existing.count += 1;
|
|
233
215
|
} else {
|
|
234
|
-
|
|
216
|
+
this.queue.set(hash, {
|
|
217
|
+
hash,
|
|
218
|
+
count: 1,
|
|
235
219
|
error:
|
|
236
|
-
data.
|
|
220
|
+
data.kind === "unhandledrejection" ? "UnhandledRejection" : "Error",
|
|
237
221
|
message: data.message,
|
|
238
222
|
stack: parseStack(data.stack),
|
|
239
223
|
handled: data.handled,
|
|
240
224
|
...(data.cause ? { cause: data.cause } : {}),
|
|
241
|
-
};
|
|
242
|
-
this.errorCounts.set(hash, { entry, count: 1 });
|
|
243
|
-
if (this.debug) {
|
|
244
|
-
console.log(`[ErrorTracker] Queued new error: ${hash}`);
|
|
245
|
-
}
|
|
225
|
+
});
|
|
246
226
|
}
|
|
247
227
|
|
|
248
|
-
|
|
249
|
-
this.flush();
|
|
250
|
-
}
|
|
251
|
-
}
|
|
228
|
+
this.log("Captured error:", data);
|
|
252
229
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (this.debug) {
|
|
256
|
-
console.log("[ErrorTracker] Skipping duplicate:", error.message);
|
|
257
|
-
}
|
|
258
|
-
return;
|
|
230
|
+
if (this.queue.size >= this.maxQueueSize) {
|
|
231
|
+
this.flush();
|
|
259
232
|
}
|
|
260
|
-
|
|
261
|
-
this.queueError({
|
|
262
|
-
message: error.message,
|
|
263
|
-
stack: error.stack,
|
|
264
|
-
type: "error",
|
|
265
|
-
handled: true,
|
|
266
|
-
cause: serializeCause(error.cause),
|
|
267
|
-
});
|
|
268
233
|
}
|
|
269
234
|
|
|
270
|
-
private flush(): void {
|
|
271
|
-
if (this.
|
|
235
|
+
private flush = (): void => {
|
|
236
|
+
if (this.queue.size === 0) return;
|
|
272
237
|
|
|
273
|
-
const errors
|
|
274
|
-
|
|
275
|
-
errors.push({ ...entry, hash, count });
|
|
276
|
-
}
|
|
277
|
-
this.errorCounts.clear();
|
|
278
|
-
|
|
279
|
-
if (this.debug) {
|
|
280
|
-
console.log("[ErrorTracker] Flushing errors:", errors);
|
|
281
|
-
}
|
|
238
|
+
const errors = [...this.queue.values()];
|
|
239
|
+
this.queue.clear();
|
|
282
240
|
|
|
283
241
|
const identifier = getAnonymousId();
|
|
284
242
|
const sourcemapsBuild = (
|
|
@@ -286,14 +244,15 @@ class ErrorTracker {
|
|
|
286
244
|
__SOURCEMAPS_BUILD__?: { buildId?: unknown };
|
|
287
245
|
}
|
|
288
246
|
).__SOURCEMAPS_BUILD__;
|
|
247
|
+
|
|
289
248
|
const buildId =
|
|
290
249
|
typeof sourcemapsBuild?.buildId === "string" &&
|
|
291
|
-
sourcemapsBuild.buildId.trim()
|
|
250
|
+
sourcemapsBuild.buildId.trim()
|
|
292
251
|
? sourcemapsBuild.buildId
|
|
293
252
|
: undefined;
|
|
294
253
|
|
|
295
|
-
const
|
|
296
|
-
token: this.siteKey,
|
|
254
|
+
const payload = JSON.stringify({
|
|
255
|
+
token: this.options.siteKey,
|
|
297
256
|
...(identifier ? { userId: identifier } : {}),
|
|
298
257
|
sessionId: getOrCreateSessionId(),
|
|
299
258
|
...(buildId ? { buildId } : {}),
|
|
@@ -306,17 +265,14 @@ class ErrorTracker {
|
|
|
306
265
|
errors,
|
|
307
266
|
});
|
|
308
267
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
268
|
+
this.log("Flushing errors:", errors);
|
|
269
|
+
this.log("Payload:", payload);
|
|
312
270
|
|
|
313
271
|
sendData({
|
|
314
272
|
url: this.endpoint,
|
|
315
|
-
data:
|
|
273
|
+
data: payload,
|
|
316
274
|
debug: this.debug,
|
|
317
275
|
debugPrefix: "[ErrorTracker]",
|
|
318
276
|
});
|
|
319
|
-
}
|
|
277
|
+
};
|
|
320
278
|
}
|
|
321
|
-
|
|
322
|
-
export default ErrorTracker;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { featureFlagsCheckUrl } from "./utils/api-urls";
|
|
2
|
+
|
|
3
|
+
export type FeatureFlagEvaluation = {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
value?: string;
|
|
6
|
+
variantKey?: string;
|
|
7
|
+
variantValue?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type FeatureFlagCheckContext = {
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
projectToken?: string;
|
|
13
|
+
projectId?: string;
|
|
14
|
+
serverId?: string;
|
|
15
|
+
externalId?: string;
|
|
16
|
+
attributes?: Record<string, unknown>;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function encodeAttributes(attributes: Record<string, unknown>): string {
|
|
21
|
+
const json = JSON.stringify(attributes);
|
|
22
|
+
if (typeof btoa !== "function") {
|
|
23
|
+
return json;
|
|
24
|
+
}
|
|
25
|
+
const bytes = new TextEncoder().encode(json);
|
|
26
|
+
let binary = "";
|
|
27
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
28
|
+
const b = bytes[i];
|
|
29
|
+
if (b !== undefined) binary += String.fromCharCode(b);
|
|
30
|
+
}
|
|
31
|
+
return btoa(binary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function fetchFeatureFlagEvaluation(
|
|
35
|
+
flagKey: string,
|
|
36
|
+
context: FeatureFlagCheckContext,
|
|
37
|
+
): Promise<FeatureFlagEvaluation> {
|
|
38
|
+
const url = featureFlagsCheckUrl(context.baseUrl, flagKey);
|
|
39
|
+
const headers = new Headers();
|
|
40
|
+
|
|
41
|
+
if (context.projectToken && context.projectId) {
|
|
42
|
+
throw new Error("provide either projectToken or projectId, not both");
|
|
43
|
+
}
|
|
44
|
+
if (context.serverId && context.externalId) {
|
|
45
|
+
throw new Error("provide either serverId or externalId, not both");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const projectHeader: [string, string] | undefined = context.projectToken
|
|
49
|
+
? ["X-Project-Token", context.projectToken]
|
|
50
|
+
: context.projectId
|
|
51
|
+
? ["X-Project-Id", context.projectId]
|
|
52
|
+
: undefined;
|
|
53
|
+
const subjectHeader: [string, string] | undefined = context.serverId
|
|
54
|
+
? ["X-Server-Id", context.serverId]
|
|
55
|
+
: context.externalId
|
|
56
|
+
? ["X-External-Id", context.externalId]
|
|
57
|
+
: undefined;
|
|
58
|
+
|
|
59
|
+
if (!projectHeader) {
|
|
60
|
+
throw new Error("feature flag check requires projectToken or projectId");
|
|
61
|
+
}
|
|
62
|
+
if (!subjectHeader) {
|
|
63
|
+
throw new Error("feature flag check requires serverId or externalId");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
headers.set(projectHeader[0], projectHeader[1]);
|
|
67
|
+
headers.set(subjectHeader[0], subjectHeader[1]);
|
|
68
|
+
|
|
69
|
+
if (context.attributes && Object.keys(context.attributes).length > 0) {
|
|
70
|
+
headers.set("X-Attributes", encodeAttributes(context.attributes));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const res = await fetch(url, {
|
|
74
|
+
method: "GET",
|
|
75
|
+
headers,
|
|
76
|
+
credentials: "omit",
|
|
77
|
+
signal: context.signal,
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw new Error(`feature flag check failed: HTTP ${res.status}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const data = (await res.json()) as { evaluation?: FeatureFlagEvaluation };
|
|
84
|
+
const evaluation = data.evaluation;
|
|
85
|
+
return evaluation?.enabled === true || evaluation?.enabled === false
|
|
86
|
+
? evaluation
|
|
87
|
+
: { enabled: false };
|
|
88
|
+
}
|
package/src/module.ts
CHANGED
|
@@ -14,3 +14,12 @@ export {
|
|
|
14
14
|
WebAnalytics,
|
|
15
15
|
type WebAnalyticsOptions,
|
|
16
16
|
} from "./analytics";
|
|
17
|
+
export {
|
|
18
|
+
type FeatureFlagCheckContext,
|
|
19
|
+
type FeatureFlagEvaluation,
|
|
20
|
+
fetchFeatureFlagEvaluation,
|
|
21
|
+
} from "./feature-flags";
|
|
22
|
+
export {
|
|
23
|
+
featureFlagsCheckUrl,
|
|
24
|
+
normalizeFeatureFlagsBaseUrl,
|
|
25
|
+
} from "./utils/api-urls";
|