@probat/react 0.1.2 → 0.1.4

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,33 +190,4 @@ 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
- /**
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 };
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 };
package/dist/index.d.ts CHANGED
@@ -190,33 +190,4 @@ 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
- /**
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 };
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 };
package/dist/index.js CHANGED
@@ -20,6 +20,41 @@ 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
+ }
23
58
  var pendingFetches = /* @__PURE__ */ new Map();
24
59
  async function fetchDecision(baseUrl, proposalId) {
25
60
  const existingFetch = pendingFetches.get(proposalId);
@@ -62,7 +97,7 @@ async function sendMetric(baseUrl, proposalId, metricName, variantLabel = "contr
62
97
  captured_at: (/* @__PURE__ */ new Date()).toISOString()
63
98
  };
64
99
  try {
65
- const response = await fetch(url, {
100
+ await fetch(url, {
66
101
  method: "POST",
67
102
  headers: {
68
103
  Accept: "application/json",
@@ -72,26 +107,7 @@ async function sendMetric(baseUrl, proposalId, metricName, variantLabel = "contr
72
107
  // CRITICAL: Include cookies to distinguish different users
73
108
  body: JSON.stringify(body)
74
109
  });
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
- });
110
+ } catch {
95
111
  }
96
112
  }
97
113
  function extractClickMeta(event) {
@@ -148,11 +164,109 @@ async function fetchComponentExperimentConfig(baseUrl, repoFullName, componentPa
148
164
  return fetchPromise;
149
165
  }
150
166
  var variantComponentCache = /* @__PURE__ */ new Map();
167
+ var moduleCache = /* @__PURE__ */ new Map();
151
168
  if (typeof window !== "undefined") {
152
169
  window.__probatReact = React4__default.default;
153
170
  window.React = window.React || React4__default.default;
154
171
  }
155
- async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath) {
172
+ function resolveRelativePath(relativePath, basePath) {
173
+ const baseDir = basePath.substring(0, basePath.lastIndexOf("/"));
174
+ const parts = baseDir.split("/").filter(Boolean);
175
+ const relativeParts = relativePath.split("/").filter(Boolean);
176
+ for (const part of relativeParts) {
177
+ if (part === "..") {
178
+ parts.pop();
179
+ } else if (part !== ".") {
180
+ parts.push(part);
181
+ }
182
+ }
183
+ return "/" + parts.join("/");
184
+ }
185
+ async function loadRelativeModule(absolutePath, baseFilePath, repoFullName, baseRef) {
186
+ const cacheKey = `${baseFilePath}:${absolutePath}`;
187
+ if (moduleCache.has(cacheKey)) {
188
+ return moduleCache.get(cacheKey);
189
+ }
190
+ const extensions = [".jsx", ".tsx", ".js", ".ts"];
191
+ let moduleCode = null;
192
+ let modulePath = null;
193
+ for (const ext of extensions) {
194
+ const testPath = absolutePath + (absolutePath.includes(".") ? "" : ext);
195
+ try {
196
+ const localRes = await fetch(testPath, {
197
+ method: "GET",
198
+ headers: { Accept: "text/plain" }
199
+ });
200
+ if (localRes.ok) {
201
+ moduleCode = await localRes.text();
202
+ modulePath = testPath;
203
+ break;
204
+ }
205
+ } catch {
206
+ }
207
+ if (!moduleCode && repoFullName) {
208
+ try {
209
+ const githubPath = testPath.startsWith("/") ? testPath.substring(1) : testPath;
210
+ const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${baseRef || "main"}/${githubPath}`;
211
+ const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
212
+ if (res.ok) {
213
+ moduleCode = await res.text();
214
+ modulePath = testPath;
215
+ break;
216
+ }
217
+ } catch {
218
+ }
219
+ }
220
+ }
221
+ if (!moduleCode || !modulePath) {
222
+ throw new Error(`Could not resolve module: ${absolutePath} from ${baseFilePath}`);
223
+ }
224
+ let Babel;
225
+ if (typeof window !== "undefined" && window.Babel) {
226
+ Babel = window.Babel;
227
+ } else {
228
+ try {
229
+ const babelModule = await import('@babel/standalone');
230
+ Babel = babelModule.default || babelModule;
231
+ } catch {
232
+ throw new Error("Babel not available for compiling relative import");
233
+ }
234
+ }
235
+ let processedCode = moduleCode;
236
+ processedCode = processedCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
237
+ processedCode = processedCode.replace(/^import\s+React\s+from\s+['"]react['"];?\s*$/m, "const React = window.React || globalThis.React;");
238
+ processedCode = processedCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
239
+ processedCode = processedCode.replace(/\bimport\.meta\b/g, "({})");
240
+ const isTSX = modulePath.endsWith(".tsx");
241
+ const compiled = Babel.transform(processedCode, {
242
+ presets: [
243
+ ["react", { runtime: "classic" }],
244
+ ["typescript", { allExtensions: true, isTSX }]
245
+ ],
246
+ plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
247
+ sourceType: "module",
248
+ filename: modulePath
249
+ }).code;
250
+ const moduleCodeWrapper = `
251
+ var require = function(name) {
252
+ if (name === "react" || name === "react/jsx-runtime") {
253
+ return window.React || globalThis.React;
254
+ }
255
+ if (name.startsWith("/@vite") || name.includes("@vite/client")) {
256
+ return {};
257
+ }
258
+ throw new Error("Unsupported module in relative import: " + name);
259
+ };
260
+ var module = { exports: {} };
261
+ var exports = module.exports;
262
+ ${compiled}
263
+ return module.exports;
264
+ `;
265
+ const moduleExports = new Function(moduleCodeWrapper)();
266
+ moduleCache.set(cacheKey, moduleExports);
267
+ return moduleExports;
268
+ }
269
+ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath, repoFullName, baseRef) {
156
270
  if (!filePath) {
157
271
  return null;
158
272
  }
@@ -163,16 +277,190 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
163
277
  }
164
278
  const loadPromise = (async () => {
165
279
  try {
166
- const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
167
- const res = await fetch(variantUrl, {
168
- method: "GET",
169
- headers: { Accept: "text/javascript" },
170
- credentials: "include"
171
- });
172
- if (!res.ok) {
173
- throw new Error(`HTTP ${res.status}`);
280
+ let code = "";
281
+ let rawCode = "";
282
+ let rawCodeFetched = false;
283
+ const isBrowser = typeof window !== "undefined";
284
+ const isNextJSServer = typeof window === "undefined" || typeof globalThis.process !== "undefined" && globalThis.process.env?.NEXT_RUNTIME === "nodejs";
285
+ if (isBrowser && !isNextJSServer) {
286
+ try {
287
+ const variantUrl = `/probat/${filePath}`;
288
+ const mod = await import(
289
+ /* @vite-ignore */
290
+ variantUrl
291
+ );
292
+ const VariantComponent2 = mod?.default || mod;
293
+ if (VariantComponent2 && typeof VariantComponent2 === "function") {
294
+ console.log(`[PROBAT] \u2705 Loaded variant via dynamic import (CSR): ${variantUrl}`);
295
+ return VariantComponent2;
296
+ }
297
+ } catch (dynamicImportError) {
298
+ console.debug(`[PROBAT] Dynamic import failed, using fetch+babel:`, dynamicImportError);
299
+ }
300
+ }
301
+ const localUrl = `/probat/${filePath}`;
302
+ try {
303
+ const localRes = await fetch(localUrl, {
304
+ method: "GET",
305
+ headers: { Accept: "text/plain" }
306
+ });
307
+ if (localRes.ok) {
308
+ rawCode = await localRes.text();
309
+ rawCodeFetched = true;
310
+ console.log(`[PROBAT] \u2705 Loaded variant from local (user's repo): ${localUrl}`);
311
+ }
312
+ } catch {
313
+ console.debug(`[PROBAT] Local file not available (${localUrl}), trying GitHub...`);
314
+ }
315
+ if (!rawCodeFetched && repoFullName) {
316
+ const githubPath = `probat/${filePath}`;
317
+ const gitRef = baseRef || "main";
318
+ const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${gitRef}/${githubPath}`;
319
+ const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
320
+ if (res.ok) {
321
+ rawCode = await res.text();
322
+ rawCodeFetched = true;
323
+ console.log(`[PROBAT] \u26A0\uFE0F Loaded variant from GitHub (fallback): ${githubUrl}`);
324
+ } else {
325
+ console.warn(`[PROBAT] \u26A0\uFE0F GitHub fetch failed (${res.status}), falling back to server compilation`);
326
+ }
327
+ }
328
+ if (rawCodeFetched && rawCode) {
329
+ let Babel;
330
+ if (typeof window !== "undefined" && window.Babel) {
331
+ Babel = window.Babel;
332
+ } else {
333
+ try {
334
+ const babelModule = await import('@babel/standalone');
335
+ Babel = babelModule.default || babelModule;
336
+ } catch (importError) {
337
+ try {
338
+ await new Promise((resolve, reject) => {
339
+ if (typeof document === "undefined") {
340
+ reject(new Error("Document not available"));
341
+ return;
342
+ }
343
+ if (window.Babel) {
344
+ Babel = window.Babel;
345
+ resolve();
346
+ return;
347
+ }
348
+ const script = document.createElement("script");
349
+ script.src = "https://unpkg.com/@babel/standalone/babel.min.js";
350
+ script.async = true;
351
+ script.onload = () => {
352
+ Babel = window.Babel;
353
+ if (!Babel) reject(new Error("Babel not found after script load"));
354
+ else resolve();
355
+ };
356
+ script.onerror = () => reject(new Error("Failed to load Babel from CDN"));
357
+ document.head.appendChild(script);
358
+ });
359
+ } catch (babelError) {
360
+ console.error("[PROBAT] Failed to load Babel, falling back to server compilation", babelError);
361
+ rawCodeFetched = false;
362
+ }
363
+ }
364
+ }
365
+ if (rawCodeFetched && rawCode && Babel) {
366
+ const isTSX = filePath.endsWith(".tsx");
367
+ rawCode = rawCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
368
+ rawCode = rawCode.replace(/^import\s+.*from\s+['"].*\.css['"];?\s*$/gm, "");
369
+ rawCode = rawCode.replace(
370
+ /^import\s+React(?:\s*,\s*\{[^}]*\})?\s+from\s+['"]react['"];?\s*$/m,
371
+ "const React = window.React || globalThis.React;"
372
+ );
373
+ rawCode = rawCode.replace(
374
+ /^import\s+\*\s+as\s+React\s+from\s+['"]react['"];?\s*$/m,
375
+ "const React = window.React || globalThis.React;"
376
+ );
377
+ rawCode = rawCode.replace(
378
+ /^import\s+\{([^}]+)\}\s+from\s+['"]react['"];?\s*$/m,
379
+ (match, imports) => `const {${imports}} = window.React || globalThis.React;`
380
+ );
381
+ rawCode = rawCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
382
+ rawCode = rawCode.replace(/\bimport\.meta\b/g, "({})");
383
+ rawCode = rawCode.replace(/^import\s+.*\/@vite\/client.*$/gm, "");
384
+ rawCode = rawCode.replace(/import\.meta\.hot(?:\.[\w$]+)*/g, "undefined");
385
+ const relativeImportMap = /* @__PURE__ */ new Map();
386
+ rawCode = rawCode.replace(
387
+ /^import\s+(\w+)\s+from\s+['"](\.\.?\/[^'"]+)['"];?\s*$/gm,
388
+ (match, importName, relativePath) => {
389
+ const baseDir = filePath.substring(0, filePath.lastIndexOf("/"));
390
+ const resolvedPath = resolveRelativePath(relativePath, baseDir);
391
+ const absolutePath = resolvedPath.startsWith("/") ? resolvedPath : "/" + resolvedPath;
392
+ relativeImportMap.set(importName, absolutePath);
393
+ return `import ${importName} from "${absolutePath}";`;
394
+ }
395
+ );
396
+ const compiled = Babel.transform(rawCode, {
397
+ presets: [
398
+ ["react", { runtime: "classic" }],
399
+ ["typescript", { allExtensions: true, isTSX }]
400
+ ],
401
+ plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
402
+ sourceType: "module",
403
+ filename: filePath
404
+ }).code;
405
+ const relativeModules = {};
406
+ if (relativeImportMap.size > 0) {
407
+ for (const [importName, absolutePath] of relativeImportMap.entries()) {
408
+ try {
409
+ const moduleExports = await loadRelativeModule(absolutePath, filePath, repoFullName, baseRef);
410
+ relativeModules[absolutePath] = moduleExports.default || moduleExports;
411
+ } catch (err) {
412
+ console.warn(`[PROBAT] Failed to load relative import ${absolutePath}:`, err);
413
+ relativeModules[absolutePath] = null;
414
+ }
415
+ }
416
+ }
417
+ const relativeModulesJson = JSON.stringify(relativeModules);
418
+ code = `
419
+ var __probatVariant = (function() {
420
+ var relativeModules = ${relativeModulesJson};
421
+ var require = function(name) {
422
+ if (name === "react" || name === "react/jsx-runtime") {
423
+ return window.React || globalThis.React;
424
+ }
425
+ if (name.startsWith("/@vite") || name.includes("@vite/client") || name.includes(".vite/deps")) {
426
+ return {};
427
+ }
428
+ if (name === "react/jsx-runtime.js") {
429
+ return window.React || globalThis.React;
430
+ }
431
+ // Handle relative imports (now converted to absolute paths)
432
+ if (name.startsWith("/") && relativeModules.hasOwnProperty(name)) {
433
+ var mod = relativeModules[name];
434
+ if (mod === null) {
435
+ throw new Error("Failed to load module: " + name);
436
+ }
437
+ return mod;
438
+ }
439
+ throw new Error("Unsupported module: " + name);
440
+ };
441
+ var module = { exports: {} };
442
+ var exports = module.exports;
443
+ ${compiled}
444
+ return module.exports.default || module.exports;
445
+ })();
446
+ `;
447
+ } else {
448
+ rawCodeFetched = false;
449
+ code = "";
450
+ }
451
+ }
452
+ if (!rawCodeFetched || code === "") {
453
+ const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
454
+ const serverRes = await fetch(variantUrl, {
455
+ method: "GET",
456
+ headers: { Accept: "text/javascript" },
457
+ credentials: "include"
458
+ });
459
+ if (!serverRes.ok) {
460
+ throw new Error(`HTTP ${serverRes.status}`);
461
+ }
462
+ code = await serverRes.text();
174
463
  }
175
- const code = await res.text();
176
464
  if (typeof window !== "undefined") {
177
465
  window.React = window.React || React4__default.default;
178
466
  }
@@ -199,146 +487,7 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
199
487
  return loadPromise;
200
488
  }
201
489
 
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
- }
490
+ // src/hooks/useProbatMetrics.ts
342
491
  function useProbatMetrics() {
343
492
  const { apiBaseUrl } = useProbatContext();
344
493
  const trackClick = React4.useCallback(
@@ -602,7 +751,9 @@ function withExperiment(Control, options) {
602
751
  apiBaseUrl,
603
752
  componentConfig.proposal_id,
604
753
  variantInfo.experiment_id,
605
- variantInfo.file_path
754
+ variantInfo.file_path,
755
+ componentConfig.repo_full_name,
756
+ componentConfig.base_ref
606
757
  );
607
758
  if (VariantComp && typeof VariantComp === "function" && alive) {
608
759
  variantComponents[label2] = VariantComp;
@@ -701,23 +852,11 @@ function withExperiment(Control, options) {
701
852
  }
702
853
  const label = choice?.label ?? "control";
703
854
  const Variant = registry[label] || registry.control || ControlComponent;
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
- );
855
+ return /* @__PURE__ */ React4__default.default.createElement("div", { onClick: (event) => trackClick(event), "data-probat-proposal": proposalId }, React4__default.default.createElement(Variant, {
856
+ key: `${proposalId}:${label}`,
857
+ ...props,
858
+ probat: { trackClick: () => trackClick(null, { force: true }) }
859
+ }));
721
860
  }
722
861
  Wrapped.displayName = `withExperiment(${Control.displayName || Control.name || "Component"})`;
723
862
  return Wrapped;
@@ -725,17 +864,13 @@ function withExperiment(Control, options) {
725
864
 
726
865
  exports.ProbatProvider = ProbatProvider;
727
866
  exports.ProbatProviderClient = ProbatProviderClient;
728
- exports.cleanupDocumentClickTracking = cleanupDocumentClickTracking;
729
- exports.clearProposalCache = clearProposalCache;
730
867
  exports.detectEnvironment = detectEnvironment;
731
868
  exports.extractClickMeta = extractClickMeta;
732
869
  exports.fetchDecision = fetchDecision;
733
870
  exports.hasTrackedVisit = hasTrackedVisit;
734
- exports.initDocumentClickTracking = initDocumentClickTracking;
735
871
  exports.markTrackedVisit = markTrackedVisit;
736
872
  exports.readChoice = readChoice;
737
873
  exports.sendMetric = sendMetric;
738
- exports.updateProposalMetadata = updateProposalMetadata;
739
874
  exports.useExperiment = useExperiment;
740
875
  exports.useProbatContext = useProbatContext;
741
876
  exports.useProbatMetrics = useProbatMetrics;