@probat/react 0.1.0 → 0.1.2

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 CHANGED
@@ -190,4 +190,33 @@ declare function writeChoice(proposalId: string, experiment_id: string, label: s
190
190
  declare function hasTrackedVisit(proposalId: string, label: string): boolean;
191
191
  declare function markTrackedVisit(proposalId: string, label: string): void;
192
192
 
193
- export { type Choice, type ProbatContextValue, ProbatProvider, ProbatProviderClient, type ProbatProviderProps as ProbatProviderClientProps, type ProbatProviderProps, type RetrieveResponse, type UseExperimentReturn, type UseProbatMetricsReturn, type WithExperimentOptions, detectEnvironment, extractClickMeta, fetchDecision, hasTrackedVisit, markTrackedVisit, readChoice, sendMetric, useExperiment, useProbatContext, useProbatMetrics, withExperiment, writeChoice };
193
+ /**
194
+ * Document-level click tracking for Probat experiments
195
+ *
196
+ * This module implements event delegation at the document level to track clicks
197
+ * even when components don't have onClick handlers or when stopPropagation() is called.
198
+ */
199
+ /**
200
+ * Initialize document-level click tracking
201
+ * Call this once when your app initializes (typically in ProbatProvider)
202
+ */
203
+ declare function initDocumentClickTracking(): void;
204
+ /**
205
+ * Clean up the listener (useful for testing or cleanup)
206
+ */
207
+ declare function cleanupDocumentClickTracking(): void;
208
+ /**
209
+ * Update proposal metadata cache (call this when proposal data changes)
210
+ * This is useful if you want to update the cache without waiting for DOM queries
211
+ */
212
+ declare function updateProposalMetadata(proposalId: string, metadata: {
213
+ experimentId?: string | null;
214
+ variantLabel?: string;
215
+ apiBaseUrl?: string;
216
+ }): void;
217
+ /**
218
+ * Clear the proposal cache (useful for testing)
219
+ */
220
+ declare function clearProposalCache(): void;
221
+
222
+ export { type Choice, type ProbatContextValue, ProbatProvider, ProbatProviderClient, type ProbatProviderProps as ProbatProviderClientProps, type ProbatProviderProps, type RetrieveResponse, type UseExperimentReturn, type UseProbatMetricsReturn, type WithExperimentOptions, cleanupDocumentClickTracking, clearProposalCache, detectEnvironment, extractClickMeta, fetchDecision, hasTrackedVisit, initDocumentClickTracking, markTrackedVisit, readChoice, sendMetric, updateProposalMetadata, useExperiment, useProbatContext, useProbatMetrics, withExperiment, writeChoice };
package/dist/index.d.ts CHANGED
@@ -190,4 +190,33 @@ declare function writeChoice(proposalId: string, experiment_id: string, label: s
190
190
  declare function hasTrackedVisit(proposalId: string, label: string): boolean;
191
191
  declare function markTrackedVisit(proposalId: string, label: string): void;
192
192
 
193
- export { type Choice, type ProbatContextValue, ProbatProvider, ProbatProviderClient, type ProbatProviderProps as ProbatProviderClientProps, type ProbatProviderProps, type RetrieveResponse, type UseExperimentReturn, type UseProbatMetricsReturn, type WithExperimentOptions, detectEnvironment, extractClickMeta, fetchDecision, hasTrackedVisit, markTrackedVisit, readChoice, sendMetric, useExperiment, useProbatContext, useProbatMetrics, withExperiment, writeChoice };
193
+ /**
194
+ * Document-level click tracking for Probat experiments
195
+ *
196
+ * This module implements event delegation at the document level to track clicks
197
+ * even when components don't have onClick handlers or when stopPropagation() is called.
198
+ */
199
+ /**
200
+ * Initialize document-level click tracking
201
+ * Call this once when your app initializes (typically in ProbatProvider)
202
+ */
203
+ declare function initDocumentClickTracking(): void;
204
+ /**
205
+ * Clean up the listener (useful for testing or cleanup)
206
+ */
207
+ declare function cleanupDocumentClickTracking(): void;
208
+ /**
209
+ * Update proposal metadata cache (call this when proposal data changes)
210
+ * This is useful if you want to update the cache without waiting for DOM queries
211
+ */
212
+ declare function updateProposalMetadata(proposalId: string, metadata: {
213
+ experimentId?: string | null;
214
+ variantLabel?: string;
215
+ apiBaseUrl?: string;
216
+ }): void;
217
+ /**
218
+ * Clear the proposal cache (useful for testing)
219
+ */
220
+ declare function clearProposalCache(): void;
221
+
222
+ export { type Choice, type ProbatContextValue, ProbatProvider, ProbatProviderClient, type ProbatProviderProps as ProbatProviderClientProps, type ProbatProviderProps, type RetrieveResponse, type UseExperimentReturn, type UseProbatMetricsReturn, type WithExperimentOptions, cleanupDocumentClickTracking, clearProposalCache, detectEnvironment, extractClickMeta, fetchDecision, hasTrackedVisit, initDocumentClickTracking, markTrackedVisit, readChoice, sendMetric, updateProposalMetadata, useExperiment, useProbatContext, useProbatMetrics, withExperiment, writeChoice };
package/dist/index.js CHANGED
@@ -20,41 +20,6 @@ function detectEnvironment() {
20
20
  }
21
21
  return "prod";
22
22
  }
23
-
24
- // src/context/ProbatContext.tsx
25
- var ProbatContext = React4.createContext(null);
26
- function ProbatProvider({
27
- apiBaseUrl,
28
- clientKey,
29
- environment: explicitEnvironment,
30
- repoFullName: explicitRepoFullName,
31
- children
32
- }) {
33
- const contextValue = React4.useMemo(() => {
34
- const resolvedApiBaseUrl = apiBaseUrl || typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href)) }) !== "undefined" && undefined?.VITE_PROBAT_API || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_API || typeof window !== "undefined" && window.__PROBAT_API || "https://gushi.onrender.com";
35
- const environment = explicitEnvironment || detectEnvironment();
36
- const resolvedRepoFullName = explicitRepoFullName || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_REPO || typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href)) }) !== "undefined" && undefined?.VITE_PROBAT_REPO || typeof window !== "undefined" && window.__PROBAT_REPO || void 0;
37
- return {
38
- apiBaseUrl: resolvedApiBaseUrl,
39
- environment,
40
- clientKey,
41
- repoFullName: resolvedRepoFullName
42
- };
43
- }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName]);
44
- return /* @__PURE__ */ React4__default.default.createElement(ProbatContext.Provider, { value: contextValue }, children);
45
- }
46
- function useProbatContext() {
47
- const context = React4.useContext(ProbatContext);
48
- if (!context) {
49
- throw new Error(
50
- "useProbatContext must be used within a ProbatProvider. Please wrap your app with <ProbatProvider>."
51
- );
52
- }
53
- return context;
54
- }
55
- function ProbatProviderClient(props) {
56
- return React4__default.default.createElement(ProbatProvider, props);
57
- }
58
23
  var pendingFetches = /* @__PURE__ */ new Map();
59
24
  async function fetchDecision(baseUrl, proposalId) {
60
25
  const existingFetch = pendingFetches.get(proposalId);
@@ -97,7 +62,7 @@ async function sendMetric(baseUrl, proposalId, metricName, variantLabel = "contr
97
62
  captured_at: (/* @__PURE__ */ new Date()).toISOString()
98
63
  };
99
64
  try {
100
- await fetch(url, {
65
+ const response = await fetch(url, {
101
66
  method: "POST",
102
67
  headers: {
103
68
  Accept: "application/json",
@@ -107,7 +72,26 @@ async function sendMetric(baseUrl, proposalId, metricName, variantLabel = "contr
107
72
  // CRITICAL: Include cookies to distinguish different users
108
73
  body: JSON.stringify(body)
109
74
  });
110
- } catch {
75
+ if (!response.ok) {
76
+ console.warn("[PROBAT] Metric send failed:", {
77
+ status: response.status,
78
+ statusText: response.statusText,
79
+ url,
80
+ body
81
+ });
82
+ } else {
83
+ console.log("[PROBAT] Metric sent successfully:", {
84
+ metricName,
85
+ proposalId,
86
+ variantLabel
87
+ });
88
+ }
89
+ } catch (error) {
90
+ console.error("[PROBAT] Error sending metric:", {
91
+ error: error instanceof Error ? error.message : String(error),
92
+ url,
93
+ body
94
+ });
111
95
  }
112
96
  }
113
97
  function extractClickMeta(event) {
@@ -215,7 +199,146 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
215
199
  return loadPromise;
216
200
  }
217
201
 
218
- // src/hooks/useProbatMetrics.ts
202
+ // src/utils/documentClickTracker.ts
203
+ var proposalCache = /* @__PURE__ */ new Map();
204
+ var isListenerAttached = false;
205
+ var lastClickTime = /* @__PURE__ */ new Map();
206
+ var DEBOUNCE_MS = 100;
207
+ function getProposalMetadata(element) {
208
+ const probatWrapper = element.closest("[data-probat-proposal]");
209
+ if (!probatWrapper) return null;
210
+ const proposalId = probatWrapper.getAttribute("data-probat-proposal");
211
+ if (!proposalId) return null;
212
+ const cacheKey = `${proposalId}`;
213
+ const cached = proposalCache.get(cacheKey);
214
+ if (cached) return cached;
215
+ const experimentId = probatWrapper.getAttribute("data-probat-experiment-id");
216
+ const variantLabel = probatWrapper.getAttribute("data-probat-variant-label") || "control";
217
+ const apiBaseUrl = probatWrapper.getAttribute("data-probat-api-base-url") || typeof window !== "undefined" && window.__PROBAT_API || "https://gushi.onrender.com";
218
+ const metadata = {
219
+ proposalId,
220
+ experimentId: experimentId || null,
221
+ variantLabel,
222
+ apiBaseUrl
223
+ };
224
+ proposalCache.set(cacheKey, metadata);
225
+ return metadata;
226
+ }
227
+ function handleDocumentClick(event) {
228
+ const target = event.target;
229
+ if (!target) return;
230
+ const metadata = getProposalMetadata(target);
231
+ if (!metadata) {
232
+ return;
233
+ }
234
+ const now2 = Date.now();
235
+ const lastClick = lastClickTime.get(metadata.proposalId) || 0;
236
+ if (now2 - lastClick < DEBOUNCE_MS) {
237
+ return;
238
+ }
239
+ lastClickTime.set(metadata.proposalId, now2);
240
+ const clickMeta = extractClickMeta(event);
241
+ target.hasAttribute("data-probat-track") || target.closest("[data-probat-track]") !== null;
242
+ const finalMeta = clickMeta || {
243
+ target_tag: target.tagName,
244
+ target_class: target.className || "",
245
+ target_id: target.id || "",
246
+ clicked_inside_probat: true
247
+ // Flag to indicate this was tracked via document-level listener
248
+ };
249
+ const experimentId = metadata.variantLabel === "control" ? void 0 : metadata.experimentId && !metadata.experimentId.startsWith("exp_") ? metadata.experimentId : void 0;
250
+ void sendMetric(
251
+ metadata.apiBaseUrl,
252
+ metadata.proposalId,
253
+ "click",
254
+ metadata.variantLabel,
255
+ experimentId,
256
+ finalMeta
257
+ );
258
+ console.log("[PROBAT] Click tracked:", {
259
+ proposalId: metadata.proposalId,
260
+ variantLabel: metadata.variantLabel,
261
+ target: target.tagName,
262
+ targetId: target.id || "none",
263
+ targetClass: target.className || "none",
264
+ meta: finalMeta
265
+ });
266
+ console.log("[PROBAT] Sending metric to:", `${metadata.apiBaseUrl}/send_metrics/${metadata.proposalId}`);
267
+ }
268
+ function initDocumentClickTracking() {
269
+ if (isListenerAttached) {
270
+ console.warn("[PROBAT] Document click listener already attached");
271
+ return;
272
+ }
273
+ if (typeof document === "undefined") {
274
+ return;
275
+ }
276
+ document.addEventListener("click", handleDocumentClick, true);
277
+ isListenerAttached = true;
278
+ console.log("[PROBAT] Document-level click tracking initialized");
279
+ }
280
+ function cleanupDocumentClickTracking() {
281
+ if (!isListenerAttached) return;
282
+ if (typeof document !== "undefined") {
283
+ document.removeEventListener("click", handleDocumentClick, true);
284
+ }
285
+ isListenerAttached = false;
286
+ proposalCache.clear();
287
+ lastClickTime.clear();
288
+ }
289
+ function updateProposalMetadata(proposalId, metadata) {
290
+ const existing = proposalCache.get(proposalId);
291
+ if (existing) {
292
+ proposalCache.set(proposalId, {
293
+ ...existing,
294
+ ...metadata
295
+ });
296
+ }
297
+ }
298
+ function clearProposalCache() {
299
+ proposalCache.clear();
300
+ }
301
+
302
+ // src/context/ProbatContext.tsx
303
+ var ProbatContext = React4.createContext(null);
304
+ function ProbatProvider({
305
+ apiBaseUrl,
306
+ clientKey,
307
+ environment: explicitEnvironment,
308
+ repoFullName: explicitRepoFullName,
309
+ children
310
+ }) {
311
+ const contextValue = React4.useMemo(() => {
312
+ const resolvedApiBaseUrl = apiBaseUrl || typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href)) }) !== "undefined" && undefined?.VITE_PROBAT_API || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_API || typeof window !== "undefined" && window.__PROBAT_API || "https://gushi.onrender.com";
313
+ const environment = explicitEnvironment || detectEnvironment();
314
+ const resolvedRepoFullName = explicitRepoFullName || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_REPO || typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href)) }) !== "undefined" && undefined?.VITE_PROBAT_REPO || typeof window !== "undefined" && window.__PROBAT_REPO || void 0;
315
+ return {
316
+ apiBaseUrl: resolvedApiBaseUrl,
317
+ environment,
318
+ clientKey,
319
+ repoFullName: resolvedRepoFullName
320
+ };
321
+ }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName]);
322
+ React4.useEffect(() => {
323
+ initDocumentClickTracking();
324
+ return () => {
325
+ cleanupDocumentClickTracking();
326
+ };
327
+ }, []);
328
+ return /* @__PURE__ */ React4__default.default.createElement(ProbatContext.Provider, { value: contextValue }, children);
329
+ }
330
+ function useProbatContext() {
331
+ const context = React4.useContext(ProbatContext);
332
+ if (!context) {
333
+ throw new Error(
334
+ "useProbatContext must be used within a ProbatProvider. Please wrap your app with <ProbatProvider>."
335
+ );
336
+ }
337
+ return context;
338
+ }
339
+ function ProbatProviderClient(props) {
340
+ return React4__default.default.createElement(ProbatProvider, props);
341
+ }
219
342
  function useProbatMetrics() {
220
343
  const { apiBaseUrl } = useProbatContext();
221
344
  const trackClick = React4.useCallback(
@@ -578,11 +701,23 @@ function withExperiment(Control, options) {
578
701
  }
579
702
  const label = choice?.label ?? "control";
580
703
  const Variant = registry[label] || registry.control || ControlComponent;
581
- return /* @__PURE__ */ React4__default.default.createElement("div", { onClick: (event) => trackClick(event), "data-probat-proposal": proposalId }, React4__default.default.createElement(Variant, {
582
- key: `${proposalId}:${label}`,
583
- ...props,
584
- probat: { trackClick: () => trackClick(null, { force: true }) }
585
- }));
704
+ return /* @__PURE__ */ React4__default.default.createElement(
705
+ "div",
706
+ {
707
+ onClick: (event) => {
708
+ trackClick(event);
709
+ },
710
+ "data-probat-proposal": proposalId,
711
+ "data-probat-experiment-id": choice?.experiment_id || "",
712
+ "data-probat-variant-label": label,
713
+ "data-probat-api-base-url": apiBaseUrl
714
+ },
715
+ React4__default.default.createElement(Variant, {
716
+ key: `${proposalId}:${label}`,
717
+ ...props,
718
+ probat: { trackClick: () => trackClick(null, { force: true }) }
719
+ })
720
+ );
586
721
  }
587
722
  Wrapped.displayName = `withExperiment(${Control.displayName || Control.name || "Component"})`;
588
723
  return Wrapped;
@@ -590,13 +725,17 @@ function withExperiment(Control, options) {
590
725
 
591
726
  exports.ProbatProvider = ProbatProvider;
592
727
  exports.ProbatProviderClient = ProbatProviderClient;
728
+ exports.cleanupDocumentClickTracking = cleanupDocumentClickTracking;
729
+ exports.clearProposalCache = clearProposalCache;
593
730
  exports.detectEnvironment = detectEnvironment;
594
731
  exports.extractClickMeta = extractClickMeta;
595
732
  exports.fetchDecision = fetchDecision;
596
733
  exports.hasTrackedVisit = hasTrackedVisit;
734
+ exports.initDocumentClickTracking = initDocumentClickTracking;
597
735
  exports.markTrackedVisit = markTrackedVisit;
598
736
  exports.readChoice = readChoice;
599
737
  exports.sendMetric = sendMetric;
738
+ exports.updateProposalMetadata = updateProposalMetadata;
600
739
  exports.useExperiment = useExperiment;
601
740
  exports.useProbatContext = useProbatContext;
602
741
  exports.useProbatMetrics = useProbatMetrics;