@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.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use client";
|
|
3
|
-
import React4, { createContext, useMemo,
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -141,11 +157,109 @@ async function fetchComponentExperimentConfig(baseUrl, repoFullName, componentPa
|
|
|
141
157
|
return fetchPromise;
|
|
142
158
|
}
|
|
143
159
|
var variantComponentCache = /* @__PURE__ */ new Map();
|
|
160
|
+
var moduleCache = /* @__PURE__ */ new Map();
|
|
144
161
|
if (typeof window !== "undefined") {
|
|
145
162
|
window.__probatReact = React4;
|
|
146
163
|
window.React = window.React || React4;
|
|
147
164
|
}
|
|
148
|
-
|
|
165
|
+
function resolveRelativePath(relativePath, basePath) {
|
|
166
|
+
const baseDir = basePath.substring(0, basePath.lastIndexOf("/"));
|
|
167
|
+
const parts = baseDir.split("/").filter(Boolean);
|
|
168
|
+
const relativeParts = relativePath.split("/").filter(Boolean);
|
|
169
|
+
for (const part of relativeParts) {
|
|
170
|
+
if (part === "..") {
|
|
171
|
+
parts.pop();
|
|
172
|
+
} else if (part !== ".") {
|
|
173
|
+
parts.push(part);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return "/" + parts.join("/");
|
|
177
|
+
}
|
|
178
|
+
async function loadRelativeModule(absolutePath, baseFilePath, repoFullName, baseRef) {
|
|
179
|
+
const cacheKey = `${baseFilePath}:${absolutePath}`;
|
|
180
|
+
if (moduleCache.has(cacheKey)) {
|
|
181
|
+
return moduleCache.get(cacheKey);
|
|
182
|
+
}
|
|
183
|
+
const extensions = [".jsx", ".tsx", ".js", ".ts"];
|
|
184
|
+
let moduleCode = null;
|
|
185
|
+
let modulePath = null;
|
|
186
|
+
for (const ext of extensions) {
|
|
187
|
+
const testPath = absolutePath + (absolutePath.includes(".") ? "" : ext);
|
|
188
|
+
try {
|
|
189
|
+
const localRes = await fetch(testPath, {
|
|
190
|
+
method: "GET",
|
|
191
|
+
headers: { Accept: "text/plain" }
|
|
192
|
+
});
|
|
193
|
+
if (localRes.ok) {
|
|
194
|
+
moduleCode = await localRes.text();
|
|
195
|
+
modulePath = testPath;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
if (!moduleCode && repoFullName) {
|
|
201
|
+
try {
|
|
202
|
+
const githubPath = testPath.startsWith("/") ? testPath.substring(1) : testPath;
|
|
203
|
+
const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${baseRef || "main"}/${githubPath}`;
|
|
204
|
+
const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
|
|
205
|
+
if (res.ok) {
|
|
206
|
+
moduleCode = await res.text();
|
|
207
|
+
modulePath = testPath;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!moduleCode || !modulePath) {
|
|
215
|
+
throw new Error(`Could not resolve module: ${absolutePath} from ${baseFilePath}`);
|
|
216
|
+
}
|
|
217
|
+
let Babel;
|
|
218
|
+
if (typeof window !== "undefined" && window.Babel) {
|
|
219
|
+
Babel = window.Babel;
|
|
220
|
+
} else {
|
|
221
|
+
try {
|
|
222
|
+
const babelModule = await import('@babel/standalone');
|
|
223
|
+
Babel = babelModule.default || babelModule;
|
|
224
|
+
} catch {
|
|
225
|
+
throw new Error("Babel not available for compiling relative import");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let processedCode = moduleCode;
|
|
229
|
+
processedCode = processedCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
|
|
230
|
+
processedCode = processedCode.replace(/^import\s+React\s+from\s+['"]react['"];?\s*$/m, "const React = window.React || globalThis.React;");
|
|
231
|
+
processedCode = processedCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
|
|
232
|
+
processedCode = processedCode.replace(/\bimport\.meta\b/g, "({})");
|
|
233
|
+
const isTSX = modulePath.endsWith(".tsx");
|
|
234
|
+
const compiled = Babel.transform(processedCode, {
|
|
235
|
+
presets: [
|
|
236
|
+
["react", { runtime: "classic" }],
|
|
237
|
+
["typescript", { allExtensions: true, isTSX }]
|
|
238
|
+
],
|
|
239
|
+
plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
|
|
240
|
+
sourceType: "module",
|
|
241
|
+
filename: modulePath
|
|
242
|
+
}).code;
|
|
243
|
+
const moduleCodeWrapper = `
|
|
244
|
+
var require = function(name) {
|
|
245
|
+
if (name === "react" || name === "react/jsx-runtime") {
|
|
246
|
+
return window.React || globalThis.React;
|
|
247
|
+
}
|
|
248
|
+
if (name.startsWith("/@vite") || name.includes("@vite/client")) {
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
throw new Error("Unsupported module in relative import: " + name);
|
|
252
|
+
};
|
|
253
|
+
var module = { exports: {} };
|
|
254
|
+
var exports = module.exports;
|
|
255
|
+
${compiled}
|
|
256
|
+
return module.exports;
|
|
257
|
+
`;
|
|
258
|
+
const moduleExports = new Function(moduleCodeWrapper)();
|
|
259
|
+
moduleCache.set(cacheKey, moduleExports);
|
|
260
|
+
return moduleExports;
|
|
261
|
+
}
|
|
262
|
+
async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath, repoFullName, baseRef) {
|
|
149
263
|
if (!filePath) {
|
|
150
264
|
return null;
|
|
151
265
|
}
|
|
@@ -156,16 +270,190 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
|
|
|
156
270
|
}
|
|
157
271
|
const loadPromise = (async () => {
|
|
158
272
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
273
|
+
let code = "";
|
|
274
|
+
let rawCode = "";
|
|
275
|
+
let rawCodeFetched = false;
|
|
276
|
+
const isBrowser = typeof window !== "undefined";
|
|
277
|
+
const isNextJSServer = typeof window === "undefined" || typeof globalThis.process !== "undefined" && globalThis.process.env?.NEXT_RUNTIME === "nodejs";
|
|
278
|
+
if (isBrowser && !isNextJSServer) {
|
|
279
|
+
try {
|
|
280
|
+
const variantUrl = `/probat/${filePath}`;
|
|
281
|
+
const mod = await import(
|
|
282
|
+
/* @vite-ignore */
|
|
283
|
+
variantUrl
|
|
284
|
+
);
|
|
285
|
+
const VariantComponent2 = mod?.default || mod;
|
|
286
|
+
if (VariantComponent2 && typeof VariantComponent2 === "function") {
|
|
287
|
+
console.log(`[PROBAT] \u2705 Loaded variant via dynamic import (CSR): ${variantUrl}`);
|
|
288
|
+
return VariantComponent2;
|
|
289
|
+
}
|
|
290
|
+
} catch (dynamicImportError) {
|
|
291
|
+
console.debug(`[PROBAT] Dynamic import failed, using fetch+babel:`, dynamicImportError);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const localUrl = `/probat/${filePath}`;
|
|
295
|
+
try {
|
|
296
|
+
const localRes = await fetch(localUrl, {
|
|
297
|
+
method: "GET",
|
|
298
|
+
headers: { Accept: "text/plain" }
|
|
299
|
+
});
|
|
300
|
+
if (localRes.ok) {
|
|
301
|
+
rawCode = await localRes.text();
|
|
302
|
+
rawCodeFetched = true;
|
|
303
|
+
console.log(`[PROBAT] \u2705 Loaded variant from local (user's repo): ${localUrl}`);
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
console.debug(`[PROBAT] Local file not available (${localUrl}), trying GitHub...`);
|
|
307
|
+
}
|
|
308
|
+
if (!rawCodeFetched && repoFullName) {
|
|
309
|
+
const githubPath = `probat/${filePath}`;
|
|
310
|
+
const gitRef = baseRef || "main";
|
|
311
|
+
const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${gitRef}/${githubPath}`;
|
|
312
|
+
const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
|
|
313
|
+
if (res.ok) {
|
|
314
|
+
rawCode = await res.text();
|
|
315
|
+
rawCodeFetched = true;
|
|
316
|
+
console.log(`[PROBAT] \u26A0\uFE0F Loaded variant from GitHub (fallback): ${githubUrl}`);
|
|
317
|
+
} else {
|
|
318
|
+
console.warn(`[PROBAT] \u26A0\uFE0F GitHub fetch failed (${res.status}), falling back to server compilation`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (rawCodeFetched && rawCode) {
|
|
322
|
+
let Babel;
|
|
323
|
+
if (typeof window !== "undefined" && window.Babel) {
|
|
324
|
+
Babel = window.Babel;
|
|
325
|
+
} else {
|
|
326
|
+
try {
|
|
327
|
+
const babelModule = await import('@babel/standalone');
|
|
328
|
+
Babel = babelModule.default || babelModule;
|
|
329
|
+
} catch (importError) {
|
|
330
|
+
try {
|
|
331
|
+
await new Promise((resolve, reject) => {
|
|
332
|
+
if (typeof document === "undefined") {
|
|
333
|
+
reject(new Error("Document not available"));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (window.Babel) {
|
|
337
|
+
Babel = window.Babel;
|
|
338
|
+
resolve();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const script = document.createElement("script");
|
|
342
|
+
script.src = "https://unpkg.com/@babel/standalone/babel.min.js";
|
|
343
|
+
script.async = true;
|
|
344
|
+
script.onload = () => {
|
|
345
|
+
Babel = window.Babel;
|
|
346
|
+
if (!Babel) reject(new Error("Babel not found after script load"));
|
|
347
|
+
else resolve();
|
|
348
|
+
};
|
|
349
|
+
script.onerror = () => reject(new Error("Failed to load Babel from CDN"));
|
|
350
|
+
document.head.appendChild(script);
|
|
351
|
+
});
|
|
352
|
+
} catch (babelError) {
|
|
353
|
+
console.error("[PROBAT] Failed to load Babel, falling back to server compilation", babelError);
|
|
354
|
+
rawCodeFetched = false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (rawCodeFetched && rawCode && Babel) {
|
|
359
|
+
const isTSX = filePath.endsWith(".tsx");
|
|
360
|
+
rawCode = rawCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
|
|
361
|
+
rawCode = rawCode.replace(/^import\s+.*from\s+['"].*\.css['"];?\s*$/gm, "");
|
|
362
|
+
rawCode = rawCode.replace(
|
|
363
|
+
/^import\s+React(?:\s*,\s*\{[^}]*\})?\s+from\s+['"]react['"];?\s*$/m,
|
|
364
|
+
"const React = window.React || globalThis.React;"
|
|
365
|
+
);
|
|
366
|
+
rawCode = rawCode.replace(
|
|
367
|
+
/^import\s+\*\s+as\s+React\s+from\s+['"]react['"];?\s*$/m,
|
|
368
|
+
"const React = window.React || globalThis.React;"
|
|
369
|
+
);
|
|
370
|
+
rawCode = rawCode.replace(
|
|
371
|
+
/^import\s+\{([^}]+)\}\s+from\s+['"]react['"];?\s*$/m,
|
|
372
|
+
(match, imports) => `const {${imports}} = window.React || globalThis.React;`
|
|
373
|
+
);
|
|
374
|
+
rawCode = rawCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
|
|
375
|
+
rawCode = rawCode.replace(/\bimport\.meta\b/g, "({})");
|
|
376
|
+
rawCode = rawCode.replace(/^import\s+.*\/@vite\/client.*$/gm, "");
|
|
377
|
+
rawCode = rawCode.replace(/import\.meta\.hot(?:\.[\w$]+)*/g, "undefined");
|
|
378
|
+
const relativeImportMap = /* @__PURE__ */ new Map();
|
|
379
|
+
rawCode = rawCode.replace(
|
|
380
|
+
/^import\s+(\w+)\s+from\s+['"](\.\.?\/[^'"]+)['"];?\s*$/gm,
|
|
381
|
+
(match, importName, relativePath) => {
|
|
382
|
+
const baseDir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
383
|
+
const resolvedPath = resolveRelativePath(relativePath, baseDir);
|
|
384
|
+
const absolutePath = resolvedPath.startsWith("/") ? resolvedPath : "/" + resolvedPath;
|
|
385
|
+
relativeImportMap.set(importName, absolutePath);
|
|
386
|
+
return `import ${importName} from "${absolutePath}";`;
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
const compiled = Babel.transform(rawCode, {
|
|
390
|
+
presets: [
|
|
391
|
+
["react", { runtime: "classic" }],
|
|
392
|
+
["typescript", { allExtensions: true, isTSX }]
|
|
393
|
+
],
|
|
394
|
+
plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
|
|
395
|
+
sourceType: "module",
|
|
396
|
+
filename: filePath
|
|
397
|
+
}).code;
|
|
398
|
+
const relativeModules = {};
|
|
399
|
+
if (relativeImportMap.size > 0) {
|
|
400
|
+
for (const [importName, absolutePath] of relativeImportMap.entries()) {
|
|
401
|
+
try {
|
|
402
|
+
const moduleExports = await loadRelativeModule(absolutePath, filePath, repoFullName, baseRef);
|
|
403
|
+
relativeModules[absolutePath] = moduleExports.default || moduleExports;
|
|
404
|
+
} catch (err) {
|
|
405
|
+
console.warn(`[PROBAT] Failed to load relative import ${absolutePath}:`, err);
|
|
406
|
+
relativeModules[absolutePath] = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const relativeModulesJson = JSON.stringify(relativeModules);
|
|
411
|
+
code = `
|
|
412
|
+
var __probatVariant = (function() {
|
|
413
|
+
var relativeModules = ${relativeModulesJson};
|
|
414
|
+
var require = function(name) {
|
|
415
|
+
if (name === "react" || name === "react/jsx-runtime") {
|
|
416
|
+
return window.React || globalThis.React;
|
|
417
|
+
}
|
|
418
|
+
if (name.startsWith("/@vite") || name.includes("@vite/client") || name.includes(".vite/deps")) {
|
|
419
|
+
return {};
|
|
420
|
+
}
|
|
421
|
+
if (name === "react/jsx-runtime.js") {
|
|
422
|
+
return window.React || globalThis.React;
|
|
423
|
+
}
|
|
424
|
+
// Handle relative imports (now converted to absolute paths)
|
|
425
|
+
if (name.startsWith("/") && relativeModules.hasOwnProperty(name)) {
|
|
426
|
+
var mod = relativeModules[name];
|
|
427
|
+
if (mod === null) {
|
|
428
|
+
throw new Error("Failed to load module: " + name);
|
|
429
|
+
}
|
|
430
|
+
return mod;
|
|
431
|
+
}
|
|
432
|
+
throw new Error("Unsupported module: " + name);
|
|
433
|
+
};
|
|
434
|
+
var module = { exports: {} };
|
|
435
|
+
var exports = module.exports;
|
|
436
|
+
${compiled}
|
|
437
|
+
return module.exports.default || module.exports;
|
|
438
|
+
})();
|
|
439
|
+
`;
|
|
440
|
+
} else {
|
|
441
|
+
rawCodeFetched = false;
|
|
442
|
+
code = "";
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!rawCodeFetched || code === "") {
|
|
446
|
+
const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
|
|
447
|
+
const serverRes = await fetch(variantUrl, {
|
|
448
|
+
method: "GET",
|
|
449
|
+
headers: { Accept: "text/javascript" },
|
|
450
|
+
credentials: "include"
|
|
451
|
+
});
|
|
452
|
+
if (!serverRes.ok) {
|
|
453
|
+
throw new Error(`HTTP ${serverRes.status}`);
|
|
454
|
+
}
|
|
455
|
+
code = await serverRes.text();
|
|
167
456
|
}
|
|
168
|
-
const code = await res.text();
|
|
169
457
|
if (typeof window !== "undefined") {
|
|
170
458
|
window.React = window.React || React4;
|
|
171
459
|
}
|
|
@@ -192,146 +480,7 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
|
|
|
192
480
|
return loadPromise;
|
|
193
481
|
}
|
|
194
482
|
|
|
195
|
-
// src/
|
|
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
|
-
}
|
|
483
|
+
// src/hooks/useProbatMetrics.ts
|
|
335
484
|
function useProbatMetrics() {
|
|
336
485
|
const { apiBaseUrl } = useProbatContext();
|
|
337
486
|
const trackClick = useCallback(
|
|
@@ -595,7 +744,9 @@ function withExperiment(Control, options) {
|
|
|
595
744
|
apiBaseUrl,
|
|
596
745
|
componentConfig.proposal_id,
|
|
597
746
|
variantInfo.experiment_id,
|
|
598
|
-
variantInfo.file_path
|
|
747
|
+
variantInfo.file_path,
|
|
748
|
+
componentConfig.repo_full_name,
|
|
749
|
+
componentConfig.base_ref
|
|
599
750
|
);
|
|
600
751
|
if (VariantComp && typeof VariantComp === "function" && alive) {
|
|
601
752
|
variantComponents[label2] = VariantComp;
|
|
@@ -694,28 +845,16 @@ function withExperiment(Control, options) {
|
|
|
694
845
|
}
|
|
695
846
|
const label = choice?.label ?? "control";
|
|
696
847
|
const Variant = registry[label] || registry.control || ControlComponent;
|
|
697
|
-
return /* @__PURE__ */ React4.createElement(
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
);
|
|
848
|
+
return /* @__PURE__ */ React4.createElement("div", { onClick: (event) => trackClick(event), "data-probat-proposal": proposalId }, React4.createElement(Variant, {
|
|
849
|
+
key: `${proposalId}:${label}`,
|
|
850
|
+
...props,
|
|
851
|
+
probat: { trackClick: () => trackClick(null, { force: true }) }
|
|
852
|
+
}));
|
|
714
853
|
}
|
|
715
854
|
Wrapped.displayName = `withExperiment(${Control.displayName || Control.name || "Component"})`;
|
|
716
855
|
return Wrapped;
|
|
717
856
|
}
|
|
718
857
|
|
|
719
|
-
export { ProbatProvider, ProbatProviderClient,
|
|
858
|
+
export { ProbatProvider, ProbatProviderClient, detectEnvironment, extractClickMeta, fetchDecision, hasTrackedVisit, markTrackedVisit, readChoice, sendMetric, useExperiment, useProbatContext, useProbatMetrics, withExperiment, writeChoice };
|
|
720
859
|
//# sourceMappingURL=index.mjs.map
|
|
721
860
|
//# sourceMappingURL=index.mjs.map
|