@probat/react 0.1.0 → 0.1.2
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 +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +182 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +180 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/context/ProbatContext.tsx +12 -1
- package/src/hoc/withExperiment.tsx +11 -1
- package/src/index.ts +6 -0
- package/src/utils/api.ts +24 -3
- package/src/utils/documentClickTracker.ts +215 -0
package/dist/index.d.mts
CHANGED
|
@@ -190,4 +190,33 @@ 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
|
-
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -190,4 +190,33 @@ 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
|
-
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -20,41 +20,6 @@ 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
|
-
}
|
|
58
23
|
var pendingFetches = /* @__PURE__ */ new Map();
|
|
59
24
|
async function fetchDecision(baseUrl, proposalId) {
|
|
60
25
|
const existingFetch = pendingFetches.get(proposalId);
|
|
@@ -97,7 +62,7 @@ async function sendMetric(baseUrl, proposalId, metricName, variantLabel = "contr
|
|
|
97
62
|
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
98
63
|
};
|
|
99
64
|
try {
|
|
100
|
-
await fetch(url, {
|
|
65
|
+
const response = await fetch(url, {
|
|
101
66
|
method: "POST",
|
|
102
67
|
headers: {
|
|
103
68
|
Accept: "application/json",
|
|
@@ -107,7 +72,26 @@ async function sendMetric(baseUrl, proposalId, metricName, variantLabel = "contr
|
|
|
107
72
|
// CRITICAL: Include cookies to distinguish different users
|
|
108
73
|
body: JSON.stringify(body)
|
|
109
74
|
});
|
|
110
|
-
|
|
75
|
+
if (!response.ok) {
|
|
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
|
+
});
|
|
111
95
|
}
|
|
112
96
|
}
|
|
113
97
|
function extractClickMeta(event) {
|
|
@@ -215,7 +199,146 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath)
|
|
|
215
199
|
return loadPromise;
|
|
216
200
|
}
|
|
217
201
|
|
|
218
|
-
// src/
|
|
202
|
+
// src/utils/documentClickTracker.ts
|
|
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
|
+
}
|
|
219
342
|
function useProbatMetrics() {
|
|
220
343
|
const { apiBaseUrl } = useProbatContext();
|
|
221
344
|
const trackClick = React4.useCallback(
|
|
@@ -578,11 +701,23 @@ function withExperiment(Control, options) {
|
|
|
578
701
|
}
|
|
579
702
|
const label = choice?.label ?? "control";
|
|
580
703
|
const Variant = registry[label] || registry.control || ControlComponent;
|
|
581
|
-
return /* @__PURE__ */ React4__default.default.createElement(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
704
|
+
return /* @__PURE__ */ React4__default.default.createElement(
|
|
705
|
+
"div",
|
|
706
|
+
{
|
|
707
|
+
onClick: (event) => {
|
|
708
|
+
trackClick(event);
|
|
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
|
+
);
|
|
586
721
|
}
|
|
587
722
|
Wrapped.displayName = `withExperiment(${Control.displayName || Control.name || "Component"})`;
|
|
588
723
|
return Wrapped;
|
|
@@ -590,13 +725,17 @@ function withExperiment(Control, options) {
|
|
|
590
725
|
|
|
591
726
|
exports.ProbatProvider = ProbatProvider;
|
|
592
727
|
exports.ProbatProviderClient = ProbatProviderClient;
|
|
728
|
+
exports.cleanupDocumentClickTracking = cleanupDocumentClickTracking;
|
|
729
|
+
exports.clearProposalCache = clearProposalCache;
|
|
593
730
|
exports.detectEnvironment = detectEnvironment;
|
|
594
731
|
exports.extractClickMeta = extractClickMeta;
|
|
595
732
|
exports.fetchDecision = fetchDecision;
|
|
596
733
|
exports.hasTrackedVisit = hasTrackedVisit;
|
|
734
|
+
exports.initDocumentClickTracking = initDocumentClickTracking;
|
|
597
735
|
exports.markTrackedVisit = markTrackedVisit;
|
|
598
736
|
exports.readChoice = readChoice;
|
|
599
737
|
exports.sendMetric = sendMetric;
|
|
738
|
+
exports.updateProposalMetadata = updateProposalMetadata;
|
|
600
739
|
exports.useExperiment = useExperiment;
|
|
601
740
|
exports.useProbatContext = useProbatContext;
|
|
602
741
|
exports.useProbatMetrics = useProbatMetrics;
|