@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.mjs
CHANGED
|
@@ -2,56 +2,34 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
import React3, { createContext, useRef, useState, useEffect, useMemo, useCallback, useContext } from 'react';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
customerId,
|
|
9
|
-
host = DEFAULT_HOST,
|
|
10
|
-
bootstrap,
|
|
11
|
-
children
|
|
12
|
-
}) {
|
|
13
|
-
const value = useMemo(
|
|
14
|
-
() => ({
|
|
15
|
-
host: host.replace(/\/$/, ""),
|
|
16
|
-
customerId,
|
|
17
|
-
bootstrap: bootstrap ?? {}
|
|
18
|
-
}),
|
|
19
|
-
[customerId, host, bootstrap]
|
|
20
|
-
);
|
|
21
|
-
return /* @__PURE__ */ React3.createElement(ProbatContext.Provider, { value }, children);
|
|
22
|
-
}
|
|
23
|
-
function useProbatContext() {
|
|
24
|
-
const ctx = useContext(ProbatContext);
|
|
25
|
-
if (!ctx) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
"useProbatContext must be used within <ProbatProviderClient>. Wrap your app with <ProbatProviderClient>."
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
return ctx;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// src/components/ProbatProviderClient.tsx
|
|
34
|
-
function ProbatProviderClient(props) {
|
|
35
|
-
return React3.createElement(ProbatProvider, props);
|
|
36
|
-
}
|
|
5
|
+
// src/types/events.ts
|
|
6
|
+
var PROBAT_ENV_DEV = "dev";
|
|
7
|
+
var PROBAT_ENV_PROD = "prod";
|
|
37
8
|
|
|
38
9
|
// src/utils/environment.ts
|
|
39
10
|
function detectEnvironment() {
|
|
40
11
|
if (typeof window === "undefined") {
|
|
41
|
-
return
|
|
12
|
+
return PROBAT_ENV_PROD;
|
|
42
13
|
}
|
|
43
14
|
const hostname = window.location.hostname;
|
|
44
15
|
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.")) {
|
|
45
|
-
return
|
|
16
|
+
return PROBAT_ENV_DEV;
|
|
46
17
|
}
|
|
47
|
-
return
|
|
18
|
+
return PROBAT_ENV_PROD;
|
|
48
19
|
}
|
|
49
20
|
|
|
50
21
|
// src/utils/eventContext.ts
|
|
51
22
|
var DISTINCT_ID_KEY = "probat:distinct_id";
|
|
52
23
|
var SESSION_ID_KEY = "probat:session_id";
|
|
24
|
+
var SESSION_START_AT_KEY = "probat:session_start_at";
|
|
25
|
+
var SESSION_SEQUENCE_KEY = "probat:session_sequence";
|
|
26
|
+
var SESSION_LAST_ACTIVITY_AT_KEY = "probat:session_last_activity_at";
|
|
27
|
+
var SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
53
28
|
var cachedDistinctId = null;
|
|
54
29
|
var cachedSessionId = null;
|
|
30
|
+
var cachedSessionStartAt = null;
|
|
31
|
+
var cachedSessionSequence = 0;
|
|
32
|
+
var cachedLastActivityAt = 0;
|
|
55
33
|
function generateId() {
|
|
56
34
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
57
35
|
return crypto.randomUUID();
|
|
@@ -64,6 +42,77 @@ function generateId() {
|
|
|
64
42
|
}
|
|
65
43
|
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
66
44
|
}
|
|
45
|
+
function nowMs() {
|
|
46
|
+
return Date.now();
|
|
47
|
+
}
|
|
48
|
+
function startNewSession(tsMs) {
|
|
49
|
+
if (typeof window === "undefined") return;
|
|
50
|
+
const sid = `sess_${generateId()}`;
|
|
51
|
+
const startedAt = new Date(tsMs).toISOString();
|
|
52
|
+
cachedSessionId = sid;
|
|
53
|
+
cachedSessionStartAt = startedAt;
|
|
54
|
+
cachedSessionSequence = 0;
|
|
55
|
+
cachedLastActivityAt = tsMs;
|
|
56
|
+
try {
|
|
57
|
+
sessionStorage.setItem(SESSION_ID_KEY, sid);
|
|
58
|
+
sessionStorage.setItem(SESSION_START_AT_KEY, startedAt);
|
|
59
|
+
sessionStorage.setItem(SESSION_SEQUENCE_KEY, "0");
|
|
60
|
+
sessionStorage.setItem(SESSION_LAST_ACTIVITY_AT_KEY, String(tsMs));
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function hydrateSessionFromStorage(tsMs) {
|
|
65
|
+
if (typeof window === "undefined") return;
|
|
66
|
+
if (cachedSessionId && cachedSessionStartAt) {
|
|
67
|
+
const expiredInMemory = !Number.isFinite(cachedLastActivityAt) || cachedLastActivityAt <= 0 || tsMs - cachedLastActivityAt > SESSION_IDLE_TIMEOUT_MS;
|
|
68
|
+
if (expiredInMemory) {
|
|
69
|
+
startNewSession(tsMs);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const sid = sessionStorage.getItem(SESSION_ID_KEY);
|
|
75
|
+
const startedAt = sessionStorage.getItem(SESSION_START_AT_KEY);
|
|
76
|
+
const sequenceRaw = sessionStorage.getItem(SESSION_SEQUENCE_KEY);
|
|
77
|
+
const lastActivityRaw = sessionStorage.getItem(SESSION_LAST_ACTIVITY_AT_KEY);
|
|
78
|
+
const sequence = Number(sequenceRaw || "0");
|
|
79
|
+
const lastActivity = Number(lastActivityRaw || "0");
|
|
80
|
+
if (!sid || !startedAt) {
|
|
81
|
+
startNewSession(tsMs);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const expired = !Number.isFinite(lastActivity) || lastActivity <= 0 || tsMs - lastActivity > SESSION_IDLE_TIMEOUT_MS;
|
|
85
|
+
if (expired) {
|
|
86
|
+
startNewSession(tsMs);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
cachedSessionId = sid;
|
|
90
|
+
cachedSessionStartAt = startedAt;
|
|
91
|
+
cachedSessionSequence = Number.isFinite(sequence) && sequence >= 0 ? sequence : 0;
|
|
92
|
+
cachedLastActivityAt = lastActivity > 0 ? lastActivity : tsMs;
|
|
93
|
+
} catch {
|
|
94
|
+
startNewSession(tsMs);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function ensureSession(tsMs) {
|
|
98
|
+
if (typeof window === "undefined") return;
|
|
99
|
+
hydrateSessionFromStorage(tsMs);
|
|
100
|
+
if (!cachedSessionId || !cachedSessionStartAt) {
|
|
101
|
+
startNewSession(tsMs);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function bumpSequence(tsMs) {
|
|
105
|
+
if (typeof window === "undefined") return 0;
|
|
106
|
+
ensureSession(tsMs);
|
|
107
|
+
cachedSessionSequence += 1;
|
|
108
|
+
cachedLastActivityAt = tsMs;
|
|
109
|
+
try {
|
|
110
|
+
sessionStorage.setItem(SESSION_SEQUENCE_KEY, String(cachedSessionSequence));
|
|
111
|
+
sessionStorage.setItem(SESSION_LAST_ACTIVITY_AT_KEY, String(tsMs));
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
return cachedSessionSequence;
|
|
115
|
+
}
|
|
67
116
|
function getDistinctId() {
|
|
68
117
|
if (cachedDistinctId) return cachedDistinctId;
|
|
69
118
|
if (typeof window === "undefined") return "server";
|
|
@@ -84,23 +133,14 @@ function getDistinctId() {
|
|
|
84
133
|
return id;
|
|
85
134
|
}
|
|
86
135
|
function getSessionId() {
|
|
87
|
-
if (cachedSessionId) return cachedSessionId;
|
|
88
136
|
if (typeof window === "undefined") return "server";
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
const id = `sess_${generateId()}`;
|
|
98
|
-
cachedSessionId = id;
|
|
99
|
-
try {
|
|
100
|
-
sessionStorage.setItem(SESSION_ID_KEY, id);
|
|
101
|
-
} catch {
|
|
102
|
-
}
|
|
103
|
-
return id;
|
|
137
|
+
ensureSession(nowMs());
|
|
138
|
+
return cachedSessionId || "server";
|
|
139
|
+
}
|
|
140
|
+
function getSessionStartAt() {
|
|
141
|
+
if (typeof window === "undefined") return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
142
|
+
ensureSession(nowMs());
|
|
143
|
+
return cachedSessionStartAt || (/* @__PURE__ */ new Date(0)).toISOString();
|
|
104
144
|
}
|
|
105
145
|
function getPageKey() {
|
|
106
146
|
if (typeof window === "undefined") return "";
|
|
@@ -115,29 +155,152 @@ function getReferrer() {
|
|
|
115
155
|
return document.referrer;
|
|
116
156
|
}
|
|
117
157
|
function buildEventContext() {
|
|
158
|
+
const ts = nowMs();
|
|
118
159
|
return {
|
|
119
160
|
distinct_id: getDistinctId(),
|
|
120
161
|
session_id: getSessionId(),
|
|
121
162
|
$page_url: getPageUrl(),
|
|
122
163
|
$pathname: typeof window !== "undefined" ? window.location.pathname : "",
|
|
123
|
-
$referrer: getReferrer()
|
|
164
|
+
$referrer: getReferrer(),
|
|
165
|
+
$session_sequence: bumpSequence(ts),
|
|
166
|
+
$session_start_at: getSessionStartAt()
|
|
124
167
|
};
|
|
125
168
|
}
|
|
126
169
|
|
|
170
|
+
// src/utils/eventQueue.ts
|
|
171
|
+
var EventQueue = class {
|
|
172
|
+
constructor(host, apiKey) {
|
|
173
|
+
this.queue = [];
|
|
174
|
+
this.flushTimer = null;
|
|
175
|
+
this.maxBatchSize = 20;
|
|
176
|
+
this.flushIntervalMs = 5e3;
|
|
177
|
+
const normalized = host.replace(/\/$/, "");
|
|
178
|
+
this.endpointBatch = `${normalized}/experiment/metrics/batch`;
|
|
179
|
+
this.endpointSingle = `${normalized}/experiment/metrics`;
|
|
180
|
+
this.apiKey = apiKey;
|
|
181
|
+
}
|
|
182
|
+
enqueue(event) {
|
|
183
|
+
this.queue.push(event);
|
|
184
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
185
|
+
this.flush();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!this.flushTimer) {
|
|
189
|
+
this.flushTimer = setTimeout(() => {
|
|
190
|
+
this.flush();
|
|
191
|
+
}, this.flushIntervalMs);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
flush(forceBeacon = false) {
|
|
195
|
+
if (this.flushTimer) {
|
|
196
|
+
clearTimeout(this.flushTimer);
|
|
197
|
+
this.flushTimer = null;
|
|
198
|
+
}
|
|
199
|
+
if (this.queue.length === 0) return;
|
|
200
|
+
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
201
|
+
this.send(batch, forceBeacon);
|
|
202
|
+
if (this.queue.length > 0) {
|
|
203
|
+
this.flushTimer = setTimeout(() => {
|
|
204
|
+
this.flush();
|
|
205
|
+
}, this.flushIntervalMs);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
send(batch, forceBeacon = false) {
|
|
209
|
+
const body = { events: batch };
|
|
210
|
+
const encodedBody = JSON.stringify(body);
|
|
211
|
+
const headers = { "Content-Type": "application/json" };
|
|
212
|
+
if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
213
|
+
const canBeacon = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && !this.apiKey && (forceBeacon || typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
214
|
+
if (canBeacon) {
|
|
215
|
+
navigator.sendBeacon(this.endpointBatch, encodedBody);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
if (batch.length === 1) {
|
|
220
|
+
fetch(this.endpointSingle, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers,
|
|
223
|
+
credentials: "include",
|
|
224
|
+
body: JSON.stringify(batch[0]),
|
|
225
|
+
keepalive: true
|
|
226
|
+
}).catch(() => {
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
fetch(this.endpointBatch, {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers,
|
|
233
|
+
credentials: "include",
|
|
234
|
+
body: encodedBody,
|
|
235
|
+
keepalive: true
|
|
236
|
+
}).catch(() => {
|
|
237
|
+
});
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
var queuesByHost = /* @__PURE__ */ new Map();
|
|
243
|
+
var lifecycleListenersInstalled = false;
|
|
244
|
+
function installLifecycleListeners() {
|
|
245
|
+
if (lifecycleListenersInstalled || typeof window === "undefined") return;
|
|
246
|
+
lifecycleListenersInstalled = true;
|
|
247
|
+
const flushWithBeacon = () => {
|
|
248
|
+
for (const queue of queuesByHost.values()) {
|
|
249
|
+
queue.flush(true);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const flushNormally = () => {
|
|
253
|
+
for (const queue of queuesByHost.values()) {
|
|
254
|
+
queue.flush(false);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
window.addEventListener("pagehide", flushWithBeacon);
|
|
258
|
+
window.addEventListener("beforeunload", flushWithBeacon);
|
|
259
|
+
document.addEventListener("visibilitychange", () => {
|
|
260
|
+
if (document.visibilityState === "hidden") {
|
|
261
|
+
flushWithBeacon();
|
|
262
|
+
} else {
|
|
263
|
+
flushNormally();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function queueKey(host, apiKey) {
|
|
268
|
+
const normalized = host.replace(/\/$/, "");
|
|
269
|
+
return apiKey ? `${normalized}::${apiKey}` : `${normalized}::__no_key__`;
|
|
270
|
+
}
|
|
271
|
+
function getEventQueue(host, apiKey) {
|
|
272
|
+
const normalized = host.replace(/\/$/, "");
|
|
273
|
+
const key = queueKey(normalized, apiKey);
|
|
274
|
+
let queue = queuesByHost.get(key);
|
|
275
|
+
if (!queue) {
|
|
276
|
+
queue = new EventQueue(normalized, apiKey);
|
|
277
|
+
queuesByHost.set(key, queue);
|
|
278
|
+
}
|
|
279
|
+
installLifecycleListeners();
|
|
280
|
+
return queue;
|
|
281
|
+
}
|
|
282
|
+
function flushEventQueue(host, forceBeacon = false, apiKey) {
|
|
283
|
+
const queue = queuesByHost.get(queueKey(host, apiKey));
|
|
284
|
+
if (!queue) return;
|
|
285
|
+
queue.flush(forceBeacon);
|
|
286
|
+
}
|
|
287
|
+
|
|
127
288
|
// src/utils/api.ts
|
|
128
289
|
var pendingDecisions = /* @__PURE__ */ new Map();
|
|
129
|
-
async function fetchDecision(host, experimentId, distinctId) {
|
|
290
|
+
async function fetchDecision(host, experimentId, distinctId, apiKey) {
|
|
130
291
|
const existing = pendingDecisions.get(experimentId);
|
|
131
292
|
if (existing) return existing;
|
|
132
293
|
const promise = (async () => {
|
|
133
294
|
try {
|
|
134
295
|
const url = `${host.replace(/\/$/, "")}/experiment/decide`;
|
|
296
|
+
const headers = {
|
|
297
|
+
"Content-Type": "application/json",
|
|
298
|
+
Accept: "application/json"
|
|
299
|
+
};
|
|
300
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
135
301
|
const res = await fetch(url, {
|
|
136
302
|
method: "POST",
|
|
137
|
-
headers
|
|
138
|
-
"Content-Type": "application/json",
|
|
139
|
-
Accept: "application/json"
|
|
140
|
-
},
|
|
303
|
+
headers,
|
|
141
304
|
credentials: "include",
|
|
142
305
|
body: JSON.stringify({
|
|
143
306
|
experiment_id: experimentId,
|
|
@@ -154,31 +317,35 @@ async function fetchDecision(host, experimentId, distinctId) {
|
|
|
154
317
|
pendingDecisions.set(experimentId, promise);
|
|
155
318
|
return promise;
|
|
156
319
|
}
|
|
157
|
-
function sendMetric(host, event, properties) {
|
|
320
|
+
function sendMetric(host, event, properties, apiKey) {
|
|
158
321
|
if (typeof window === "undefined") return;
|
|
322
|
+
const environment = detectEnvironment();
|
|
159
323
|
const ctx = buildEventContext();
|
|
160
324
|
const payload = {
|
|
161
325
|
event,
|
|
162
|
-
environment
|
|
326
|
+
environment,
|
|
163
327
|
properties: {
|
|
164
328
|
...ctx,
|
|
329
|
+
environment,
|
|
165
330
|
source: "react-sdk",
|
|
166
331
|
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
167
332
|
...properties
|
|
168
333
|
}
|
|
169
334
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
} catch {
|
|
335
|
+
const queuePayload = {
|
|
336
|
+
event: payload.event,
|
|
337
|
+
environment: payload.environment,
|
|
338
|
+
properties: payload.properties
|
|
339
|
+
};
|
|
340
|
+
const queue = getEventQueue(host, apiKey);
|
|
341
|
+
queue.enqueue(queuePayload);
|
|
342
|
+
if (event === "$experiment_exposure" || event === "$experiment_click") {
|
|
343
|
+
queue.flush(false);
|
|
180
344
|
}
|
|
181
345
|
}
|
|
346
|
+
function flushMetrics(host, forceBeacon = false, apiKey) {
|
|
347
|
+
flushEventQueue(host, forceBeacon, apiKey);
|
|
348
|
+
}
|
|
182
349
|
function extractClickMeta(target) {
|
|
183
350
|
if (!target || !(target instanceof HTMLElement)) return null;
|
|
184
351
|
const primary = target.closest('[data-probat-click="primary"]');
|
|
@@ -198,7 +365,56 @@ function buildMeta(el, isPrimary) {
|
|
|
198
365
|
return meta;
|
|
199
366
|
}
|
|
200
367
|
|
|
201
|
-
// src/
|
|
368
|
+
// src/context/ProbatContext.tsx
|
|
369
|
+
var ProbatContext = createContext(null);
|
|
370
|
+
var DEFAULT_HOST = "https://api.probat.app";
|
|
371
|
+
function ProbatProvider({
|
|
372
|
+
customerId,
|
|
373
|
+
host = DEFAULT_HOST,
|
|
374
|
+
apiKey,
|
|
375
|
+
bootstrap,
|
|
376
|
+
trackSessionLifecycle = true,
|
|
377
|
+
children
|
|
378
|
+
}) {
|
|
379
|
+
const normalizedHost = host.replace(/\/$/, "");
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
if (!trackSessionLifecycle) return;
|
|
382
|
+
if (typeof window === "undefined") return;
|
|
383
|
+
sendMetric(normalizedHost, "$session_start", {
|
|
384
|
+
...customerId ? { distinct_id: customerId } : {}
|
|
385
|
+
}, apiKey);
|
|
386
|
+
return () => {
|
|
387
|
+
sendMetric(normalizedHost, "$session_end", {
|
|
388
|
+
...customerId ? { distinct_id: customerId } : {}
|
|
389
|
+
}, apiKey);
|
|
390
|
+
flushMetrics(normalizedHost, true, apiKey);
|
|
391
|
+
};
|
|
392
|
+
}, [normalizedHost, customerId, trackSessionLifecycle, apiKey]);
|
|
393
|
+
const value = useMemo(
|
|
394
|
+
() => ({
|
|
395
|
+
host: normalizedHost,
|
|
396
|
+
apiKey,
|
|
397
|
+
customerId,
|
|
398
|
+
bootstrap: bootstrap ?? {}
|
|
399
|
+
}),
|
|
400
|
+
[customerId, normalizedHost, apiKey, bootstrap]
|
|
401
|
+
);
|
|
402
|
+
return /* @__PURE__ */ React3.createElement(ProbatContext.Provider, { value }, children);
|
|
403
|
+
}
|
|
404
|
+
function useProbatContext() {
|
|
405
|
+
const ctx = useContext(ProbatContext);
|
|
406
|
+
if (!ctx) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
"useProbatContext must be used within <ProbatProviderClient>. Wrap your app with <ProbatProviderClient>."
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return ctx;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/components/ProbatProviderClient.tsx
|
|
415
|
+
function ProbatProviderClient(props) {
|
|
416
|
+
return React3.createElement(ProbatProvider, props);
|
|
417
|
+
}
|
|
202
418
|
var ASSIGNMENT_PREFIX = "probat:assignment:";
|
|
203
419
|
function readAssignment(id) {
|
|
204
420
|
if (typeof window === "undefined") return null;
|
|
@@ -219,17 +435,33 @@ function writeAssignment(id, variantKey) {
|
|
|
219
435
|
} catch {
|
|
220
436
|
}
|
|
221
437
|
}
|
|
438
|
+
function readForceParam() {
|
|
439
|
+
if (typeof window === "undefined") return null;
|
|
440
|
+
try {
|
|
441
|
+
return new URLSearchParams(window.location.search).get("__probat_force");
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
222
446
|
function useExperiment(id, options = {}) {
|
|
223
447
|
const { fallback = "control", debug = false } = options;
|
|
224
|
-
const { host, bootstrap, customerId } = useProbatContext();
|
|
448
|
+
const { host, bootstrap, customerId, apiKey } = useProbatContext();
|
|
225
449
|
const [variantKey, setVariantKey] = useState(() => {
|
|
450
|
+
const forced = readForceParam();
|
|
451
|
+
if (forced) return forced;
|
|
226
452
|
if (bootstrap[id]) return bootstrap[id];
|
|
227
453
|
return "control";
|
|
228
454
|
});
|
|
229
455
|
const [resolved, setResolved] = useState(() => {
|
|
230
|
-
return !!bootstrap[id];
|
|
456
|
+
return !!(readForceParam() || bootstrap[id]);
|
|
231
457
|
});
|
|
232
458
|
useEffect(() => {
|
|
459
|
+
const forced = readForceParam();
|
|
460
|
+
if (forced) {
|
|
461
|
+
setVariantKey(forced);
|
|
462
|
+
setResolved(true);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
233
465
|
if (bootstrap[id] || readAssignment(id)) {
|
|
234
466
|
const key = bootstrap[id] ?? readAssignment(id) ?? "control";
|
|
235
467
|
setVariantKey(key);
|
|
@@ -240,7 +472,7 @@ function useExperiment(id, options = {}) {
|
|
|
240
472
|
(async () => {
|
|
241
473
|
try {
|
|
242
474
|
const distinctId = customerId ?? getDistinctId();
|
|
243
|
-
const key = await fetchDecision(host, id, distinctId);
|
|
475
|
+
const key = await fetchDecision(host, id, distinctId, apiKey);
|
|
244
476
|
if (cancelled) return;
|
|
245
477
|
setVariantKey(key);
|
|
246
478
|
writeAssignment(id, key);
|
|
@@ -359,6 +591,7 @@ function useTrack(options) {
|
|
|
359
591
|
const {
|
|
360
592
|
experimentId,
|
|
361
593
|
componentInstanceId,
|
|
594
|
+
resolved = true,
|
|
362
595
|
impression: trackImpression = true,
|
|
363
596
|
click: trackClick = true,
|
|
364
597
|
impressionEventName = "$experiment_exposure",
|
|
@@ -367,7 +600,7 @@ function useTrack(options) {
|
|
|
367
600
|
} = options;
|
|
368
601
|
const variantKey = options.variantKey ?? void 0;
|
|
369
602
|
const explicitCustomerId = "customerId" in options ? options.customerId : void 0;
|
|
370
|
-
const { host, customerId: providerCustomerId } = useProbatContext();
|
|
603
|
+
const { host, customerId: providerCustomerId, apiKey } = useProbatContext();
|
|
371
604
|
const resolvedCustomerId = explicitCustomerId ?? providerCustomerId;
|
|
372
605
|
const isCustomerMode = !variantKey;
|
|
373
606
|
const autoInstanceId = useStableInstanceId(experimentId);
|
|
@@ -392,7 +625,7 @@ function useTrack(options) {
|
|
|
392
625
|
);
|
|
393
626
|
const dedupeVariant = variantKey ?? resolvedCustomerId ?? "__anon__";
|
|
394
627
|
useEffect(() => {
|
|
395
|
-
if (!trackImpression) return;
|
|
628
|
+
if (!trackImpression || !resolved) return;
|
|
396
629
|
impressionSent.current = false;
|
|
397
630
|
const pageKey = getPageKey();
|
|
398
631
|
const dedupeKey = makeDedupeKey(experimentId, dedupeVariant, instanceId, pageKey);
|
|
@@ -406,7 +639,7 @@ function useTrack(options) {
|
|
|
406
639
|
if (!impressionSent.current) {
|
|
407
640
|
impressionSent.current = true;
|
|
408
641
|
markSeen(dedupeKey);
|
|
409
|
-
sendMetric(host, impressionEventName, eventProps);
|
|
642
|
+
sendMetric(host, impressionEventName, eventProps, apiKey);
|
|
410
643
|
if (debug) console.log(`[probat] Impression sent (no IO) for "${experimentId}"`);
|
|
411
644
|
}
|
|
412
645
|
return;
|
|
@@ -420,7 +653,7 @@ function useTrack(options) {
|
|
|
420
653
|
if (impressionSent.current) return;
|
|
421
654
|
impressionSent.current = true;
|
|
422
655
|
markSeen(dedupeKey);
|
|
423
|
-
sendMetric(host, impressionEventName, eventProps);
|
|
656
|
+
sendMetric(host, impressionEventName, eventProps, apiKey);
|
|
424
657
|
if (debug) console.log(`[probat] Impression sent for "${experimentId}"`);
|
|
425
658
|
observer.disconnect();
|
|
426
659
|
}, 250);
|
|
@@ -438,6 +671,7 @@ function useTrack(options) {
|
|
|
438
671
|
};
|
|
439
672
|
}, [
|
|
440
673
|
trackImpression,
|
|
674
|
+
resolved,
|
|
441
675
|
experimentId,
|
|
442
676
|
dedupeVariant,
|
|
443
677
|
instanceId,
|
|
@@ -448,18 +682,18 @@ function useTrack(options) {
|
|
|
448
682
|
]);
|
|
449
683
|
const handleClick = useCallback(
|
|
450
684
|
(e) => {
|
|
451
|
-
if (!trackClick) return;
|
|
685
|
+
if (!trackClick || !resolved) return;
|
|
452
686
|
const meta = extractClickMeta(e.target);
|
|
453
687
|
if (!meta) return;
|
|
454
688
|
sendMetric(host, clickEventName, {
|
|
455
689
|
...eventProps,
|
|
456
690
|
...meta
|
|
457
|
-
});
|
|
691
|
+
}, apiKey);
|
|
458
692
|
if (debug) {
|
|
459
693
|
console.log(`[probat] Click tracked for "${experimentId}"`, meta);
|
|
460
694
|
}
|
|
461
695
|
},
|
|
462
|
-
[trackClick, host, clickEventName, eventProps, experimentId, debug]
|
|
696
|
+
[trackClick, resolved, host, clickEventName, eventProps, experimentId, debug]
|
|
463
697
|
);
|
|
464
698
|
useEffect(() => {
|
|
465
699
|
const el = containerRef.current;
|
|
@@ -493,7 +727,8 @@ function Experiment({
|
|
|
493
727
|
experimentId: id,
|
|
494
728
|
variantKey,
|
|
495
729
|
componentInstanceId,
|
|
496
|
-
|
|
730
|
+
resolved,
|
|
731
|
+
impression: track?.impression !== false,
|
|
497
732
|
click: track?.primaryClick !== false,
|
|
498
733
|
impressionEventName: track?.impressionEventName,
|
|
499
734
|
clickEventName: track?.clickEventName,
|
|
@@ -531,17 +766,42 @@ function Track({ children, ...trackOptions }) {
|
|
|
531
766
|
);
|
|
532
767
|
}
|
|
533
768
|
function useProbatMetrics() {
|
|
534
|
-
const { host, customerId } = useProbatContext();
|
|
769
|
+
const { host, customerId, apiKey } = useProbatContext();
|
|
535
770
|
const capture = useCallback(
|
|
536
771
|
(event, properties = {}) => {
|
|
537
772
|
sendMetric(host, event, {
|
|
538
773
|
...customerId ? { distinct_id: customerId } : {},
|
|
539
774
|
...properties
|
|
775
|
+
}, apiKey);
|
|
776
|
+
},
|
|
777
|
+
[host, customerId, apiKey]
|
|
778
|
+
);
|
|
779
|
+
const captureGoal = useCallback(
|
|
780
|
+
(funnelId, funnelStep, properties = {}) => {
|
|
781
|
+
sendMetric(host, "$goal_reached", {
|
|
782
|
+
...customerId ? { distinct_id: customerId } : {},
|
|
783
|
+
$funnel_id: funnelId,
|
|
784
|
+
$funnel_step: funnelStep,
|
|
785
|
+
...properties
|
|
540
786
|
});
|
|
541
787
|
},
|
|
542
788
|
[host, customerId]
|
|
543
789
|
);
|
|
544
|
-
|
|
790
|
+
const captureFeatureInteraction = useCallback(
|
|
791
|
+
(interactionName, properties = {}) => {
|
|
792
|
+
sendMetric(host, "$feature_interaction", {
|
|
793
|
+
...customerId ? { distinct_id: customerId } : {},
|
|
794
|
+
interaction_name: interactionName,
|
|
795
|
+
...properties
|
|
796
|
+
});
|
|
797
|
+
},
|
|
798
|
+
[host, customerId]
|
|
799
|
+
);
|
|
800
|
+
return {
|
|
801
|
+
capture,
|
|
802
|
+
captureGoal,
|
|
803
|
+
captureFeatureInteraction
|
|
804
|
+
};
|
|
545
805
|
}
|
|
546
806
|
function createExperimentContext(experimentId) {
|
|
547
807
|
const Ctx = createContext(null);
|
|
@@ -563,6 +823,6 @@ function createExperimentContext(experimentId) {
|
|
|
563
823
|
return { ExperimentProvider, useVariantKey };
|
|
564
824
|
}
|
|
565
825
|
|
|
566
|
-
export { Experiment, ProbatProviderClient, Track, createExperimentContext, fetchDecision, sendMetric, useExperiment, useProbatMetrics, useTrack };
|
|
826
|
+
export { Experiment, PROBAT_ENV_DEV, PROBAT_ENV_PROD, ProbatProviderClient, Track, createExperimentContext, fetchDecision, flushMetrics, sendMetric, useExperiment, useProbatMetrics, useTrack };
|
|
567
827
|
//# sourceMappingURL=index.mjs.map
|
|
568
828
|
//# sourceMappingURL=index.mjs.map
|