@probat/react 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1 -30
- package/dist/index.d.ts +1 -30
- package/dist/index.js +189 -157
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +191 -155
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
- 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 +155 -16
- package/src/utils/documentClickTracker.ts +61 -46
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);
|
|
@@ -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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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/
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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,
|
|
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
|