@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.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);
@@ -126,7 +161,7 @@ if (typeof window !== "undefined") {
126
161
  window.__probatReact = React4;
127
162
  window.React = window.React || React4;
128
163
  }
129
- async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath) {
164
+ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath, repoFullName, baseRef) {
130
165
  if (!filePath) {
131
166
  return null;
132
167
  }
@@ -137,16 +172,151 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
137
172
  }
138
173
  const loadPromise = (async () => {
139
174
  try {
140
- const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
141
- const res = await fetch(variantUrl, {
142
- method: "GET",
143
- headers: { Accept: "text/javascript" },
144
- credentials: "include"
145
- });
146
- if (!res.ok) {
147
- 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();
148
319
  }
149
- const code = await res.text();
150
320
  if (typeof window !== "undefined") {
151
321
  window.React = window.React || React4;
152
322
  }
@@ -173,131 +343,7 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
173
343
  return loadPromise;
174
344
  }
175
345
 
176
- // src/utils/documentClickTracker.ts
177
- var proposalCache = /* @__PURE__ */ new Map();
178
- var isListenerAttached = false;
179
- var lastClickTime = /* @__PURE__ */ new Map();
180
- var DEBOUNCE_MS = 100;
181
- function getProposalMetadata(element) {
182
- const probatWrapper = element.closest("[data-probat-proposal]");
183
- if (!probatWrapper) return null;
184
- const proposalId = probatWrapper.getAttribute("data-probat-proposal");
185
- if (!proposalId) return null;
186
- const cacheKey = `${proposalId}`;
187
- const cached = proposalCache.get(cacheKey);
188
- if (cached) return cached;
189
- const experimentId = probatWrapper.getAttribute("data-probat-experiment-id");
190
- const variantLabel = probatWrapper.getAttribute("data-probat-variant-label") || "control";
191
- const apiBaseUrl = probatWrapper.getAttribute("data-probat-api-base-url") || typeof window !== "undefined" && window.__PROBAT_API || "https://gushi.onrender.com";
192
- const metadata = {
193
- proposalId,
194
- experimentId: experimentId || null,
195
- variantLabel,
196
- apiBaseUrl
197
- };
198
- proposalCache.set(cacheKey, metadata);
199
- return metadata;
200
- }
201
- function handleDocumentClick(event) {
202
- const target = event.target;
203
- if (!target) return;
204
- const metadata = getProposalMetadata(target);
205
- if (!metadata) return;
206
- const now2 = Date.now();
207
- const lastClick = lastClickTime.get(metadata.proposalId) || 0;
208
- if (now2 - lastClick < DEBOUNCE_MS) {
209
- return;
210
- }
211
- lastClickTime.set(metadata.proposalId, now2);
212
- const clickMeta = extractClickMeta(event);
213
- const hasTrackAttribute = target.hasAttribute("data-probat-track") || target.closest("[data-probat-track]") !== null;
214
- const shouldTrack = clickMeta !== void 0 || hasTrackAttribute;
215
- if (!shouldTrack) {
216
- return;
217
- }
218
- void sendMetric(
219
- metadata.apiBaseUrl,
220
- metadata.proposalId,
221
- "click",
222
- metadata.variantLabel,
223
- metadata.experimentId || void 0,
224
- clickMeta
225
- );
226
- }
227
- function initDocumentClickTracking() {
228
- if (isListenerAttached) {
229
- console.warn("[PROBAT] Document click listener already attached");
230
- return;
231
- }
232
- if (typeof document === "undefined") {
233
- return;
234
- }
235
- document.addEventListener("click", handleDocumentClick, true);
236
- isListenerAttached = true;
237
- console.log("[PROBAT] Document-level click tracking initialized");
238
- }
239
- function cleanupDocumentClickTracking() {
240
- if (!isListenerAttached) return;
241
- if (typeof document !== "undefined") {
242
- document.removeEventListener("click", handleDocumentClick, true);
243
- }
244
- isListenerAttached = false;
245
- proposalCache.clear();
246
- lastClickTime.clear();
247
- }
248
- function updateProposalMetadata(proposalId, metadata) {
249
- const existing = proposalCache.get(proposalId);
250
- if (existing) {
251
- proposalCache.set(proposalId, {
252
- ...existing,
253
- ...metadata
254
- });
255
- }
256
- }
257
- function clearProposalCache() {
258
- proposalCache.clear();
259
- }
260
-
261
- // src/context/ProbatContext.tsx
262
- var ProbatContext = createContext(null);
263
- function ProbatProvider({
264
- apiBaseUrl,
265
- clientKey,
266
- environment: explicitEnvironment,
267
- repoFullName: explicitRepoFullName,
268
- children
269
- }) {
270
- const contextValue = useMemo(() => {
271
- 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";
272
- const environment = explicitEnvironment || detectEnvironment();
273
- 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;
274
- return {
275
- apiBaseUrl: resolvedApiBaseUrl,
276
- environment,
277
- clientKey,
278
- repoFullName: resolvedRepoFullName
279
- };
280
- }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName]);
281
- useEffect(() => {
282
- initDocumentClickTracking();
283
- return () => {
284
- cleanupDocumentClickTracking();
285
- };
286
- }, []);
287
- return /* @__PURE__ */ React4.createElement(ProbatContext.Provider, { value: contextValue }, children);
288
- }
289
- function useProbatContext() {
290
- const context = useContext(ProbatContext);
291
- if (!context) {
292
- throw new Error(
293
- "useProbatContext must be used within a ProbatProvider. Please wrap your app with <ProbatProvider>."
294
- );
295
- }
296
- return context;
297
- }
298
- function ProbatProviderClient(props) {
299
- return React4.createElement(ProbatProvider, props);
300
- }
346
+ // src/hooks/useProbatMetrics.ts
301
347
  function useProbatMetrics() {
302
348
  const { apiBaseUrl } = useProbatContext();
303
349
  const trackClick = useCallback(
@@ -561,7 +607,9 @@ function withExperiment(Control, options) {
561
607
  apiBaseUrl,
562
608
  componentConfig.proposal_id,
563
609
  variantInfo.experiment_id,
564
- variantInfo.file_path
610
+ variantInfo.file_path,
611
+ componentConfig.repo_full_name,
612
+ componentConfig.base_ref
565
613
  );
566
614
  if (VariantComp && typeof VariantComp === "function" && alive) {
567
615
  variantComponents[label2] = VariantComp;
@@ -660,28 +708,16 @@ function withExperiment(Control, options) {
660
708
  }
661
709
  const label = choice?.label ?? "control";
662
710
  const Variant = registry[label] || registry.control || ControlComponent;
663
- return /* @__PURE__ */ React4.createElement(
664
- "div",
665
- {
666
- onClick: (event) => {
667
- trackClick(event);
668
- },
669
- "data-probat-proposal": proposalId,
670
- "data-probat-experiment-id": choice?.experiment_id || "",
671
- "data-probat-variant-label": label,
672
- "data-probat-api-base-url": apiBaseUrl
673
- },
674
- React4.createElement(Variant, {
675
- key: `${proposalId}:${label}`,
676
- ...props,
677
- probat: { trackClick: () => trackClick(null, { force: true }) }
678
- })
679
- );
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
+ }));
680
716
  }
681
717
  Wrapped.displayName = `withExperiment(${Control.displayName || Control.name || "Component"})`;
682
718
  return Wrapped;
683
719
  }
684
720
 
685
- 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 };
686
722
  //# sourceMappingURL=index.mjs.map
687
723
  //# sourceMappingURL=index.mjs.map