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