@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 +1 -30
- package/dist/index.d.ts +1 -30
- package/dist/index.js +328 -193
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +330 -191
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
- package/src/context/ProbatContext.tsx +1 -12
- package/src/hoc/itrt-frontend.code-workspace +3 -0
- package/src/hoc/withExperiment.tsx +4 -12
- package/src/index.ts +0 -6
- package/src/utils/api.ts +347 -42
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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/
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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;
|