@probat/react 0.1.4 → 0.2.0

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.js CHANGED
@@ -21,6 +21,336 @@ function detectEnvironment() {
21
21
  return "prod";
22
22
  }
23
23
 
24
+ // src/utils/heatmapTracker.ts
25
+ function getStoredExperimentInfo() {
26
+ if (typeof window === "undefined") return {};
27
+ try {
28
+ const proposalId = window.localStorage.getItem("probat_active_proposal_id") || void 0;
29
+ const variantLabel = window.localStorage.getItem("probat_active_variant_label") || void 0;
30
+ return { proposalId, variantLabel };
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+ var HeatmapTracker = class {
36
+ constructor(config) {
37
+ this.clicks = [];
38
+ this.movements = [];
39
+ this.batchTimer = null;
40
+ this.cursorBatchTimer = null;
41
+ this.lastCursorTime = 0;
42
+ this.isInitialized = false;
43
+ this.handleMouseMove = (event) => {
44
+ if (!this.config.enabled || !this.config.trackCursor) return;
45
+ if (typeof window === "undefined") return;
46
+ const stored = getStoredExperimentInfo();
47
+ const proposalId = this.config.proposalId || stored.proposalId;
48
+ const variantLabel = this.config.variantLabel || stored.variantLabel;
49
+ const now2 = Date.now();
50
+ const cursorThrottle = this.config.cursorThrottle ?? 100;
51
+ if (now2 - this.lastCursorTime < cursorThrottle) {
52
+ return;
53
+ }
54
+ this.lastCursorTime = now2;
55
+ if (this.movements.length < 3) {
56
+ console.log("[PROBAT Heatmap] Cursor movement captured:", {
57
+ x: event.clientX,
58
+ y: event.clientY,
59
+ movementsInBatch: this.movements.length + 1
60
+ });
61
+ }
62
+ const pageUrl = window.location.href;
63
+ const siteUrl = window.location.origin;
64
+ const x = event.clientX;
65
+ const y = event.clientY;
66
+ const viewportWidth = window.innerWidth;
67
+ const viewportHeight = window.innerHeight;
68
+ const movementEvent = {
69
+ page_url: pageUrl,
70
+ site_url: siteUrl,
71
+ x_coordinate: x,
72
+ y_coordinate: y,
73
+ viewport_width: viewportWidth,
74
+ viewport_height: viewportHeight,
75
+ session_id: this.sessionId,
76
+ proposal_id: proposalId,
77
+ variant_label: variantLabel
78
+ };
79
+ this.movements.push(movementEvent);
80
+ const cursorBatchSize = this.config.cursorBatchSize ?? 50;
81
+ if (this.movements.length >= cursorBatchSize) {
82
+ this.sendCursorBatch();
83
+ } else {
84
+ this.scheduleCursorBatchSend();
85
+ }
86
+ };
87
+ this.handleClick = (event) => {
88
+ if (!this.config.enabled) return;
89
+ if (typeof window === "undefined") return;
90
+ const stored = getStoredExperimentInfo();
91
+ const proposalId = this.config.proposalId || stored.proposalId;
92
+ const variantLabel = this.config.variantLabel || stored.variantLabel;
93
+ const target = event.target;
94
+ if (!target) return;
95
+ if (this.shouldExcludeElement(target)) {
96
+ return;
97
+ }
98
+ const pageUrl = window.location.href;
99
+ const siteUrl = window.location.origin;
100
+ const x = event.clientX;
101
+ const y = event.clientY;
102
+ const viewportWidth = window.innerWidth;
103
+ const viewportHeight = window.innerHeight;
104
+ const elementInfo = this.extractElementInfo(target);
105
+ const clickEvent = {
106
+ page_url: pageUrl,
107
+ site_url: siteUrl,
108
+ x_coordinate: x,
109
+ y_coordinate: y,
110
+ viewport_width: viewportWidth,
111
+ viewport_height: viewportHeight,
112
+ element_tag: elementInfo.tag,
113
+ element_class: elementInfo.class,
114
+ element_id: elementInfo.id,
115
+ session_id: this.sessionId,
116
+ proposal_id: proposalId,
117
+ variant_label: variantLabel
118
+ };
119
+ this.clicks.push(clickEvent);
120
+ const batchSize = this.config.batchSize ?? 10;
121
+ if (this.clicks.length >= batchSize) {
122
+ this.sendBatch();
123
+ } else {
124
+ this.scheduleBatchSend();
125
+ }
126
+ };
127
+ this.sessionId = this.getOrCreateSessionId();
128
+ const stored = getStoredExperimentInfo();
129
+ this.config = {
130
+ apiBaseUrl: config.apiBaseUrl,
131
+ batchSize: config.batchSize || 10,
132
+ batchInterval: config.batchInterval || 5e3,
133
+ enabled: config.enabled !== false,
134
+ excludeSelectors: config.excludeSelectors || [
135
+ "[data-heatmap-exclude]",
136
+ 'input[type="password"]',
137
+ 'input[type="email"]',
138
+ "textarea"
139
+ ],
140
+ trackCursor: config.trackCursor !== false,
141
+ // Enable cursor tracking by default
142
+ cursorThrottle: config.cursorThrottle || 100,
143
+ // Capture cursor position every 100ms
144
+ cursorBatchSize: config.cursorBatchSize || 50,
145
+ // Send every 50 movements
146
+ proposalId: config.proposalId || stored.proposalId,
147
+ variantLabel: config.variantLabel || stored.variantLabel
148
+ };
149
+ }
150
+ getOrCreateSessionId() {
151
+ if (typeof window === "undefined") return "";
152
+ const storageKey = "probat_heatmap_session_id";
153
+ let sessionId = localStorage.getItem(storageKey);
154
+ if (!sessionId) {
155
+ sessionId = `heatmap_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
156
+ localStorage.setItem(storageKey, sessionId);
157
+ }
158
+ return sessionId;
159
+ }
160
+ shouldExcludeElement(element) {
161
+ if (!this.config.excludeSelectors) return false;
162
+ for (const selector of this.config.excludeSelectors) {
163
+ if (element.matches(selector) || element.closest(selector)) {
164
+ return true;
165
+ }
166
+ }
167
+ return false;
168
+ }
169
+ extractElementInfo(element) {
170
+ return {
171
+ tag: element.tagName || null,
172
+ class: element.className && typeof element.className === "string" ? element.className : null,
173
+ id: element.id || null
174
+ };
175
+ }
176
+ scheduleCursorBatchSend() {
177
+ if (this.cursorBatchTimer) {
178
+ clearTimeout(this.cursorBatchTimer);
179
+ }
180
+ this.cursorBatchTimer = setTimeout(() => {
181
+ if (this.movements.length > 0) {
182
+ this.sendCursorBatch();
183
+ }
184
+ }, this.config.batchInterval ?? 5e3);
185
+ }
186
+ async sendCursorBatch() {
187
+ if (this.movements.length === 0) return;
188
+ const siteUrl = this.movements[0]?.site_url || window.location.origin;
189
+ const batch = {
190
+ movements: this.movements,
191
+ site_url: siteUrl,
192
+ proposal_id: this.config.proposalId,
193
+ variant_label: this.config.variantLabel
194
+ };
195
+ this.movements = [];
196
+ if (this.cursorBatchTimer) {
197
+ clearTimeout(this.cursorBatchTimer);
198
+ this.cursorBatchTimer = null;
199
+ }
200
+ try {
201
+ console.log("[PROBAT Heatmap] Sending cursor movements batch:", {
202
+ count: batch.movements.length,
203
+ site_url: batch.site_url
204
+ });
205
+ const response = await fetch(`${this.config.apiBaseUrl}/api/heatmap/cursor-movements`, {
206
+ method: "POST",
207
+ headers: {
208
+ "Content-Type": "application/json"
209
+ },
210
+ body: JSON.stringify(batch)
211
+ // Don't wait for response - fire and forget for performance
212
+ });
213
+ if (!response.ok) {
214
+ console.warn("[PROBAT Heatmap] Failed to send cursor movements:", response.status);
215
+ } else {
216
+ console.log("[PROBAT Heatmap] Successfully sent cursor movements batch");
217
+ }
218
+ } catch (error) {
219
+ console.warn("[PROBAT Heatmap] Error sending cursor movements:", error);
220
+ }
221
+ }
222
+ scheduleBatchSend() {
223
+ if (this.batchTimer) {
224
+ clearTimeout(this.batchTimer);
225
+ }
226
+ this.batchTimer = setTimeout(() => {
227
+ if (this.clicks.length > 0) {
228
+ this.sendBatch();
229
+ }
230
+ }, this.config.batchInterval ?? 5e3);
231
+ }
232
+ async sendBatch() {
233
+ if (this.clicks.length === 0) return;
234
+ if (this.batchTimer) {
235
+ clearTimeout(this.batchTimer);
236
+ this.batchTimer = null;
237
+ }
238
+ const siteUrl = this.clicks[0]?.site_url || window.location.origin;
239
+ const batch = {
240
+ clicks: [...this.clicks],
241
+ site_url: siteUrl
242
+ };
243
+ this.clicks = [];
244
+ try {
245
+ const response = await fetch(`${this.config.apiBaseUrl}/api/heatmap/clicks`, {
246
+ method: "POST",
247
+ headers: {
248
+ "Content-Type": "application/json"
249
+ },
250
+ body: JSON.stringify(batch)
251
+ });
252
+ if (!response.ok) {
253
+ console.warn("[PROBAT Heatmap] Failed to send clicks:", response.status);
254
+ }
255
+ } catch (error) {
256
+ console.warn("[PROBAT Heatmap] Error sending clicks:", error);
257
+ }
258
+ }
259
+ init() {
260
+ if (this.isInitialized) {
261
+ console.warn("[PROBAT Heatmap] Tracker already initialized");
262
+ return;
263
+ }
264
+ if (typeof window === "undefined") {
265
+ return;
266
+ }
267
+ document.addEventListener("click", this.handleClick, true);
268
+ if (this.config.trackCursor) {
269
+ document.addEventListener("mousemove", this.handleMouseMove, { passive: true });
270
+ console.log("[PROBAT Heatmap] Cursor tracking enabled");
271
+ } else {
272
+ console.log("[PROBAT Heatmap] Cursor tracking disabled");
273
+ }
274
+ this.isInitialized = true;
275
+ console.log("[PROBAT Heatmap] Tracker initialized", {
276
+ enabled: this.config.enabled,
277
+ trackCursor: this.config.trackCursor,
278
+ cursorThrottle: this.config.cursorThrottle,
279
+ cursorBatchSize: this.config.cursorBatchSize
280
+ });
281
+ window.addEventListener("beforeunload", () => {
282
+ if (this.clicks.length > 0) {
283
+ const siteUrl = this.clicks[0]?.site_url || window.location.origin;
284
+ const batch = {
285
+ clicks: this.clicks,
286
+ site_url: siteUrl
287
+ };
288
+ const blob = new Blob([JSON.stringify(batch)], {
289
+ type: "application/json"
290
+ });
291
+ navigator.sendBeacon(
292
+ `${this.config.apiBaseUrl}/api/heatmap/clicks`,
293
+ blob
294
+ );
295
+ }
296
+ if (this.movements.length > 0) {
297
+ const siteUrl = this.movements[0]?.site_url || window.location.origin;
298
+ const batch = {
299
+ movements: this.movements,
300
+ site_url: siteUrl
301
+ };
302
+ const blob = new Blob([JSON.stringify(batch)], {
303
+ type: "application/json"
304
+ });
305
+ navigator.sendBeacon(
306
+ `${this.config.apiBaseUrl}/api/heatmap/cursor-movements`,
307
+ blob
308
+ );
309
+ }
310
+ });
311
+ }
312
+ stop() {
313
+ if (!this.isInitialized) return;
314
+ if (this.clicks.length > 0) {
315
+ this.sendBatch();
316
+ }
317
+ if (this.movements.length > 0) {
318
+ this.sendCursorBatch();
319
+ }
320
+ document.removeEventListener("click", this.handleClick, true);
321
+ if (this.config.trackCursor) {
322
+ document.removeEventListener("mousemove", this.handleMouseMove);
323
+ }
324
+ if (this.batchTimer) {
325
+ clearTimeout(this.batchTimer);
326
+ this.batchTimer = null;
327
+ }
328
+ if (this.cursorBatchTimer) {
329
+ clearTimeout(this.cursorBatchTimer);
330
+ this.cursorBatchTimer = null;
331
+ }
332
+ this.isInitialized = false;
333
+ }
334
+ };
335
+ var trackerInstance = null;
336
+ function initHeatmapTracking(config) {
337
+ if (trackerInstance) {
338
+ trackerInstance.stop();
339
+ }
340
+ trackerInstance = new HeatmapTracker(config);
341
+ trackerInstance.init();
342
+ return trackerInstance;
343
+ }
344
+ function getHeatmapTracker() {
345
+ return trackerInstance;
346
+ }
347
+ function stopHeatmapTracking() {
348
+ if (trackerInstance) {
349
+ trackerInstance.stop();
350
+ trackerInstance = null;
351
+ }
352
+ }
353
+
24
354
  // src/context/ProbatContext.tsx
25
355
  var ProbatContext = React4.createContext(null);
26
356
  function ProbatProvider({
@@ -28,8 +358,12 @@ function ProbatProvider({
28
358
  clientKey,
29
359
  environment: explicitEnvironment,
30
360
  repoFullName: explicitRepoFullName,
361
+ proposalId,
362
+ variantLabel,
31
363
  children
32
364
  }) {
365
+ const storedProposalId = typeof window !== "undefined" ? window.localStorage.getItem("probat_active_proposal_id") || void 0 : void 0;
366
+ const storedVariantLabel = typeof window !== "undefined" ? window.localStorage.getItem("probat_active_variant_label") || void 0 : void 0;
33
367
  const contextValue = React4.useMemo(() => {
34
368
  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
369
  const environment = explicitEnvironment || detectEnvironment();
@@ -38,9 +372,38 @@ function ProbatProvider({
38
372
  apiBaseUrl: resolvedApiBaseUrl,
39
373
  environment,
40
374
  clientKey,
41
- repoFullName: resolvedRepoFullName
375
+ repoFullName: resolvedRepoFullName,
376
+ proposalId: proposalId || storedProposalId,
377
+ variantLabel: variantLabel || storedVariantLabel
42
378
  };
43
- }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName]);
379
+ }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName, proposalId, variantLabel, storedProposalId, storedVariantLabel]);
380
+ React4.useEffect(() => {
381
+ if (typeof window !== "undefined") {
382
+ initHeatmapTracking({
383
+ apiBaseUrl: contextValue.apiBaseUrl,
384
+ batchSize: 10,
385
+ batchInterval: 5e3,
386
+ enabled: true,
387
+ excludeSelectors: [
388
+ "[data-heatmap-exclude]",
389
+ 'input[type="password"]',
390
+ 'input[type="email"]',
391
+ "textarea"
392
+ ],
393
+ // Explicitly enable cursor tracking with sensible defaults
394
+ trackCursor: true,
395
+ cursorThrottle: 100,
396
+ // capture every 100ms
397
+ cursorBatchSize: 50,
398
+ // send every 50 movements (or after batchInterval)
399
+ proposalId: contextValue.proposalId,
400
+ variantLabel: contextValue.variantLabel
401
+ });
402
+ }
403
+ return () => {
404
+ stopHeatmapTracking();
405
+ };
406
+ }, [contextValue.apiBaseUrl]);
44
407
  return /* @__PURE__ */ React4__default.default.createElement(ProbatContext.Provider, { value: contextValue }, children);
45
408
  }
46
409
  function useProbatContext() {
@@ -74,6 +437,14 @@ async function fetchDecision(baseUrl, proposalId) {
74
437
  const data = await res.json();
75
438
  const experiment_id = (data.experiment_id || `exp_${proposalId}`).toString();
76
439
  const label = data.label && data.label.trim() ? data.label : "control";
440
+ if (typeof window !== "undefined") {
441
+ try {
442
+ window.localStorage.setItem("probat_active_proposal_id", proposalId);
443
+ window.localStorage.setItem("probat_active_variant_label", label);
444
+ } catch (e) {
445
+ console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", e);
446
+ }
447
+ }
77
448
  return { experiment_id, label };
78
449
  } finally {
79
450
  pendingFetches.delete(proposalId);
@@ -152,6 +523,13 @@ async function fetchComponentExperimentConfig(baseUrl, repoFullName, componentPa
152
523
  throw new Error(`HTTP ${res.status}`);
153
524
  }
154
525
  const data = await res.json();
526
+ if (typeof window !== "undefined" && data?.proposal_id) {
527
+ try {
528
+ window.localStorage.setItem("probat_active_proposal_id", data.proposal_id);
529
+ } catch (e) {
530
+ console.warn("[PROBAT] Failed to set proposal_id in localStorage:", e);
531
+ }
532
+ }
155
533
  return data;
156
534
  } catch (e) {
157
535
  console.warn(`[PROBAT] Failed to fetch component config: ${e}`);
@@ -281,14 +659,12 @@ async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath,
281
659
  let rawCode = "";
282
660
  let rawCodeFetched = false;
283
661
  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) {
662
+ const isNextJS = isBrowser && (window.__NEXT_DATA__ !== void 0 || window.__NEXT_LOADED_PAGES__ !== void 0 || typeof globalThis.__NEXT_DATA__ !== "undefined");
663
+ if (isBrowser && !isNextJS) {
286
664
  try {
287
665
  const variantUrl = `/probat/${filePath}`;
288
- const mod = await import(
289
- /* @vite-ignore */
290
- variantUrl
291
- );
666
+ const dynamicImportFunc = new Function("url", "return import(url)");
667
+ const mod = await dynamicImportFunc(variantUrl);
292
668
  const VariantComponent2 = mod?.default || mod;
293
669
  if (VariantComponent2 && typeof VariantComponent2 === "function") {
294
670
  console.log(`[PROBAT] \u2705 Loaded variant via dynamic import (CSR): ${variantUrl}`);
@@ -788,23 +1164,50 @@ function withExperiment(Control, options) {
788
1164
  let alive = true;
789
1165
  const cached = readChoice(proposalId);
790
1166
  if (cached) {
791
- setChoice({
1167
+ const choiceData = {
792
1168
  experiment_id: cached.experiment_id,
793
1169
  label: cached.label
794
- });
1170
+ };
1171
+ setChoice(choiceData);
1172
+ if (typeof window !== "undefined") {
1173
+ try {
1174
+ window.localStorage.setItem("probat_active_proposal_id", proposalId);
1175
+ window.localStorage.setItem("probat_active_variant_label", cached.label);
1176
+ } catch (e) {
1177
+ console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", e);
1178
+ }
1179
+ }
795
1180
  } else {
796
1181
  (async () => {
797
1182
  try {
798
1183
  const { experiment_id, label: label2 } = await fetchDecision(apiBaseUrl, proposalId);
799
1184
  if (!alive) return;
800
1185
  writeChoice(proposalId, experiment_id, label2);
801
- setChoice({ experiment_id, label: label2 });
1186
+ const choiceData = { experiment_id, label: label2 };
1187
+ setChoice(choiceData);
1188
+ if (typeof window !== "undefined") {
1189
+ try {
1190
+ window.localStorage.setItem("probat_active_proposal_id", proposalId);
1191
+ window.localStorage.setItem("probat_active_variant_label", label2);
1192
+ } catch (e) {
1193
+ console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", e);
1194
+ }
1195
+ }
802
1196
  } catch (e) {
803
1197
  if (!alive) return;
804
- setChoice({
1198
+ const choiceData = {
805
1199
  experiment_id: `exp_${proposalId}`,
806
1200
  label: "control"
807
- });
1201
+ };
1202
+ setChoice(choiceData);
1203
+ if (typeof window !== "undefined") {
1204
+ try {
1205
+ window.localStorage.setItem("probat_active_proposal_id", proposalId);
1206
+ window.localStorage.setItem("probat_active_variant_label", "control");
1207
+ } catch (err) {
1208
+ console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", err);
1209
+ }
1210
+ }
808
1211
  }
809
1212
  })();
810
1213
  }
@@ -867,10 +1270,13 @@ exports.ProbatProviderClient = ProbatProviderClient;
867
1270
  exports.detectEnvironment = detectEnvironment;
868
1271
  exports.extractClickMeta = extractClickMeta;
869
1272
  exports.fetchDecision = fetchDecision;
1273
+ exports.getHeatmapTracker = getHeatmapTracker;
870
1274
  exports.hasTrackedVisit = hasTrackedVisit;
1275
+ exports.initHeatmapTracking = initHeatmapTracking;
871
1276
  exports.markTrackedVisit = markTrackedVisit;
872
1277
  exports.readChoice = readChoice;
873
1278
  exports.sendMetric = sendMetric;
1279
+ exports.stopHeatmapTracking = stopHeatmapTracking;
874
1280
  exports.useExperiment = useExperiment;
875
1281
  exports.useProbatContext = useProbatContext;
876
1282
  exports.useProbatMetrics = useProbatMetrics;