@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.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
- var ProbatContext = React3.createContext(null);
12
- var DEFAULT_HOST = "https://gushi.onrender.com";
13
- function ProbatProvider({
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 "prod";
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 "dev";
22
+ return PROBAT_ENV_DEV;
52
23
  }
53
- return "prod";
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
- try {
96
- const stored = sessionStorage.getItem(SESSION_ID_KEY);
97
- if (stored) {
98
- cachedSessionId = stored;
99
- return stored;
100
- }
101
- } catch {
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: detectEnvironment(),
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
- try {
177
- const url = `${host.replace(/\/$/, "")}/experiment/metrics`;
178
- fetch(url, {
179
- method: "POST",
180
- headers: { "Content-Type": "application/json" },
181
- credentials: "include",
182
- body: JSON.stringify(payload)
183
- }).catch(() => {
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/hooks/useExperiment.ts
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
- impression: resolved ? track?.impression !== false : false,
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
- return { capture };
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;