@probat/react 0.1.2 → 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.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  "use client";
3
- import React4, { createContext, useMemo, useEffect, useContext, useCallback, useState } from 'react';
3
+ import React4, { createContext, useMemo, useContext, useCallback, useState, useEffect } from 'react';
4
4
 
5
5
  // src/utils/environment.ts
6
6
  function detectEnvironment() {
@@ -13,6 +13,41 @@ function detectEnvironment() {
13
13
  }
14
14
  return "prod";
15
15
  }
16
+
17
+ // src/context/ProbatContext.tsx
18
+ var ProbatContext = createContext(null);
19
+ function ProbatProvider({
20
+ apiBaseUrl,
21
+ clientKey,
22
+ environment: explicitEnvironment,
23
+ repoFullName: explicitRepoFullName,
24
+ children
25
+ }) {
26
+ const contextValue = useMemo(() => {
27
+ const resolvedApiBaseUrl = apiBaseUrl || typeof import.meta !== "undefined" && import.meta.env?.VITE_PROBAT_API || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_API || typeof window !== "undefined" && window.__PROBAT_API || "https://gushi.onrender.com";
28
+ const environment = explicitEnvironment || detectEnvironment();
29
+ const resolvedRepoFullName = explicitRepoFullName || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_REPO || typeof import.meta !== "undefined" && import.meta.env?.VITE_PROBAT_REPO || typeof window !== "undefined" && window.__PROBAT_REPO || void 0;
30
+ return {
31
+ apiBaseUrl: resolvedApiBaseUrl,
32
+ environment,
33
+ clientKey,
34
+ repoFullName: resolvedRepoFullName
35
+ };
36
+ }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName]);
37
+ return /* @__PURE__ */ React4.createElement(ProbatContext.Provider, { value: contextValue }, children);
38
+ }
39
+ function useProbatContext() {
40
+ const context = useContext(ProbatContext);
41
+ if (!context) {
42
+ throw new Error(
43
+ "useProbatContext must be used within a ProbatProvider. Please wrap your app with <ProbatProvider>."
44
+ );
45
+ }
46
+ return context;
47
+ }
48
+ function ProbatProviderClient(props) {
49
+ return React4.createElement(ProbatProvider, props);
50
+ }
16
51
  var pendingFetches = /* @__PURE__ */ new Map();
17
52
  async function fetchDecision(baseUrl, proposalId) {
18
53
  const existingFetch = pendingFetches.get(proposalId);
@@ -55,7 +90,7 @@ async function sendMetric(baseUrl, proposalId, metricName, variantLabel = "contr
55
90
  captured_at: (/* @__PURE__ */ new Date()).toISOString()
56
91
  };
57
92
  try {
58
- const response = await fetch(url, {
93
+ await fetch(url, {
59
94
  method: "POST",
60
95
  headers: {
61
96
  Accept: "application/json",
@@ -65,26 +100,7 @@ async function sendMetric(baseUrl, proposalId, metricName, variantLabel = "contr
65
100
  // CRITICAL: Include cookies to distinguish different users
66
101
  body: JSON.stringify(body)
67
102
  });
68
- if (!response.ok) {
69
- console.warn("[PROBAT] Metric send failed:", {
70
- status: response.status,
71
- statusText: response.statusText,
72
- url,
73
- body
74
- });
75
- } else {
76
- console.log("[PROBAT] Metric sent successfully:", {
77
- metricName,
78
- proposalId,
79
- variantLabel
80
- });
81
- }
82
- } catch (error) {
83
- console.error("[PROBAT] Error sending metric:", {
84
- error: error instanceof Error ? error.message : String(error),
85
- url,
86
- body
87
- });
103
+ } catch {
88
104
  }
89
105
  }
90
106
  function extractClickMeta(event) {
@@ -145,7 +161,7 @@ if (typeof window !== "undefined") {
145
161
  window.__probatReact = React4;
146
162
  window.React = window.React || React4;
147
163
  }
148
- async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath) {
164
+ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath, repoFullName, baseRef) {
149
165
  if (!filePath) {
150
166
  return null;
151
167
  }
@@ -156,16 +172,151 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
156
172
  }
157
173
  const loadPromise = (async () => {
158
174
  try {
159
- const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
160
- const res = await fetch(variantUrl, {
161
- method: "GET",
162
- headers: { Accept: "text/javascript" },
163
- credentials: "include"
164
- });
165
- if (!res.ok) {
166
- throw new Error(`HTTP ${res.status}`);
175
+ try {
176
+ const variantUrl = `/probat/${filePath}`;
177
+ const mod = await import(
178
+ /* @vite-ignore */
179
+ variantUrl
180
+ );
181
+ const VariantComponent2 = mod?.default || mod;
182
+ if (VariantComponent2 && typeof VariantComponent2 === "function") {
183
+ return VariantComponent2;
184
+ }
185
+ } catch {
186
+ }
187
+ let code = "";
188
+ let rawCode = "";
189
+ let rawCodeFetched = false;
190
+ const localUrl = `/probat/${filePath}`;
191
+ try {
192
+ const localRes = await fetch(localUrl, {
193
+ method: "GET",
194
+ headers: { Accept: "text/plain" }
195
+ });
196
+ if (localRes.ok) {
197
+ rawCode = await localRes.text();
198
+ rawCodeFetched = true;
199
+ console.log(`[PROBAT] \u2705 Loaded variant from local (user's repo): ${localUrl}`);
200
+ }
201
+ } catch {
202
+ console.debug(`[PROBAT] Local file not available (${localUrl}), trying GitHub...`);
203
+ }
204
+ if (!rawCodeFetched && repoFullName) {
205
+ const githubPath = `probat/${filePath}`;
206
+ const gitRef = baseRef || "main";
207
+ const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${gitRef}/${githubPath}`;
208
+ const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
209
+ if (res.ok) {
210
+ rawCode = await res.text();
211
+ rawCodeFetched = true;
212
+ console.log(`[PROBAT] \u26A0\uFE0F Loaded variant from GitHub (fallback): ${githubUrl}`);
213
+ } else {
214
+ console.warn(`[PROBAT] \u26A0\uFE0F GitHub fetch failed (${res.status}), falling back to server compilation`);
215
+ }
216
+ }
217
+ if (rawCodeFetched && rawCode) {
218
+ let Babel;
219
+ if (typeof window !== "undefined" && window.Babel) {
220
+ Babel = window.Babel;
221
+ } else {
222
+ try {
223
+ const babelModule = await import('@babel/standalone');
224
+ Babel = babelModule.default || babelModule;
225
+ } catch (importError) {
226
+ try {
227
+ await new Promise((resolve, reject) => {
228
+ if (typeof document === "undefined") {
229
+ reject(new Error("Document not available"));
230
+ return;
231
+ }
232
+ if (window.Babel) {
233
+ Babel = window.Babel;
234
+ resolve();
235
+ return;
236
+ }
237
+ const script = document.createElement("script");
238
+ script.src = "https://unpkg.com/@babel/standalone/babel.min.js";
239
+ script.async = true;
240
+ script.onload = () => {
241
+ Babel = window.Babel;
242
+ if (!Babel) reject(new Error("Babel not found after script load"));
243
+ else resolve();
244
+ };
245
+ script.onerror = () => reject(new Error("Failed to load Babel from CDN"));
246
+ document.head.appendChild(script);
247
+ });
248
+ } catch (babelError) {
249
+ console.error("[PROBAT] Failed to load Babel, falling back to server compilation", babelError);
250
+ rawCodeFetched = false;
251
+ }
252
+ }
253
+ }
254
+ if (rawCodeFetched && rawCode && Babel) {
255
+ const isTSX = filePath.endsWith(".tsx");
256
+ rawCode = rawCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
257
+ rawCode = rawCode.replace(/^import\s+.*from\s+['"].*\.css['"];?\s*$/gm, "");
258
+ rawCode = rawCode.replace(
259
+ /^import\s+React(?:\s*,\s*\{[^}]*\})?\s+from\s+['"]react['"];?\s*$/m,
260
+ "const React = window.React || globalThis.React;"
261
+ );
262
+ rawCode = rawCode.replace(
263
+ /^import\s+\*\s+as\s+React\s+from\s+['"]react['"];?\s*$/m,
264
+ "const React = window.React || globalThis.React;"
265
+ );
266
+ rawCode = rawCode.replace(
267
+ /^import\s+\{([^}]+)\}\s+from\s+['"]react['"];?\s*$/m,
268
+ (match, imports) => `const {${imports}} = window.React || globalThis.React;`
269
+ );
270
+ rawCode = rawCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
271
+ rawCode = rawCode.replace(/\bimport\.meta\b/g, "({})");
272
+ rawCode = rawCode.replace(/^import\s+.*\/@vite\/client.*$/gm, "");
273
+ rawCode = rawCode.replace(/import\.meta\.hot(?:\.[\w$]+)*/g, "undefined");
274
+ const compiled = Babel.transform(rawCode, {
275
+ presets: [
276
+ ["react", { runtime: "classic" }],
277
+ ["typescript", { allExtensions: true, isTSX }]
278
+ ],
279
+ plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
280
+ sourceType: "module",
281
+ filename: filePath
282
+ }).code;
283
+ code = `
284
+ var __probatVariant = (function() {
285
+ var require = function(name) {
286
+ if (name === "react" || name === "react/jsx-runtime") {
287
+ return window.React || globalThis.React;
288
+ }
289
+ if (name.startsWith("/@vite") || name.includes("@vite/client") || name.includes(".vite/deps")) {
290
+ return {};
291
+ }
292
+ if (name === "react/jsx-runtime.js") {
293
+ return window.React || globalThis.React;
294
+ }
295
+ throw new Error("Unsupported module: " + name);
296
+ };
297
+ var module = { exports: {} };
298
+ var exports = module.exports;
299
+ ${compiled}
300
+ return module.exports.default || module.exports;
301
+ })();
302
+ `;
303
+ } else {
304
+ rawCodeFetched = false;
305
+ code = "";
306
+ }
307
+ }
308
+ if (!rawCodeFetched || code === "") {
309
+ const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
310
+ const serverRes = await fetch(variantUrl, {
311
+ method: "GET",
312
+ headers: { Accept: "text/javascript" },
313
+ credentials: "include"
314
+ });
315
+ if (!serverRes.ok) {
316
+ throw new Error(`HTTP ${serverRes.status}`);
317
+ }
318
+ code = await serverRes.text();
167
319
  }
168
- const code = await res.text();
169
320
  if (typeof window !== "undefined") {
170
321
  window.React = window.React || React4;
171
322
  }
@@ -192,146 +343,7 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
192
343
  return loadPromise;
193
344
  }
194
345
 
195
- // src/utils/documentClickTracker.ts
196
- var proposalCache = /* @__PURE__ */ new Map();
197
- var isListenerAttached = false;
198
- var lastClickTime = /* @__PURE__ */ new Map();
199
- var DEBOUNCE_MS = 100;
200
- function getProposalMetadata(element) {
201
- const probatWrapper = element.closest("[data-probat-proposal]");
202
- if (!probatWrapper) return null;
203
- const proposalId = probatWrapper.getAttribute("data-probat-proposal");
204
- if (!proposalId) return null;
205
- const cacheKey = `${proposalId}`;
206
- const cached = proposalCache.get(cacheKey);
207
- if (cached) return cached;
208
- const experimentId = probatWrapper.getAttribute("data-probat-experiment-id");
209
- const variantLabel = probatWrapper.getAttribute("data-probat-variant-label") || "control";
210
- const apiBaseUrl = probatWrapper.getAttribute("data-probat-api-base-url") || typeof window !== "undefined" && window.__PROBAT_API || "https://gushi.onrender.com";
211
- const metadata = {
212
- proposalId,
213
- experimentId: experimentId || null,
214
- variantLabel,
215
- apiBaseUrl
216
- };
217
- proposalCache.set(cacheKey, metadata);
218
- return metadata;
219
- }
220
- function handleDocumentClick(event) {
221
- const target = event.target;
222
- if (!target) return;
223
- const metadata = getProposalMetadata(target);
224
- if (!metadata) {
225
- return;
226
- }
227
- const now2 = Date.now();
228
- const lastClick = lastClickTime.get(metadata.proposalId) || 0;
229
- if (now2 - lastClick < DEBOUNCE_MS) {
230
- return;
231
- }
232
- lastClickTime.set(metadata.proposalId, now2);
233
- const clickMeta = extractClickMeta(event);
234
- target.hasAttribute("data-probat-track") || target.closest("[data-probat-track]") !== null;
235
- const finalMeta = clickMeta || {
236
- target_tag: target.tagName,
237
- target_class: target.className || "",
238
- target_id: target.id || "",
239
- clicked_inside_probat: true
240
- // Flag to indicate this was tracked via document-level listener
241
- };
242
- const experimentId = metadata.variantLabel === "control" ? void 0 : metadata.experimentId && !metadata.experimentId.startsWith("exp_") ? metadata.experimentId : void 0;
243
- void sendMetric(
244
- metadata.apiBaseUrl,
245
- metadata.proposalId,
246
- "click",
247
- metadata.variantLabel,
248
- experimentId,
249
- finalMeta
250
- );
251
- console.log("[PROBAT] Click tracked:", {
252
- proposalId: metadata.proposalId,
253
- variantLabel: metadata.variantLabel,
254
- target: target.tagName,
255
- targetId: target.id || "none",
256
- targetClass: target.className || "none",
257
- meta: finalMeta
258
- });
259
- console.log("[PROBAT] Sending metric to:", `${metadata.apiBaseUrl}/send_metrics/${metadata.proposalId}`);
260
- }
261
- function initDocumentClickTracking() {
262
- if (isListenerAttached) {
263
- console.warn("[PROBAT] Document click listener already attached");
264
- return;
265
- }
266
- if (typeof document === "undefined") {
267
- return;
268
- }
269
- document.addEventListener("click", handleDocumentClick, true);
270
- isListenerAttached = true;
271
- console.log("[PROBAT] Document-level click tracking initialized");
272
- }
273
- function cleanupDocumentClickTracking() {
274
- if (!isListenerAttached) return;
275
- if (typeof document !== "undefined") {
276
- document.removeEventListener("click", handleDocumentClick, true);
277
- }
278
- isListenerAttached = false;
279
- proposalCache.clear();
280
- lastClickTime.clear();
281
- }
282
- function updateProposalMetadata(proposalId, metadata) {
283
- const existing = proposalCache.get(proposalId);
284
- if (existing) {
285
- proposalCache.set(proposalId, {
286
- ...existing,
287
- ...metadata
288
- });
289
- }
290
- }
291
- function clearProposalCache() {
292
- proposalCache.clear();
293
- }
294
-
295
- // src/context/ProbatContext.tsx
296
- var ProbatContext = createContext(null);
297
- function ProbatProvider({
298
- apiBaseUrl,
299
- clientKey,
300
- environment: explicitEnvironment,
301
- repoFullName: explicitRepoFullName,
302
- children
303
- }) {
304
- const contextValue = useMemo(() => {
305
- const resolvedApiBaseUrl = apiBaseUrl || typeof import.meta !== "undefined" && import.meta.env?.VITE_PROBAT_API || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_API || typeof window !== "undefined" && window.__PROBAT_API || "https://gushi.onrender.com";
306
- const environment = explicitEnvironment || detectEnvironment();
307
- const resolvedRepoFullName = explicitRepoFullName || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_REPO || typeof import.meta !== "undefined" && import.meta.env?.VITE_PROBAT_REPO || typeof window !== "undefined" && window.__PROBAT_REPO || void 0;
308
- return {
309
- apiBaseUrl: resolvedApiBaseUrl,
310
- environment,
311
- clientKey,
312
- repoFullName: resolvedRepoFullName
313
- };
314
- }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName]);
315
- useEffect(() => {
316
- initDocumentClickTracking();
317
- return () => {
318
- cleanupDocumentClickTracking();
319
- };
320
- }, []);
321
- return /* @__PURE__ */ React4.createElement(ProbatContext.Provider, { value: contextValue }, children);
322
- }
323
- function useProbatContext() {
324
- const context = useContext(ProbatContext);
325
- if (!context) {
326
- throw new Error(
327
- "useProbatContext must be used within a ProbatProvider. Please wrap your app with <ProbatProvider>."
328
- );
329
- }
330
- return context;
331
- }
332
- function ProbatProviderClient(props) {
333
- return React4.createElement(ProbatProvider, props);
334
- }
346
+ // src/hooks/useProbatMetrics.ts
335
347
  function useProbatMetrics() {
336
348
  const { apiBaseUrl } = useProbatContext();
337
349
  const trackClick = useCallback(
@@ -595,7 +607,9 @@ function withExperiment(Control, options) {
595
607
  apiBaseUrl,
596
608
  componentConfig.proposal_id,
597
609
  variantInfo.experiment_id,
598
- variantInfo.file_path
610
+ variantInfo.file_path,
611
+ componentConfig.repo_full_name,
612
+ componentConfig.base_ref
599
613
  );
600
614
  if (VariantComp && typeof VariantComp === "function" && alive) {
601
615
  variantComponents[label2] = VariantComp;
@@ -694,28 +708,16 @@ function withExperiment(Control, options) {
694
708
  }
695
709
  const label = choice?.label ?? "control";
696
710
  const Variant = registry[label] || registry.control || ControlComponent;
697
- return /* @__PURE__ */ React4.createElement(
698
- "div",
699
- {
700
- onClick: (event) => {
701
- trackClick(event);
702
- },
703
- "data-probat-proposal": proposalId,
704
- "data-probat-experiment-id": choice?.experiment_id || "",
705
- "data-probat-variant-label": label,
706
- "data-probat-api-base-url": apiBaseUrl
707
- },
708
- React4.createElement(Variant, {
709
- key: `${proposalId}:${label}`,
710
- ...props,
711
- probat: { trackClick: () => trackClick(null, { force: true }) }
712
- })
713
- );
711
+ return /* @__PURE__ */ React4.createElement("div", { onClick: (event) => trackClick(event), "data-probat-proposal": proposalId }, React4.createElement(Variant, {
712
+ key: `${proposalId}:${label}`,
713
+ ...props,
714
+ probat: { trackClick: () => trackClick(null, { force: true }) }
715
+ }));
714
716
  }
715
717
  Wrapped.displayName = `withExperiment(${Control.displayName || Control.name || "Component"})`;
716
718
  return Wrapped;
717
719
  }
718
720
 
719
- export { ProbatProvider, ProbatProviderClient, cleanupDocumentClickTracking, clearProposalCache, detectEnvironment, extractClickMeta, fetchDecision, hasTrackedVisit, initDocumentClickTracking, markTrackedVisit, readChoice, sendMetric, updateProposalMetadata, useExperiment, useProbatContext, useProbatMetrics, withExperiment, writeChoice };
721
+ export { ProbatProvider, ProbatProviderClient, detectEnvironment, extractClickMeta, fetchDecision, hasTrackedVisit, markTrackedVisit, readChoice, sendMetric, useExperiment, useProbatContext, useProbatMetrics, withExperiment, writeChoice };
720
722
  //# sourceMappingURL=index.mjs.map
721
723
  //# sourceMappingURL=index.mjs.map