@probat/react 0.4.5 → 0.4.6
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/dist/index.d.mts +42 -9
- package/dist/index.d.ts +42 -9
- package/dist/index.js +324 -70
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +322 -71
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/Experiment.test.tsx +6 -5
- package/src/__tests__/Track.test.tsx +1 -1
- package/src/__tests__/eventQueue.test.ts +68 -0
- package/src/__tests__/setup.ts +55 -0
- package/src/__tests__/useExperiment.test.tsx +3 -2
- package/src/__tests__/useTrack.test.tsx +4 -4
- package/src/__tests__/utils.test.ts +24 -1
- package/src/context/ProbatContext.tsx +27 -5
- package/src/hooks/useExperiment.ts +24 -2
- package/src/hooks/useProbatMetrics.ts +47 -3
- package/src/index.ts +11 -1
- package/src/types/events.ts +48 -0
- package/src/utils/api.ts +30 -18
- package/src/utils/environment.ts +6 -5
- package/src/utils/eventContext.ts +117 -17
- package/src/utils/eventQueue.ts +157 -0
package/dist/index.js
CHANGED
|
@@ -8,58 +8,34 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
8
8
|
|
|
9
9
|
var React3__default = /*#__PURE__*/_interopDefault(React3);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
customerId,
|
|
15
|
-
host = DEFAULT_HOST,
|
|
16
|
-
apiKey,
|
|
17
|
-
bootstrap,
|
|
18
|
-
children
|
|
19
|
-
}) {
|
|
20
|
-
const value = React3.useMemo(
|
|
21
|
-
() => ({
|
|
22
|
-
host: host.replace(/\/$/, ""),
|
|
23
|
-
apiKey,
|
|
24
|
-
customerId,
|
|
25
|
-
bootstrap: bootstrap ?? {}
|
|
26
|
-
}),
|
|
27
|
-
[customerId, host, apiKey, bootstrap]
|
|
28
|
-
);
|
|
29
|
-
return /* @__PURE__ */ React3__default.default.createElement(ProbatContext.Provider, { value }, children);
|
|
30
|
-
}
|
|
31
|
-
function useProbatContext() {
|
|
32
|
-
const ctx = React3.useContext(ProbatContext);
|
|
33
|
-
if (!ctx) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
"useProbatContext must be used within <ProbatProviderClient>. Wrap your app with <ProbatProviderClient>."
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
return ctx;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// src/components/ProbatProviderClient.tsx
|
|
42
|
-
function ProbatProviderClient(props) {
|
|
43
|
-
return React3__default.default.createElement(ProbatProvider, props);
|
|
44
|
-
}
|
|
11
|
+
// src/types/events.ts
|
|
12
|
+
var PROBAT_ENV_DEV = "dev";
|
|
13
|
+
var PROBAT_ENV_PROD = "prod";
|
|
45
14
|
|
|
46
15
|
// src/utils/environment.ts
|
|
47
16
|
function detectEnvironment() {
|
|
48
17
|
if (typeof window === "undefined") {
|
|
49
|
-
return
|
|
18
|
+
return PROBAT_ENV_PROD;
|
|
50
19
|
}
|
|
51
20
|
const hostname = window.location.hostname;
|
|
52
21
|
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.startsWith("172.16.") || hostname.startsWith("172.17.") || hostname.startsWith("172.18.") || hostname.startsWith("172.19.") || hostname.startsWith("172.20.") || hostname.startsWith("172.21.") || hostname.startsWith("172.22.") || hostname.startsWith("172.23.") || hostname.startsWith("172.24.") || hostname.startsWith("172.25.") || hostname.startsWith("172.26.") || hostname.startsWith("172.27.") || hostname.startsWith("172.28.") || hostname.startsWith("172.29.") || hostname.startsWith("172.30.") || hostname.startsWith("172.31.")) {
|
|
53
|
-
return
|
|
22
|
+
return PROBAT_ENV_DEV;
|
|
54
23
|
}
|
|
55
|
-
return
|
|
24
|
+
return PROBAT_ENV_PROD;
|
|
56
25
|
}
|
|
57
26
|
|
|
58
27
|
// src/utils/eventContext.ts
|
|
59
28
|
var DISTINCT_ID_KEY = "probat:distinct_id";
|
|
60
29
|
var SESSION_ID_KEY = "probat:session_id";
|
|
30
|
+
var SESSION_START_AT_KEY = "probat:session_start_at";
|
|
31
|
+
var SESSION_SEQUENCE_KEY = "probat:session_sequence";
|
|
32
|
+
var SESSION_LAST_ACTIVITY_AT_KEY = "probat:session_last_activity_at";
|
|
33
|
+
var SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
61
34
|
var cachedDistinctId = null;
|
|
62
35
|
var cachedSessionId = null;
|
|
36
|
+
var cachedSessionStartAt = null;
|
|
37
|
+
var cachedSessionSequence = 0;
|
|
38
|
+
var cachedLastActivityAt = 0;
|
|
63
39
|
function generateId() {
|
|
64
40
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
65
41
|
return crypto.randomUUID();
|
|
@@ -72,6 +48,77 @@ function generateId() {
|
|
|
72
48
|
}
|
|
73
49
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
74
50
|
}
|
|
51
|
+
function nowMs() {
|
|
52
|
+
return Date.now();
|
|
53
|
+
}
|
|
54
|
+
function startNewSession(tsMs) {
|
|
55
|
+
if (typeof window === "undefined") return;
|
|
56
|
+
const sid = `sess_${generateId()}`;
|
|
57
|
+
const startedAt = new Date(tsMs).toISOString();
|
|
58
|
+
cachedSessionId = sid;
|
|
59
|
+
cachedSessionStartAt = startedAt;
|
|
60
|
+
cachedSessionSequence = 0;
|
|
61
|
+
cachedLastActivityAt = tsMs;
|
|
62
|
+
try {
|
|
63
|
+
sessionStorage.setItem(SESSION_ID_KEY, sid);
|
|
64
|
+
sessionStorage.setItem(SESSION_START_AT_KEY, startedAt);
|
|
65
|
+
sessionStorage.setItem(SESSION_SEQUENCE_KEY, "0");
|
|
66
|
+
sessionStorage.setItem(SESSION_LAST_ACTIVITY_AT_KEY, String(tsMs));
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function hydrateSessionFromStorage(tsMs) {
|
|
71
|
+
if (typeof window === "undefined") return;
|
|
72
|
+
if (cachedSessionId && cachedSessionStartAt) {
|
|
73
|
+
const expiredInMemory = !Number.isFinite(cachedLastActivityAt) || cachedLastActivityAt <= 0 || tsMs - cachedLastActivityAt > SESSION_IDLE_TIMEOUT_MS;
|
|
74
|
+
if (expiredInMemory) {
|
|
75
|
+
startNewSession(tsMs);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const sid = sessionStorage.getItem(SESSION_ID_KEY);
|
|
81
|
+
const startedAt = sessionStorage.getItem(SESSION_START_AT_KEY);
|
|
82
|
+
const sequenceRaw = sessionStorage.getItem(SESSION_SEQUENCE_KEY);
|
|
83
|
+
const lastActivityRaw = sessionStorage.getItem(SESSION_LAST_ACTIVITY_AT_KEY);
|
|
84
|
+
const sequence = Number(sequenceRaw || "0");
|
|
85
|
+
const lastActivity = Number(lastActivityRaw || "0");
|
|
86
|
+
if (!sid || !startedAt) {
|
|
87
|
+
startNewSession(tsMs);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const expired = !Number.isFinite(lastActivity) || lastActivity <= 0 || tsMs - lastActivity > SESSION_IDLE_TIMEOUT_MS;
|
|
91
|
+
if (expired) {
|
|
92
|
+
startNewSession(tsMs);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
cachedSessionId = sid;
|
|
96
|
+
cachedSessionStartAt = startedAt;
|
|
97
|
+
cachedSessionSequence = Number.isFinite(sequence) && sequence >= 0 ? sequence : 0;
|
|
98
|
+
cachedLastActivityAt = lastActivity > 0 ? lastActivity : tsMs;
|
|
99
|
+
} catch {
|
|
100
|
+
startNewSession(tsMs);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function ensureSession(tsMs) {
|
|
104
|
+
if (typeof window === "undefined") return;
|
|
105
|
+
hydrateSessionFromStorage(tsMs);
|
|
106
|
+
if (!cachedSessionId || !cachedSessionStartAt) {
|
|
107
|
+
startNewSession(tsMs);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function bumpSequence(tsMs) {
|
|
111
|
+
if (typeof window === "undefined") return 0;
|
|
112
|
+
ensureSession(tsMs);
|
|
113
|
+
cachedSessionSequence += 1;
|
|
114
|
+
cachedLastActivityAt = tsMs;
|
|
115
|
+
try {
|
|
116
|
+
sessionStorage.setItem(SESSION_SEQUENCE_KEY, String(cachedSessionSequence));
|
|
117
|
+
sessionStorage.setItem(SESSION_LAST_ACTIVITY_AT_KEY, String(tsMs));
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
return cachedSessionSequence;
|
|
121
|
+
}
|
|
75
122
|
function getDistinctId() {
|
|
76
123
|
if (cachedDistinctId) return cachedDistinctId;
|
|
77
124
|
if (typeof window === "undefined") return "server";
|
|
@@ -92,23 +139,14 @@ function getDistinctId() {
|
|
|
92
139
|
return id;
|
|
93
140
|
}
|
|
94
141
|
function getSessionId() {
|
|
95
|
-
if (cachedSessionId) return cachedSessionId;
|
|
96
142
|
if (typeof window === "undefined") return "server";
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
const id = `sess_${generateId()}`;
|
|
106
|
-
cachedSessionId = id;
|
|
107
|
-
try {
|
|
108
|
-
sessionStorage.setItem(SESSION_ID_KEY, id);
|
|
109
|
-
} catch {
|
|
110
|
-
}
|
|
111
|
-
return id;
|
|
143
|
+
ensureSession(nowMs());
|
|
144
|
+
return cachedSessionId || "server";
|
|
145
|
+
}
|
|
146
|
+
function getSessionStartAt() {
|
|
147
|
+
if (typeof window === "undefined") return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
148
|
+
ensureSession(nowMs());
|
|
149
|
+
return cachedSessionStartAt || (/* @__PURE__ */ new Date(0)).toISOString();
|
|
112
150
|
}
|
|
113
151
|
function getPageKey() {
|
|
114
152
|
if (typeof window === "undefined") return "";
|
|
@@ -123,13 +161,134 @@ function getReferrer() {
|
|
|
123
161
|
return document.referrer;
|
|
124
162
|
}
|
|
125
163
|
function buildEventContext() {
|
|
164
|
+
const ts = nowMs();
|
|
126
165
|
return {
|
|
127
166
|
distinct_id: getDistinctId(),
|
|
128
167
|
session_id: getSessionId(),
|
|
129
168
|
$page_url: getPageUrl(),
|
|
130
169
|
$pathname: typeof window !== "undefined" ? window.location.pathname : "",
|
|
131
|
-
$referrer: getReferrer()
|
|
170
|
+
$referrer: getReferrer(),
|
|
171
|
+
$session_sequence: bumpSequence(ts),
|
|
172
|
+
$session_start_at: getSessionStartAt()
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/utils/eventQueue.ts
|
|
177
|
+
var EventQueue = class {
|
|
178
|
+
constructor(host, apiKey) {
|
|
179
|
+
this.queue = [];
|
|
180
|
+
this.flushTimer = null;
|
|
181
|
+
this.maxBatchSize = 20;
|
|
182
|
+
this.flushIntervalMs = 5e3;
|
|
183
|
+
const normalized = host.replace(/\/$/, "");
|
|
184
|
+
this.endpointBatch = `${normalized}/experiment/metrics/batch`;
|
|
185
|
+
this.endpointSingle = `${normalized}/experiment/metrics`;
|
|
186
|
+
this.apiKey = apiKey;
|
|
187
|
+
}
|
|
188
|
+
enqueue(event) {
|
|
189
|
+
this.queue.push(event);
|
|
190
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
191
|
+
this.flush();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!this.flushTimer) {
|
|
195
|
+
this.flushTimer = setTimeout(() => {
|
|
196
|
+
this.flush();
|
|
197
|
+
}, this.flushIntervalMs);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
flush(forceBeacon = false) {
|
|
201
|
+
if (this.flushTimer) {
|
|
202
|
+
clearTimeout(this.flushTimer);
|
|
203
|
+
this.flushTimer = null;
|
|
204
|
+
}
|
|
205
|
+
if (this.queue.length === 0) return;
|
|
206
|
+
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
207
|
+
this.send(batch, forceBeacon);
|
|
208
|
+
if (this.queue.length > 0) {
|
|
209
|
+
this.flushTimer = setTimeout(() => {
|
|
210
|
+
this.flush();
|
|
211
|
+
}, this.flushIntervalMs);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
send(batch, forceBeacon = false) {
|
|
215
|
+
const body = { events: batch };
|
|
216
|
+
const encodedBody = JSON.stringify(body);
|
|
217
|
+
const headers = { "Content-Type": "application/json" };
|
|
218
|
+
if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
219
|
+
const canBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && !this.apiKey && (forceBeacon || typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
220
|
+
if (canBeacon) {
|
|
221
|
+
navigator.sendBeacon(this.endpointBatch, encodedBody);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
if (batch.length === 1) {
|
|
226
|
+
fetch(this.endpointSingle, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers,
|
|
229
|
+
credentials: "include",
|
|
230
|
+
body: JSON.stringify(batch[0]),
|
|
231
|
+
keepalive: true
|
|
232
|
+
}).catch(() => {
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
fetch(this.endpointBatch, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers,
|
|
239
|
+
credentials: "include",
|
|
240
|
+
body: encodedBody,
|
|
241
|
+
keepalive: true
|
|
242
|
+
}).catch(() => {
|
|
243
|
+
});
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
var queuesByHost = /* @__PURE__ */ new Map();
|
|
249
|
+
var lifecycleListenersInstalled = false;
|
|
250
|
+
function installLifecycleListeners() {
|
|
251
|
+
if (lifecycleListenersInstalled || typeof window === "undefined") return;
|
|
252
|
+
lifecycleListenersInstalled = true;
|
|
253
|
+
const flushWithBeacon = () => {
|
|
254
|
+
for (const queue of queuesByHost.values()) {
|
|
255
|
+
queue.flush(true);
|
|
256
|
+
}
|
|
132
257
|
};
|
|
258
|
+
const flushNormally = () => {
|
|
259
|
+
for (const queue of queuesByHost.values()) {
|
|
260
|
+
queue.flush(false);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
window.addEventListener("pagehide", flushWithBeacon);
|
|
264
|
+
window.addEventListener("beforeunload", flushWithBeacon);
|
|
265
|
+
document.addEventListener("visibilitychange", () => {
|
|
266
|
+
if (document.visibilityState === "hidden") {
|
|
267
|
+
flushWithBeacon();
|
|
268
|
+
} else {
|
|
269
|
+
flushNormally();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function queueKey(host, apiKey) {
|
|
274
|
+
const normalized = host.replace(/\/$/, "");
|
|
275
|
+
return apiKey ? `${normalized}::${apiKey}` : `${normalized}::__no_key__`;
|
|
276
|
+
}
|
|
277
|
+
function getEventQueue(host, apiKey) {
|
|
278
|
+
const normalized = host.replace(/\/$/, "");
|
|
279
|
+
const key = queueKey(normalized, apiKey);
|
|
280
|
+
let queue = queuesByHost.get(key);
|
|
281
|
+
if (!queue) {
|
|
282
|
+
queue = new EventQueue(normalized, apiKey);
|
|
283
|
+
queuesByHost.set(key, queue);
|
|
284
|
+
}
|
|
285
|
+
installLifecycleListeners();
|
|
286
|
+
return queue;
|
|
287
|
+
}
|
|
288
|
+
function flushEventQueue(host, forceBeacon = false, apiKey) {
|
|
289
|
+
const queue = queuesByHost.get(queueKey(host, apiKey));
|
|
290
|
+
if (!queue) return;
|
|
291
|
+
queue.flush(forceBeacon);
|
|
133
292
|
}
|
|
134
293
|
|
|
135
294
|
// src/utils/api.ts
|
|
@@ -166,31 +325,33 @@ async function fetchDecision(host, experimentId, distinctId, apiKey) {
|
|
|
166
325
|
}
|
|
167
326
|
function sendMetric(host, event, properties, apiKey) {
|
|
168
327
|
if (typeof window === "undefined") return;
|
|
328
|
+
const environment = detectEnvironment();
|
|
169
329
|
const ctx = buildEventContext();
|
|
170
330
|
const payload = {
|
|
171
331
|
event,
|
|
172
|
-
environment
|
|
332
|
+
environment,
|
|
173
333
|
properties: {
|
|
174
334
|
...ctx,
|
|
335
|
+
environment,
|
|
175
336
|
source: "react-sdk",
|
|
176
337
|
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
177
338
|
...properties
|
|
178
339
|
}
|
|
179
340
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}).catch(() => {
|
|
190
|
-
});
|
|
191
|
-
} catch {
|
|
341
|
+
const queuePayload = {
|
|
342
|
+
event: payload.event,
|
|
343
|
+
environment: payload.environment,
|
|
344
|
+
properties: payload.properties
|
|
345
|
+
};
|
|
346
|
+
const queue = getEventQueue(host, apiKey);
|
|
347
|
+
queue.enqueue(queuePayload);
|
|
348
|
+
if (event === "$experiment_exposure" || event === "$experiment_click") {
|
|
349
|
+
queue.flush(false);
|
|
192
350
|
}
|
|
193
351
|
}
|
|
352
|
+
function flushMetrics(host, forceBeacon = false, apiKey) {
|
|
353
|
+
flushEventQueue(host, forceBeacon, apiKey);
|
|
354
|
+
}
|
|
194
355
|
function extractClickMeta(target) {
|
|
195
356
|
if (!target || !(target instanceof HTMLElement)) return null;
|
|
196
357
|
const primary = target.closest('[data-probat-click="primary"]');
|
|
@@ -210,7 +371,56 @@ function buildMeta(el, isPrimary) {
|
|
|
210
371
|
return meta;
|
|
211
372
|
}
|
|
212
373
|
|
|
213
|
-
// src/
|
|
374
|
+
// src/context/ProbatContext.tsx
|
|
375
|
+
var ProbatContext = React3.createContext(null);
|
|
376
|
+
var DEFAULT_HOST = "https://api.probat.app";
|
|
377
|
+
function ProbatProvider({
|
|
378
|
+
customerId,
|
|
379
|
+
host = DEFAULT_HOST,
|
|
380
|
+
apiKey,
|
|
381
|
+
bootstrap,
|
|
382
|
+
trackSessionLifecycle = true,
|
|
383
|
+
children
|
|
384
|
+
}) {
|
|
385
|
+
const normalizedHost = host.replace(/\/$/, "");
|
|
386
|
+
React3.useEffect(() => {
|
|
387
|
+
if (!trackSessionLifecycle) return;
|
|
388
|
+
if (typeof window === "undefined") return;
|
|
389
|
+
sendMetric(normalizedHost, "$session_start", {
|
|
390
|
+
...customerId ? { distinct_id: customerId } : {}
|
|
391
|
+
}, apiKey);
|
|
392
|
+
return () => {
|
|
393
|
+
sendMetric(normalizedHost, "$session_end", {
|
|
394
|
+
...customerId ? { distinct_id: customerId } : {}
|
|
395
|
+
}, apiKey);
|
|
396
|
+
flushMetrics(normalizedHost, true, apiKey);
|
|
397
|
+
};
|
|
398
|
+
}, [normalizedHost, customerId, trackSessionLifecycle, apiKey]);
|
|
399
|
+
const value = React3.useMemo(
|
|
400
|
+
() => ({
|
|
401
|
+
host: normalizedHost,
|
|
402
|
+
apiKey,
|
|
403
|
+
customerId,
|
|
404
|
+
bootstrap: bootstrap ?? {}
|
|
405
|
+
}),
|
|
406
|
+
[customerId, normalizedHost, apiKey, bootstrap]
|
|
407
|
+
);
|
|
408
|
+
return /* @__PURE__ */ React3__default.default.createElement(ProbatContext.Provider, { value }, children);
|
|
409
|
+
}
|
|
410
|
+
function useProbatContext() {
|
|
411
|
+
const ctx = React3.useContext(ProbatContext);
|
|
412
|
+
if (!ctx) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
"useProbatContext must be used within <ProbatProviderClient>. Wrap your app with <ProbatProviderClient>."
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return ctx;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/components/ProbatProviderClient.tsx
|
|
421
|
+
function ProbatProviderClient(props) {
|
|
422
|
+
return React3__default.default.createElement(ProbatProvider, props);
|
|
423
|
+
}
|
|
214
424
|
var ASSIGNMENT_PREFIX = "probat:assignment:";
|
|
215
425
|
function readAssignment(id) {
|
|
216
426
|
if (typeof window === "undefined") return null;
|
|
@@ -231,17 +441,33 @@ function writeAssignment(id, variantKey) {
|
|
|
231
441
|
} catch {
|
|
232
442
|
}
|
|
233
443
|
}
|
|
444
|
+
function readForceParam() {
|
|
445
|
+
if (typeof window === "undefined") return null;
|
|
446
|
+
try {
|
|
447
|
+
return new URLSearchParams(window.location.search).get("__probat_force");
|
|
448
|
+
} catch {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
234
452
|
function useExperiment(id, options = {}) {
|
|
235
453
|
const { fallback = "control", debug = false } = options;
|
|
236
454
|
const { host, bootstrap, customerId, apiKey } = useProbatContext();
|
|
237
455
|
const [variantKey, setVariantKey] = React3.useState(() => {
|
|
456
|
+
const forced = readForceParam();
|
|
457
|
+
if (forced) return forced;
|
|
238
458
|
if (bootstrap[id]) return bootstrap[id];
|
|
239
459
|
return "control";
|
|
240
460
|
});
|
|
241
461
|
const [resolved, setResolved] = React3.useState(() => {
|
|
242
|
-
return !!bootstrap[id];
|
|
462
|
+
return !!(readForceParam() || bootstrap[id]);
|
|
243
463
|
});
|
|
244
464
|
React3.useEffect(() => {
|
|
465
|
+
const forced = readForceParam();
|
|
466
|
+
if (forced) {
|
|
467
|
+
setVariantKey(forced);
|
|
468
|
+
setResolved(true);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
245
471
|
if (bootstrap[id] || readAssignment(id)) {
|
|
246
472
|
const key = bootstrap[id] ?? readAssignment(id) ?? "control";
|
|
247
473
|
setVariantKey(key);
|
|
@@ -556,7 +782,32 @@ function useProbatMetrics() {
|
|
|
556
782
|
},
|
|
557
783
|
[host, customerId, apiKey]
|
|
558
784
|
);
|
|
559
|
-
|
|
785
|
+
const captureGoal = React3.useCallback(
|
|
786
|
+
(funnelId, funnelStep, properties = {}) => {
|
|
787
|
+
sendMetric(host, "$goal_reached", {
|
|
788
|
+
...customerId ? { distinct_id: customerId } : {},
|
|
789
|
+
$funnel_id: funnelId,
|
|
790
|
+
$funnel_step: funnelStep,
|
|
791
|
+
...properties
|
|
792
|
+
});
|
|
793
|
+
},
|
|
794
|
+
[host, customerId]
|
|
795
|
+
);
|
|
796
|
+
const captureFeatureInteraction = React3.useCallback(
|
|
797
|
+
(interactionName, properties = {}) => {
|
|
798
|
+
sendMetric(host, "$feature_interaction", {
|
|
799
|
+
...customerId ? { distinct_id: customerId } : {},
|
|
800
|
+
interaction_name: interactionName,
|
|
801
|
+
...properties
|
|
802
|
+
});
|
|
803
|
+
},
|
|
804
|
+
[host, customerId]
|
|
805
|
+
);
|
|
806
|
+
return {
|
|
807
|
+
capture,
|
|
808
|
+
captureGoal,
|
|
809
|
+
captureFeatureInteraction
|
|
810
|
+
};
|
|
560
811
|
}
|
|
561
812
|
function createExperimentContext(experimentId) {
|
|
562
813
|
const Ctx = React3.createContext(null);
|
|
@@ -579,10 +830,13 @@ function createExperimentContext(experimentId) {
|
|
|
579
830
|
}
|
|
580
831
|
|
|
581
832
|
exports.Experiment = Experiment;
|
|
833
|
+
exports.PROBAT_ENV_DEV = PROBAT_ENV_DEV;
|
|
834
|
+
exports.PROBAT_ENV_PROD = PROBAT_ENV_PROD;
|
|
582
835
|
exports.ProbatProviderClient = ProbatProviderClient;
|
|
583
836
|
exports.Track = Track;
|
|
584
837
|
exports.createExperimentContext = createExperimentContext;
|
|
585
838
|
exports.fetchDecision = fetchDecision;
|
|
839
|
+
exports.flushMetrics = flushMetrics;
|
|
586
840
|
exports.sendMetric = sendMetric;
|
|
587
841
|
exports.useExperiment = useExperiment;
|
|
588
842
|
exports.useProbatMetrics = useProbatMetrics;
|