@probat/react 0.1.1 → 0.1.3

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);
@@ -133,7 +168,7 @@ if (typeof window !== "undefined") {
133
168
  window.__probatReact = React4__default.default;
134
169
  window.React = window.React || React4__default.default;
135
170
  }
136
- async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath) {
171
+ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath, repoFullName, baseRef) {
137
172
  if (!filePath) {
138
173
  return null;
139
174
  }
@@ -144,16 +179,151 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
144
179
  }
145
180
  const loadPromise = (async () => {
146
181
  try {
147
- const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
148
- const res = await fetch(variantUrl, {
149
- method: "GET",
150
- headers: { Accept: "text/javascript" },
151
- credentials: "include"
152
- });
153
- if (!res.ok) {
154
- throw new Error(`HTTP ${res.status}`);
182
+ try {
183
+ const variantUrl = `/probat/${filePath}`;
184
+ const mod = await import(
185
+ /* @vite-ignore */
186
+ variantUrl
187
+ );
188
+ const VariantComponent2 = mod?.default || mod;
189
+ if (VariantComponent2 && typeof VariantComponent2 === "function") {
190
+ return VariantComponent2;
191
+ }
192
+ } catch {
193
+ }
194
+ let code = "";
195
+ let rawCode = "";
196
+ let rawCodeFetched = false;
197
+ const localUrl = `/probat/${filePath}`;
198
+ try {
199
+ const localRes = await fetch(localUrl, {
200
+ method: "GET",
201
+ headers: { Accept: "text/plain" }
202
+ });
203
+ if (localRes.ok) {
204
+ rawCode = await localRes.text();
205
+ rawCodeFetched = true;
206
+ console.log(`[PROBAT] \u2705 Loaded variant from local (user's repo): ${localUrl}`);
207
+ }
208
+ } catch {
209
+ console.debug(`[PROBAT] Local file not available (${localUrl}), trying GitHub...`);
210
+ }
211
+ if (!rawCodeFetched && repoFullName) {
212
+ const githubPath = `probat/${filePath}`;
213
+ const gitRef = baseRef || "main";
214
+ const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${gitRef}/${githubPath}`;
215
+ const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
216
+ if (res.ok) {
217
+ rawCode = await res.text();
218
+ rawCodeFetched = true;
219
+ console.log(`[PROBAT] \u26A0\uFE0F Loaded variant from GitHub (fallback): ${githubUrl}`);
220
+ } else {
221
+ console.warn(`[PROBAT] \u26A0\uFE0F GitHub fetch failed (${res.status}), falling back to server compilation`);
222
+ }
223
+ }
224
+ if (rawCodeFetched && rawCode) {
225
+ let Babel;
226
+ if (typeof window !== "undefined" && window.Babel) {
227
+ Babel = window.Babel;
228
+ } else {
229
+ try {
230
+ const babelModule = await import('@babel/standalone');
231
+ Babel = babelModule.default || babelModule;
232
+ } catch (importError) {
233
+ try {
234
+ await new Promise((resolve, reject) => {
235
+ if (typeof document === "undefined") {
236
+ reject(new Error("Document not available"));
237
+ return;
238
+ }
239
+ if (window.Babel) {
240
+ Babel = window.Babel;
241
+ resolve();
242
+ return;
243
+ }
244
+ const script = document.createElement("script");
245
+ script.src = "https://unpkg.com/@babel/standalone/babel.min.js";
246
+ script.async = true;
247
+ script.onload = () => {
248
+ Babel = window.Babel;
249
+ if (!Babel) reject(new Error("Babel not found after script load"));
250
+ else resolve();
251
+ };
252
+ script.onerror = () => reject(new Error("Failed to load Babel from CDN"));
253
+ document.head.appendChild(script);
254
+ });
255
+ } catch (babelError) {
256
+ console.error("[PROBAT] Failed to load Babel, falling back to server compilation", babelError);
257
+ rawCodeFetched = false;
258
+ }
259
+ }
260
+ }
261
+ if (rawCodeFetched && rawCode && Babel) {
262
+ const isTSX = filePath.endsWith(".tsx");
263
+ rawCode = rawCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
264
+ rawCode = rawCode.replace(/^import\s+.*from\s+['"].*\.css['"];?\s*$/gm, "");
265
+ rawCode = rawCode.replace(
266
+ /^import\s+React(?:\s*,\s*\{[^}]*\})?\s+from\s+['"]react['"];?\s*$/m,
267
+ "const React = window.React || globalThis.React;"
268
+ );
269
+ rawCode = rawCode.replace(
270
+ /^import\s+\*\s+as\s+React\s+from\s+['"]react['"];?\s*$/m,
271
+ "const React = window.React || globalThis.React;"
272
+ );
273
+ rawCode = rawCode.replace(
274
+ /^import\s+\{([^}]+)\}\s+from\s+['"]react['"];?\s*$/m,
275
+ (match, imports) => `const {${imports}} = window.React || globalThis.React;`
276
+ );
277
+ rawCode = rawCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
278
+ rawCode = rawCode.replace(/\bimport\.meta\b/g, "({})");
279
+ rawCode = rawCode.replace(/^import\s+.*\/@vite\/client.*$/gm, "");
280
+ rawCode = rawCode.replace(/import\.meta\.hot(?:\.[\w$]+)*/g, "undefined");
281
+ const compiled = Babel.transform(rawCode, {
282
+ presets: [
283
+ ["react", { runtime: "classic" }],
284
+ ["typescript", { allExtensions: true, isTSX }]
285
+ ],
286
+ plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
287
+ sourceType: "module",
288
+ filename: filePath
289
+ }).code;
290
+ code = `
291
+ var __probatVariant = (function() {
292
+ var require = function(name) {
293
+ if (name === "react" || name === "react/jsx-runtime") {
294
+ return window.React || globalThis.React;
295
+ }
296
+ if (name.startsWith("/@vite") || name.includes("@vite/client") || name.includes(".vite/deps")) {
297
+ return {};
298
+ }
299
+ if (name === "react/jsx-runtime.js") {
300
+ return window.React || globalThis.React;
301
+ }
302
+ throw new Error("Unsupported module: " + name);
303
+ };
304
+ var module = { exports: {} };
305
+ var exports = module.exports;
306
+ ${compiled}
307
+ return module.exports.default || module.exports;
308
+ })();
309
+ `;
310
+ } else {
311
+ rawCodeFetched = false;
312
+ code = "";
313
+ }
314
+ }
315
+ if (!rawCodeFetched || code === "") {
316
+ const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
317
+ const serverRes = await fetch(variantUrl, {
318
+ method: "GET",
319
+ headers: { Accept: "text/javascript" },
320
+ credentials: "include"
321
+ });
322
+ if (!serverRes.ok) {
323
+ throw new Error(`HTTP ${serverRes.status}`);
324
+ }
325
+ code = await serverRes.text();
155
326
  }
156
- const code = await res.text();
157
327
  if (typeof window !== "undefined") {
158
328
  window.React = window.React || React4__default.default;
159
329
  }
@@ -180,131 +350,7 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
180
350
  return loadPromise;
181
351
  }
182
352
 
183
- // src/utils/documentClickTracker.ts
184
- var proposalCache = /* @__PURE__ */ new Map();
185
- var isListenerAttached = false;
186
- var lastClickTime = /* @__PURE__ */ new Map();
187
- var DEBOUNCE_MS = 100;
188
- function getProposalMetadata(element) {
189
- const probatWrapper = element.closest("[data-probat-proposal]");
190
- if (!probatWrapper) return null;
191
- const proposalId = probatWrapper.getAttribute("data-probat-proposal");
192
- if (!proposalId) return null;
193
- const cacheKey = `${proposalId}`;
194
- const cached = proposalCache.get(cacheKey);
195
- if (cached) return cached;
196
- const experimentId = probatWrapper.getAttribute("data-probat-experiment-id");
197
- const variantLabel = probatWrapper.getAttribute("data-probat-variant-label") || "control";
198
- const apiBaseUrl = probatWrapper.getAttribute("data-probat-api-base-url") || typeof window !== "undefined" && window.__PROBAT_API || "https://gushi.onrender.com";
199
- const metadata = {
200
- proposalId,
201
- experimentId: experimentId || null,
202
- variantLabel,
203
- apiBaseUrl
204
- };
205
- proposalCache.set(cacheKey, metadata);
206
- return metadata;
207
- }
208
- function handleDocumentClick(event) {
209
- const target = event.target;
210
- if (!target) return;
211
- const metadata = getProposalMetadata(target);
212
- if (!metadata) return;
213
- const now2 = Date.now();
214
- const lastClick = lastClickTime.get(metadata.proposalId) || 0;
215
- if (now2 - lastClick < DEBOUNCE_MS) {
216
- return;
217
- }
218
- lastClickTime.set(metadata.proposalId, now2);
219
- const clickMeta = extractClickMeta(event);
220
- const hasTrackAttribute = target.hasAttribute("data-probat-track") || target.closest("[data-probat-track]") !== null;
221
- const shouldTrack = clickMeta !== void 0 || hasTrackAttribute;
222
- if (!shouldTrack) {
223
- return;
224
- }
225
- void sendMetric(
226
- metadata.apiBaseUrl,
227
- metadata.proposalId,
228
- "click",
229
- metadata.variantLabel,
230
- metadata.experimentId || void 0,
231
- clickMeta
232
- );
233
- }
234
- function initDocumentClickTracking() {
235
- if (isListenerAttached) {
236
- console.warn("[PROBAT] Document click listener already attached");
237
- return;
238
- }
239
- if (typeof document === "undefined") {
240
- return;
241
- }
242
- document.addEventListener("click", handleDocumentClick, true);
243
- isListenerAttached = true;
244
- console.log("[PROBAT] Document-level click tracking initialized");
245
- }
246
- function cleanupDocumentClickTracking() {
247
- if (!isListenerAttached) return;
248
- if (typeof document !== "undefined") {
249
- document.removeEventListener("click", handleDocumentClick, true);
250
- }
251
- isListenerAttached = false;
252
- proposalCache.clear();
253
- lastClickTime.clear();
254
- }
255
- function updateProposalMetadata(proposalId, metadata) {
256
- const existing = proposalCache.get(proposalId);
257
- if (existing) {
258
- proposalCache.set(proposalId, {
259
- ...existing,
260
- ...metadata
261
- });
262
- }
263
- }
264
- function clearProposalCache() {
265
- proposalCache.clear();
266
- }
267
-
268
- // src/context/ProbatContext.tsx
269
- var ProbatContext = React4.createContext(null);
270
- function ProbatProvider({
271
- apiBaseUrl,
272
- clientKey,
273
- environment: explicitEnvironment,
274
- repoFullName: explicitRepoFullName,
275
- children
276
- }) {
277
- const contextValue = React4.useMemo(() => {
278
- 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";
279
- const environment = explicitEnvironment || detectEnvironment();
280
- 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;
281
- return {
282
- apiBaseUrl: resolvedApiBaseUrl,
283
- environment,
284
- clientKey,
285
- repoFullName: resolvedRepoFullName
286
- };
287
- }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName]);
288
- React4.useEffect(() => {
289
- initDocumentClickTracking();
290
- return () => {
291
- cleanupDocumentClickTracking();
292
- };
293
- }, []);
294
- return /* @__PURE__ */ React4__default.default.createElement(ProbatContext.Provider, { value: contextValue }, children);
295
- }
296
- function useProbatContext() {
297
- const context = React4.useContext(ProbatContext);
298
- if (!context) {
299
- throw new Error(
300
- "useProbatContext must be used within a ProbatProvider. Please wrap your app with <ProbatProvider>."
301
- );
302
- }
303
- return context;
304
- }
305
- function ProbatProviderClient(props) {
306
- return React4__default.default.createElement(ProbatProvider, props);
307
- }
353
+ // src/hooks/useProbatMetrics.ts
308
354
  function useProbatMetrics() {
309
355
  const { apiBaseUrl } = useProbatContext();
310
356
  const trackClick = React4.useCallback(
@@ -568,7 +614,9 @@ function withExperiment(Control, options) {
568
614
  apiBaseUrl,
569
615
  componentConfig.proposal_id,
570
616
  variantInfo.experiment_id,
571
- variantInfo.file_path
617
+ variantInfo.file_path,
618
+ componentConfig.repo_full_name,
619
+ componentConfig.base_ref
572
620
  );
573
621
  if (VariantComp && typeof VariantComp === "function" && alive) {
574
622
  variantComponents[label2] = VariantComp;
@@ -667,23 +715,11 @@ function withExperiment(Control, options) {
667
715
  }
668
716
  const label = choice?.label ?? "control";
669
717
  const Variant = registry[label] || registry.control || ControlComponent;
670
- return /* @__PURE__ */ React4__default.default.createElement(
671
- "div",
672
- {
673
- onClick: (event) => {
674
- trackClick(event);
675
- },
676
- "data-probat-proposal": proposalId,
677
- "data-probat-experiment-id": choice?.experiment_id || "",
678
- "data-probat-variant-label": label,
679
- "data-probat-api-base-url": apiBaseUrl
680
- },
681
- React4__default.default.createElement(Variant, {
682
- key: `${proposalId}:${label}`,
683
- ...props,
684
- probat: { trackClick: () => trackClick(null, { force: true }) }
685
- })
686
- );
718
+ return /* @__PURE__ */ React4__default.default.createElement("div", { onClick: (event) => trackClick(event), "data-probat-proposal": proposalId }, React4__default.default.createElement(Variant, {
719
+ key: `${proposalId}:${label}`,
720
+ ...props,
721
+ probat: { trackClick: () => trackClick(null, { force: true }) }
722
+ }));
687
723
  }
688
724
  Wrapped.displayName = `withExperiment(${Control.displayName || Control.name || "Component"})`;
689
725
  return Wrapped;
@@ -691,17 +727,13 @@ function withExperiment(Control, options) {
691
727
 
692
728
  exports.ProbatProvider = ProbatProvider;
693
729
  exports.ProbatProviderClient = ProbatProviderClient;
694
- exports.cleanupDocumentClickTracking = cleanupDocumentClickTracking;
695
- exports.clearProposalCache = clearProposalCache;
696
730
  exports.detectEnvironment = detectEnvironment;
697
731
  exports.extractClickMeta = extractClickMeta;
698
732
  exports.fetchDecision = fetchDecision;
699
733
  exports.hasTrackedVisit = hasTrackedVisit;
700
- exports.initDocumentClickTracking = initDocumentClickTracking;
701
734
  exports.markTrackedVisit = markTrackedVisit;
702
735
  exports.readChoice = readChoice;
703
736
  exports.sendMetric = sendMetric;
704
- exports.updateProposalMetadata = updateProposalMetadata;
705
737
  exports.useExperiment = useExperiment;
706
738
  exports.useProbatContext = useProbatContext;
707
739
  exports.useProbatMetrics = useProbatMetrics;