@probat/react 0.4.4 → 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 +46 -9
- package/dist/index.d.ts +46 -9
- package/dist/index.js +346 -83
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +344 -84
- 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/components/Experiment.tsx +2 -1
- package/src/context/ProbatContext.tsx +32 -5
- package/src/hooks/useExperiment.ts +26 -4
- package/src/hooks/useProbatMetrics.ts +49 -5
- package/src/hooks/useTrack.ts +11 -7
- package/src/index.ts +11 -1
- package/src/types/events.ts +48 -0
- package/src/utils/api.ts +40 -22
- 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,56 +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
|
-
bootstrap,
|
|
17
|
-
children
|
|
18
|
-
}) {
|
|
19
|
-
const value = React3.useMemo(
|
|
20
|
-
() => ({
|
|
21
|
-
host: host.replace(/\/$/, ""),
|
|
22
|
-
customerId,
|
|
23
|
-
bootstrap: bootstrap ?? {}
|
|
24
|
-
}),
|
|
25
|
-
[customerId, host, bootstrap]
|
|
26
|
-
);
|
|
27
|
-
return /* @__PURE__ */ React3__default.default.createElement(ProbatContext.Provider, { value }, children);
|
|
28
|
-
}
|
|
29
|
-
function useProbatContext() {
|
|
30
|
-
const ctx = React3.useContext(ProbatContext);
|
|
31
|
-
if (!ctx) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
"useProbatContext must be used within <ProbatProviderClient>. Wrap your app with <ProbatProviderClient>."
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
return ctx;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// src/components/ProbatProviderClient.tsx
|
|
40
|
-
function ProbatProviderClient(props) {
|
|
41
|
-
return React3__default.default.createElement(ProbatProvider, props);
|
|
42
|
-
}
|
|
11
|
+
// src/types/events.ts
|
|
12
|
+
var PROBAT_ENV_DEV = "dev";
|
|
13
|
+
var PROBAT_ENV_PROD = "prod";
|
|
43
14
|
|
|
44
15
|
// src/utils/environment.ts
|
|
45
16
|
function detectEnvironment() {
|
|
46
17
|
if (typeof window === "undefined") {
|
|
47
|
-
return
|
|
18
|
+
return PROBAT_ENV_PROD;
|
|
48
19
|
}
|
|
49
20
|
const hostname = window.location.hostname;
|
|
50
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.")) {
|
|
51
|
-
return
|
|
22
|
+
return PROBAT_ENV_DEV;
|
|
52
23
|
}
|
|
53
|
-
return
|
|
24
|
+
return PROBAT_ENV_PROD;
|
|
54
25
|
}
|
|
55
26
|
|
|
56
27
|
// src/utils/eventContext.ts
|
|
57
28
|
var DISTINCT_ID_KEY = "probat:distinct_id";
|
|
58
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;
|
|
59
34
|
var cachedDistinctId = null;
|
|
60
35
|
var cachedSessionId = null;
|
|
36
|
+
var cachedSessionStartAt = null;
|
|
37
|
+
var cachedSessionSequence = 0;
|
|
38
|
+
var cachedLastActivityAt = 0;
|
|
61
39
|
function generateId() {
|
|
62
40
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
63
41
|
return crypto.randomUUID();
|
|
@@ -70,6 +48,77 @@ function generateId() {
|
|
|
70
48
|
}
|
|
71
49
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
72
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
|
+
}
|
|
73
122
|
function getDistinctId() {
|
|
74
123
|
if (cachedDistinctId) return cachedDistinctId;
|
|
75
124
|
if (typeof window === "undefined") return "server";
|
|
@@ -90,23 +139,14 @@ function getDistinctId() {
|
|
|
90
139
|
return id;
|
|
91
140
|
}
|
|
92
141
|
function getSessionId() {
|
|
93
|
-
if (cachedSessionId) return cachedSessionId;
|
|
94
142
|
if (typeof window === "undefined") return "server";
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
const id = `sess_${generateId()}`;
|
|
104
|
-
cachedSessionId = id;
|
|
105
|
-
try {
|
|
106
|
-
sessionStorage.setItem(SESSION_ID_KEY, id);
|
|
107
|
-
} catch {
|
|
108
|
-
}
|
|
109
|
-
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();
|
|
110
150
|
}
|
|
111
151
|
function getPageKey() {
|
|
112
152
|
if (typeof window === "undefined") return "";
|
|
@@ -121,29 +161,152 @@ function getReferrer() {
|
|
|
121
161
|
return document.referrer;
|
|
122
162
|
}
|
|
123
163
|
function buildEventContext() {
|
|
164
|
+
const ts = nowMs();
|
|
124
165
|
return {
|
|
125
166
|
distinct_id: getDistinctId(),
|
|
126
167
|
session_id: getSessionId(),
|
|
127
168
|
$page_url: getPageUrl(),
|
|
128
169
|
$pathname: typeof window !== "undefined" ? window.location.pathname : "",
|
|
129
|
-
$referrer: getReferrer()
|
|
170
|
+
$referrer: getReferrer(),
|
|
171
|
+
$session_sequence: bumpSequence(ts),
|
|
172
|
+
$session_start_at: getSessionStartAt()
|
|
130
173
|
};
|
|
131
174
|
}
|
|
132
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
|
+
}
|
|
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);
|
|
292
|
+
}
|
|
293
|
+
|
|
133
294
|
// src/utils/api.ts
|
|
134
295
|
var pendingDecisions = /* @__PURE__ */ new Map();
|
|
135
|
-
async function fetchDecision(host, experimentId, distinctId) {
|
|
296
|
+
async function fetchDecision(host, experimentId, distinctId, apiKey) {
|
|
136
297
|
const existing = pendingDecisions.get(experimentId);
|
|
137
298
|
if (existing) return existing;
|
|
138
299
|
const promise = (async () => {
|
|
139
300
|
try {
|
|
140
301
|
const url = `${host.replace(/\/$/, "")}/experiment/decide`;
|
|
302
|
+
const headers = {
|
|
303
|
+
"Content-Type": "application/json",
|
|
304
|
+
Accept: "application/json"
|
|
305
|
+
};
|
|
306
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
141
307
|
const res = await fetch(url, {
|
|
142
308
|
method: "POST",
|
|
143
|
-
headers
|
|
144
|
-
"Content-Type": "application/json",
|
|
145
|
-
Accept: "application/json"
|
|
146
|
-
},
|
|
309
|
+
headers,
|
|
147
310
|
credentials: "include",
|
|
148
311
|
body: JSON.stringify({
|
|
149
312
|
experiment_id: experimentId,
|
|
@@ -160,31 +323,35 @@ async function fetchDecision(host, experimentId, distinctId) {
|
|
|
160
323
|
pendingDecisions.set(experimentId, promise);
|
|
161
324
|
return promise;
|
|
162
325
|
}
|
|
163
|
-
function sendMetric(host, event, properties) {
|
|
326
|
+
function sendMetric(host, event, properties, apiKey) {
|
|
164
327
|
if (typeof window === "undefined") return;
|
|
328
|
+
const environment = detectEnvironment();
|
|
165
329
|
const ctx = buildEventContext();
|
|
166
330
|
const payload = {
|
|
167
331
|
event,
|
|
168
|
-
environment
|
|
332
|
+
environment,
|
|
169
333
|
properties: {
|
|
170
334
|
...ctx,
|
|
335
|
+
environment,
|
|
171
336
|
source: "react-sdk",
|
|
172
337
|
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
173
338
|
...properties
|
|
174
339
|
}
|
|
175
340
|
};
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
} 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);
|
|
186
350
|
}
|
|
187
351
|
}
|
|
352
|
+
function flushMetrics(host, forceBeacon = false, apiKey) {
|
|
353
|
+
flushEventQueue(host, forceBeacon, apiKey);
|
|
354
|
+
}
|
|
188
355
|
function extractClickMeta(target) {
|
|
189
356
|
if (!target || !(target instanceof HTMLElement)) return null;
|
|
190
357
|
const primary = target.closest('[data-probat-click="primary"]');
|
|
@@ -204,7 +371,56 @@ function buildMeta(el, isPrimary) {
|
|
|
204
371
|
return meta;
|
|
205
372
|
}
|
|
206
373
|
|
|
207
|
-
// 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
|
+
}
|
|
208
424
|
var ASSIGNMENT_PREFIX = "probat:assignment:";
|
|
209
425
|
function readAssignment(id) {
|
|
210
426
|
if (typeof window === "undefined") return null;
|
|
@@ -225,17 +441,33 @@ function writeAssignment(id, variantKey) {
|
|
|
225
441
|
} catch {
|
|
226
442
|
}
|
|
227
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
|
+
}
|
|
228
452
|
function useExperiment(id, options = {}) {
|
|
229
453
|
const { fallback = "control", debug = false } = options;
|
|
230
|
-
const { host, bootstrap, customerId } = useProbatContext();
|
|
454
|
+
const { host, bootstrap, customerId, apiKey } = useProbatContext();
|
|
231
455
|
const [variantKey, setVariantKey] = React3.useState(() => {
|
|
456
|
+
const forced = readForceParam();
|
|
457
|
+
if (forced) return forced;
|
|
232
458
|
if (bootstrap[id]) return bootstrap[id];
|
|
233
459
|
return "control";
|
|
234
460
|
});
|
|
235
461
|
const [resolved, setResolved] = React3.useState(() => {
|
|
236
|
-
return !!bootstrap[id];
|
|
462
|
+
return !!(readForceParam() || bootstrap[id]);
|
|
237
463
|
});
|
|
238
464
|
React3.useEffect(() => {
|
|
465
|
+
const forced = readForceParam();
|
|
466
|
+
if (forced) {
|
|
467
|
+
setVariantKey(forced);
|
|
468
|
+
setResolved(true);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
239
471
|
if (bootstrap[id] || readAssignment(id)) {
|
|
240
472
|
const key = bootstrap[id] ?? readAssignment(id) ?? "control";
|
|
241
473
|
setVariantKey(key);
|
|
@@ -246,7 +478,7 @@ function useExperiment(id, options = {}) {
|
|
|
246
478
|
(async () => {
|
|
247
479
|
try {
|
|
248
480
|
const distinctId = customerId ?? getDistinctId();
|
|
249
|
-
const key = await fetchDecision(host, id, distinctId);
|
|
481
|
+
const key = await fetchDecision(host, id, distinctId, apiKey);
|
|
250
482
|
if (cancelled) return;
|
|
251
483
|
setVariantKey(key);
|
|
252
484
|
writeAssignment(id, key);
|
|
@@ -365,6 +597,7 @@ function useTrack(options) {
|
|
|
365
597
|
const {
|
|
366
598
|
experimentId,
|
|
367
599
|
componentInstanceId,
|
|
600
|
+
resolved = true,
|
|
368
601
|
impression: trackImpression = true,
|
|
369
602
|
click: trackClick = true,
|
|
370
603
|
impressionEventName = "$experiment_exposure",
|
|
@@ -373,7 +606,7 @@ function useTrack(options) {
|
|
|
373
606
|
} = options;
|
|
374
607
|
const variantKey = options.variantKey ?? void 0;
|
|
375
608
|
const explicitCustomerId = "customerId" in options ? options.customerId : void 0;
|
|
376
|
-
const { host, customerId: providerCustomerId } = useProbatContext();
|
|
609
|
+
const { host, customerId: providerCustomerId, apiKey } = useProbatContext();
|
|
377
610
|
const resolvedCustomerId = explicitCustomerId ?? providerCustomerId;
|
|
378
611
|
const isCustomerMode = !variantKey;
|
|
379
612
|
const autoInstanceId = useStableInstanceId(experimentId);
|
|
@@ -398,7 +631,7 @@ function useTrack(options) {
|
|
|
398
631
|
);
|
|
399
632
|
const dedupeVariant = variantKey ?? resolvedCustomerId ?? "__anon__";
|
|
400
633
|
React3.useEffect(() => {
|
|
401
|
-
if (!trackImpression) return;
|
|
634
|
+
if (!trackImpression || !resolved) return;
|
|
402
635
|
impressionSent.current = false;
|
|
403
636
|
const pageKey = getPageKey();
|
|
404
637
|
const dedupeKey = makeDedupeKey(experimentId, dedupeVariant, instanceId, pageKey);
|
|
@@ -412,7 +645,7 @@ function useTrack(options) {
|
|
|
412
645
|
if (!impressionSent.current) {
|
|
413
646
|
impressionSent.current = true;
|
|
414
647
|
markSeen(dedupeKey);
|
|
415
|
-
sendMetric(host, impressionEventName, eventProps);
|
|
648
|
+
sendMetric(host, impressionEventName, eventProps, apiKey);
|
|
416
649
|
if (debug) console.log(`[probat] Impression sent (no IO) for "${experimentId}"`);
|
|
417
650
|
}
|
|
418
651
|
return;
|
|
@@ -426,7 +659,7 @@ function useTrack(options) {
|
|
|
426
659
|
if (impressionSent.current) return;
|
|
427
660
|
impressionSent.current = true;
|
|
428
661
|
markSeen(dedupeKey);
|
|
429
|
-
sendMetric(host, impressionEventName, eventProps);
|
|
662
|
+
sendMetric(host, impressionEventName, eventProps, apiKey);
|
|
430
663
|
if (debug) console.log(`[probat] Impression sent for "${experimentId}"`);
|
|
431
664
|
observer.disconnect();
|
|
432
665
|
}, 250);
|
|
@@ -444,6 +677,7 @@ function useTrack(options) {
|
|
|
444
677
|
};
|
|
445
678
|
}, [
|
|
446
679
|
trackImpression,
|
|
680
|
+
resolved,
|
|
447
681
|
experimentId,
|
|
448
682
|
dedupeVariant,
|
|
449
683
|
instanceId,
|
|
@@ -454,18 +688,18 @@ function useTrack(options) {
|
|
|
454
688
|
]);
|
|
455
689
|
const handleClick = React3.useCallback(
|
|
456
690
|
(e) => {
|
|
457
|
-
if (!trackClick) return;
|
|
691
|
+
if (!trackClick || !resolved) return;
|
|
458
692
|
const meta = extractClickMeta(e.target);
|
|
459
693
|
if (!meta) return;
|
|
460
694
|
sendMetric(host, clickEventName, {
|
|
461
695
|
...eventProps,
|
|
462
696
|
...meta
|
|
463
|
-
});
|
|
697
|
+
}, apiKey);
|
|
464
698
|
if (debug) {
|
|
465
699
|
console.log(`[probat] Click tracked for "${experimentId}"`, meta);
|
|
466
700
|
}
|
|
467
701
|
},
|
|
468
|
-
[trackClick, host, clickEventName, eventProps, experimentId, debug]
|
|
702
|
+
[trackClick, resolved, host, clickEventName, eventProps, experimentId, debug]
|
|
469
703
|
);
|
|
470
704
|
React3.useEffect(() => {
|
|
471
705
|
const el = containerRef.current;
|
|
@@ -499,7 +733,8 @@ function Experiment({
|
|
|
499
733
|
experimentId: id,
|
|
500
734
|
variantKey,
|
|
501
735
|
componentInstanceId,
|
|
502
|
-
|
|
736
|
+
resolved,
|
|
737
|
+
impression: track?.impression !== false,
|
|
503
738
|
click: track?.primaryClick !== false,
|
|
504
739
|
impressionEventName: track?.impressionEventName,
|
|
505
740
|
clickEventName: track?.clickEventName,
|
|
@@ -537,17 +772,42 @@ function Track({ children, ...trackOptions }) {
|
|
|
537
772
|
);
|
|
538
773
|
}
|
|
539
774
|
function useProbatMetrics() {
|
|
540
|
-
const { host, customerId } = useProbatContext();
|
|
775
|
+
const { host, customerId, apiKey } = useProbatContext();
|
|
541
776
|
const capture = React3.useCallback(
|
|
542
777
|
(event, properties = {}) => {
|
|
543
778
|
sendMetric(host, event, {
|
|
544
779
|
...customerId ? { distinct_id: customerId } : {},
|
|
545
780
|
...properties
|
|
781
|
+
}, apiKey);
|
|
782
|
+
},
|
|
783
|
+
[host, customerId, apiKey]
|
|
784
|
+
);
|
|
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
|
|
546
792
|
});
|
|
547
793
|
},
|
|
548
794
|
[host, customerId]
|
|
549
795
|
);
|
|
550
|
-
|
|
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
|
+
};
|
|
551
811
|
}
|
|
552
812
|
function createExperimentContext(experimentId) {
|
|
553
813
|
const Ctx = React3.createContext(null);
|
|
@@ -570,10 +830,13 @@ function createExperimentContext(experimentId) {
|
|
|
570
830
|
}
|
|
571
831
|
|
|
572
832
|
exports.Experiment = Experiment;
|
|
833
|
+
exports.PROBAT_ENV_DEV = PROBAT_ENV_DEV;
|
|
834
|
+
exports.PROBAT_ENV_PROD = PROBAT_ENV_PROD;
|
|
573
835
|
exports.ProbatProviderClient = ProbatProviderClient;
|
|
574
836
|
exports.Track = Track;
|
|
575
837
|
exports.createExperimentContext = createExperimentContext;
|
|
576
838
|
exports.fetchDecision = fetchDecision;
|
|
839
|
+
exports.flushMetrics = flushMetrics;
|
|
577
840
|
exports.sendMetric = sendMetric;
|
|
578
841
|
exports.useExperiment = useExperiment;
|
|
579
842
|
exports.useProbatMetrics = useProbatMetrics;
|