alphana-sdk 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.
@@ -0,0 +1,318 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface TrackerConfig {
5
+ /**
6
+ * URL to POST events to.
7
+ * Defaults to `https://api.alphana.ir/api/events` — override only if self-hosting.
8
+ */
9
+ endpoint?: string;
10
+ /**
11
+ * App ID obtained from the UserTracker dashboard.
12
+ * Sent in every request body so the backend associates events with the correct app.
13
+ */
14
+ appId?: string;
15
+ /**
16
+ * App secret key obtained from the UserTracker dashboard.
17
+ * Sent as the `Authorization: Bearer` header on every request.
18
+ */
19
+ secretKey?: string;
20
+ /** Provide a custom session ID; auto-generated via crypto.randomUUID if omitted. */
21
+ sessionId?: string;
22
+ /** Track SPA route changes (pushState / replaceState / popstate). Default: true */
23
+ trackNavigation?: boolean;
24
+ /** Track time spent on each page. Default: true */
25
+ trackTime?: boolean;
26
+ /** Collect mouse-move, click, and scroll data for heatmap. Default: true */
27
+ trackHeatmap?: boolean;
28
+ /**
29
+ * Fraction of mousemove / scroll events to record (0–1).
30
+ * 1 = record every event, 0.3 = ~30 % sampled. Default: 0.3
31
+ */
32
+ mouseSampleRate?: number;
33
+ /** Maximum heatmap points stored in memory per page. Default: 2000 */
34
+ maxHeatmapPoints?: number;
35
+ /**
36
+ * Number of events to accumulate before an automatic flush is triggered.
37
+ * Default: 20
38
+ */
39
+ batchSize?: number;
40
+ /**
41
+ * Milliseconds between automatic batch flushes regardless of queue size.
42
+ * Default: 5000 (5 s)
43
+ */
44
+ flushInterval?: number;
45
+ /**
46
+ * Automatically capture console.info/warn/error, window.onerror, and
47
+ * unhandledrejection events and send them to the backend log endpoint.
48
+ * Default: true (when endpoint is provided)
49
+ */
50
+ trackLogs?: boolean;
51
+ /**
52
+ * Automatically capture and send full-page screenshots to the backend
53
+ * every 5 minutes for use in the heatmap view.
54
+ * Requires `html2canvas` to be installed in the host application:
55
+ * npm install html2canvas
56
+ * Default: true (when endpoint is provided and html2canvas is installed)
57
+ */
58
+ trackSnapshots?: boolean;
59
+ /**
60
+ * Minimum milliseconds between screenshots for the same page path.
61
+ * Defaults to 300 000 (5 minutes). Only used when `trackSnapshots` is true.
62
+ */
63
+ snapshotIntervalMs?: number;
64
+ /** Called synchronously for every emitted event. */
65
+ onEvent?: (event: TrackerEvent) => void;
66
+ }
67
+ interface GeoLocation {
68
+ /** ISO 3166-1 alpha-2 country code, e.g. "US" */
69
+ country: string;
70
+ /** Human-readable country name, e.g. "United States" */
71
+ countryName: string;
72
+ city?: string;
73
+ region?: string;
74
+ latitude?: number;
75
+ longitude?: number;
76
+ }
77
+ interface PageView {
78
+ path: string;
79
+ title: string;
80
+ timestamp: number;
81
+ sessionId: string;
82
+ referrer?: string;
83
+ }
84
+ interface TimeSpent {
85
+ path: string;
86
+ /** Duration in milliseconds */
87
+ duration: number;
88
+ sessionId: string;
89
+ timestamp: number;
90
+ }
91
+ interface HeatmapPoint {
92
+ /** X position as a percentage of the full page width (0–100) */
93
+ xPct: number;
94
+ /** Y position as a percentage of the full page height (0–100) */
95
+ yPct: number;
96
+ /** Absolute X pixel in the viewport at the time of the event */
97
+ x: number;
98
+ /** Absolute Y pixel from the top of the page (includes scroll) */
99
+ y: number;
100
+ type: "move" | "click" | "scroll";
101
+ path: string;
102
+ timestamp: number;
103
+ }
104
+ type TrackerEvent = {
105
+ type: "pageview";
106
+ data: PageView;
107
+ } | {
108
+ type: "timespent";
109
+ data: TimeSpent;
110
+ } | {
111
+ type: "heatmap";
112
+ data: HeatmapPoint;
113
+ };
114
+ interface SessionData {
115
+ id: string;
116
+ startedAt: number;
117
+ pageViews: PageView[];
118
+ /** Cumulative milliseconds per path */
119
+ timeSpent: Record<string, number>;
120
+ /** Collected points per path */
121
+ heatmap: Record<string, HeatmapPoint[]>;
122
+ /** Approximate visitor location resolved from IP (filled asynchronously) */
123
+ location?: GeoLocation;
124
+ }
125
+ declare global {
126
+ interface WindowEventMap {
127
+ "tracker:navigate": CustomEvent<{
128
+ path: string;
129
+ title: string;
130
+ }>;
131
+ }
132
+ }
133
+
134
+ interface UserTrackerProviderProps {
135
+ /** Tracker configuration. Captured on first render — changes are ignored. */
136
+ config?: TrackerConfig;
137
+ children: ReactNode;
138
+ }
139
+ /**
140
+ * Wraps your application (or a subtree) and provides a `UserTracker` instance
141
+ * via React context.
142
+ *
143
+ * The tracker is created once, initialized on mount, and destroyed on unmount.
144
+ *
145
+ * **Next.js App Router** — mark your layout wrapper as a Client Component:
146
+ * ```tsx
147
+ * 'use client';
148
+ * import { UserTrackerProvider } from 'user-tracker/react';
149
+ * export default function RootLayout({ children }) {
150
+ * return <UserTrackerProvider config={{ endpoint: '/api/events' }}>{children}</UserTrackerProvider>;
151
+ * }
152
+ * ```
153
+ */
154
+ declare function UserTrackerProvider({ config, children, }: UserTrackerProviderProps): react_jsx_runtime.JSX.Element;
155
+
156
+ type LogLevel = "info" | "warn" | "error";
157
+ /**
158
+ * Automatically captures console.info/warn/error output and unhandled errors,
159
+ * then ships them to the backend `/logs/ingest` endpoint.
160
+ *
161
+ * All console methods are restored exactly in `destroy()`.
162
+ */
163
+ declare class LogCapture {
164
+ private readonly endpoint;
165
+ private readonly sessionId;
166
+ private readonly appId?;
167
+ private readonly authHeaders;
168
+ private origInfo;
169
+ private origWarn;
170
+ private origError;
171
+ private prevOnError;
172
+ private prevOnUnhandledRejection;
173
+ private initialized;
174
+ constructor(options: {
175
+ endpoint: string;
176
+ sessionId: string;
177
+ secretKey?: string;
178
+ appId?: string;
179
+ });
180
+ init(): void;
181
+ destroy(): void;
182
+ /** Manually capture a log entry (e.g. from try/catch). */
183
+ capture(level: LogLevel, message: string, extra?: {
184
+ stack?: string;
185
+ meta?: Record<string, unknown>;
186
+ }): void;
187
+ private format;
188
+ private send;
189
+ }
190
+
191
+ type SubscriberFn = (event: TrackerEvent) => void;
192
+ declare class UserTracker {
193
+ private readonly cfg;
194
+ private session;
195
+ private navigation?;
196
+ private time?;
197
+ private heatmap?;
198
+ /** Public so consumers can call logCapture.capture() for manual log entries. */
199
+ logCapture?: LogCapture;
200
+ private snapshot?;
201
+ private initialized;
202
+ private readonly subscribers;
203
+ /** In-memory queue of events waiting to be flushed. */
204
+ private queue;
205
+ private flushTimer;
206
+ private heartbeatTimer;
207
+ private location;
208
+ constructor(config?: TrackerConfig);
209
+ /**
210
+ * Attach event listeners and start tracking.
211
+ * Safe to call during SSR — returns `this` immediately if `window` is
212
+ * undefined so it can be chained: `const tracker = new UserTracker(cfg).init()`.
213
+ */
214
+ init(): this;
215
+ /** Remove all event listeners, flush remaining queue, and reset state. */
216
+ destroy(): void;
217
+ private handleVisibilityChange;
218
+ private handlePageHide;
219
+ private handleNavigate;
220
+ /**
221
+ * Send a keep-alive heartbeat so the backend knows this session is still
222
+ * active. Called every 30 s while the tab is visible.
223
+ */
224
+ private sendHeartbeat;
225
+ /**
226
+ * Send a synchronous beacon to mark this session as inactive.
227
+ * Called on page unload / tab hidden so the dashboard reflects real-time.
228
+ */
229
+ private sendDeactivate;
230
+ /** Emit a tracker event. Also used internally by the plugins. */
231
+ emit(event: TrackerEvent): void;
232
+ /**
233
+ * Subscribe to every emitted event. Returns an unsubscribe function.
234
+ *
235
+ * ```ts
236
+ * const unsub = tracker.subscribe(event => console.log(event));
237
+ * // later…
238
+ * unsub();
239
+ * ```
240
+ */
241
+ subscribe(fn: SubscriberFn): () => void;
242
+ /**
243
+ * Manually record a page view and dispatch `tracker:navigate`.
244
+ * Required for Next.js App Router — call it inside a `useEffect` that
245
+ * depends on `usePathname()`:
246
+ *
247
+ * ```tsx
248
+ * const pathname = usePathname();
249
+ * useEffect(() => { tracker.trackPageView(pathname); }, [pathname]);
250
+ * ```
251
+ */
252
+ trackPageView(path?: string): void;
253
+ /** A read-only snapshot of the current session. */
254
+ getSession(): Readonly<SessionData>;
255
+ /** All page views recorded so far. */
256
+ getPageViews(): PageView[];
257
+ /** Cumulative milliseconds spent per path. */
258
+ getTimeSpent(): Record<string, number>;
259
+ /** Heatmap points for a specific path. */
260
+ getHeatmapData(path: string): HeatmapPoint[];
261
+ /** Heatmap points for all tracked paths. */
262
+ getHeatmapData(): Record<string, HeatmapPoint[]>;
263
+ /** Drain the queue and POST all pending events to the batch endpoint. */
264
+ private flush;
265
+ /**
266
+ * Synchronous best-effort flush via `navigator.sendBeacon`.
267
+ * Used on `pagehide` / `visibilitychange:hidden` where async fetch may be
268
+ * cancelled by the browser before it completes.
269
+ */
270
+ private flushBeacon;
271
+ private buildBatchBody;
272
+ private sendBatch;
273
+ }
274
+
275
+ /**
276
+ * Returns the `UserTracker` instance from the nearest `<UserTrackerProvider>`.
277
+ * Returns `null` when called outside of a provider.
278
+ */
279
+ declare function useTracker(): UserTracker | null;
280
+ /**
281
+ * Manually records a page view whenever `path` changes.
282
+ *
283
+ * Pass the current pathname — particularly useful with Next.js App Router:
284
+ * ```tsx
285
+ * 'use client';
286
+ * import { usePathname } from 'next/navigation';
287
+ * import { usePageView } from 'user-tracker/react';
288
+ *
289
+ * export function NavigationTracker() {
290
+ * usePageView(usePathname());
291
+ * return null;
292
+ * }
293
+ * ```
294
+ * When no `path` is provided the hook is a no-op (automatic tracking via the
295
+ * NavigationPlugin handles it).
296
+ */
297
+ declare function usePageView(path?: string): void;
298
+ /**
299
+ * Returns a live array of `HeatmapPoint` objects for the given path (defaults
300
+ * to `window.location.pathname`).
301
+ *
302
+ * The state is updated in batches — at most once every `refreshMs` ms — to
303
+ * avoid a re-render on every single mouse move.
304
+ *
305
+ * @param path The page path to query. Defaults to the current pathname.
306
+ * @param refreshMs Minimum interval between state updates. Default: 500.
307
+ */
308
+ declare function useHeatmapData(path?: string, refreshMs?: number): HeatmapPoint[];
309
+ /**
310
+ * Returns a live array of all page views recorded in the current session.
311
+ */
312
+ declare function usePageViews(): PageView[];
313
+ /**
314
+ * Returns a live record of cumulative milliseconds spent per path.
315
+ */
316
+ declare function useTimeSpent(): Record<string, number>;
317
+
318
+ export { UserTrackerProvider, type UserTrackerProviderProps, useHeatmapData, usePageView, usePageViews, useTimeSpent, useTracker };
@@ -0,0 +1,2 @@
1
+ "use strict";var V=Object.create;var l=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,_=Object.prototype.hasOwnProperty;var F=(n,t)=>{for(var e in t)l(n,e,{get:t[e],enumerable:!0})},S=(n,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of M(t))!_.call(n,r)&&r!==e&&l(n,r,{get:()=>t[r],enumerable:!(i=B(t,r))||i.enumerable});return n};var K=(n,t,e)=>(e=n!=null?V(j(n)):{},S(t||!n||!n.__esModule?l(e,"default",{value:n,enumerable:!0}):e,n)),z=n=>S(l({},"__esModule",{value:!0}),n);var q={};F(q,{UserTrackerProvider:()=>C,useHeatmapData:()=>N,usePageView:()=>O,usePageViews:()=>U,useTimeSpent:()=>D,useTracker:()=>H});module.exports=z(q);var k=require("react");function T(){return typeof crypto!="undefined"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{let t=Math.random()*16|0;return(n==="x"?t:t&3|8).toString(16)})}async function I(){try{let n=await fetch("https://ipapi.co/json/",{method:"GET",headers:{Accept:"application/json"}});if(!n.ok)return null;let t=await n.json();return t.error?null:{country:typeof t.country_code=="string"?t.country_code:"",countryName:typeof t.country_name=="string"?t.country_name:"",city:typeof t.city=="string"?t.city:void 0,region:typeof t.region=="string"?t.region:void 0,latitude:typeof t.latitude=="number"?t.latitude:void 0,longitude:typeof t.longitude=="number"?t.longitude:void 0}}catch(n){return null}}var u=class{constructor({emit:t,sessionId:e}){this.previousPath="";this.originalPushState=null;this.originalReplaceState=null;this.handlePopState=()=>{this.handleNavigation()};this.emit=t,this.sessionId=e}init(){this.recordPageView(window.location.pathname+window.location.search),window.addEventListener("popstate",this.handlePopState),this.originalPushState=history.pushState.bind(history);let t=this.originalPushState;history.pushState=(i,r,s)=>{t(i,r,s),this.handleNavigation()},this.originalReplaceState=history.replaceState.bind(history);let e=this.originalReplaceState;history.replaceState=(i,r,s)=>{e(i,r,s),this.handleNavigation()}}destroy(){window.removeEventListener("popstate",this.handlePopState),this.originalPushState&&(history.pushState=this.originalPushState),this.originalReplaceState&&(history.replaceState=this.originalReplaceState)}handleNavigation(){let t=window.location.pathname+window.location.search;t!==this.previousPath&&this.recordPageView(t)}recordPageView(t){this.previousPath=t,this.emit({type:"pageview",data:{path:t,title:document.title,timestamp:Date.now(),sessionId:this.sessionId,referrer:document.referrer||void 0}}),window.dispatchEvent(new CustomEvent("tracker:navigate",{detail:{path:t,title:document.title}}))}};var g=class{constructor({emit:t,sessionId:e}){this.currentPath="";this.startTime=0;this.tracking=!1;this.handleNavigate=t=>{this.stopTracking(),this.currentPath=t.detail.path,this.startTracking()};this.handleVisibilityChange=()=>{document.hidden?this.stopTracking():this.startTracking()};this.handleUnload=()=>{this.stopTracking()};this.emit=t,this.sessionId=e}init(){this.currentPath=window.location.pathname+window.location.search,this.startTracking(),window.addEventListener("tracker:navigate",this.handleNavigate),document.addEventListener("visibilitychange",this.handleVisibilityChange),window.addEventListener("beforeunload",this.handleUnload),window.addEventListener("pagehide",this.handleUnload)}destroy(){this.stopTracking(),window.removeEventListener("tracker:navigate",this.handleNavigate),document.removeEventListener("visibilitychange",this.handleVisibilityChange),window.removeEventListener("beforeunload",this.handleUnload),window.removeEventListener("pagehide",this.handleUnload)}startTracking(){this.startTime=Date.now(),this.tracking=!0}stopTracking(){if(!this.tracking||!this.currentPath)return;let t=Date.now()-this.startTime;if(t<100){this.tracking=!1;return}this.emit({type:"timespent",data:{path:this.currentPath,duration:t,sessionId:this.sessionId,timestamp:Date.now()}}),this.tracking=!1}};function P(n,t){let e=0;return(...i)=>{let r=Date.now();r-e>=t&&(e=r,n(...i))}}var m=class{constructor({emit:t,sessionId:e,sampleRate:i=.3,maxPoints:r=2e3}){this.currentPath="";this.pointCounts={};this.handleMouseMove=t=>{if(Math.random()>this.sampleRate)return;let e=document.documentElement.scrollWidth,i=document.documentElement.scrollHeight,r=t.clientY+window.scrollY;this.recordPoint({x:t.clientX,y:r,xPct:e>0?t.clientX/e*100:0,yPct:i>0?r/i*100:0,type:"move"})};this.handleClick=t=>{let e=document.documentElement.scrollWidth,i=document.documentElement.scrollHeight,r=t.clientY+window.scrollY;this.recordPoint({x:t.clientX,y:r,xPct:e>0?t.clientX/e*100:0,yPct:i>0?r/i*100:0,type:"click"})};this.handleScroll=()=>{if(Math.random()>this.sampleRate)return;let t=document.documentElement.scrollWidth,e=document.documentElement.scrollHeight,i=window.innerWidth,r=window.innerHeight,s=window.scrollX+i/2,o=window.scrollY+r/2;this.recordPoint({x:i/2,y:o,xPct:t>0?s/t*100:0,yPct:e>0?o/e*100:0,type:"scroll"})};this.handleNavigate=t=>{this.currentPath=t.detail.path};this.emit=t,this.sessionId=e,this.sampleRate=i,this.maxPoints=r,this.throttledMouseMove=P(this.handleMouseMove,50),this.throttledScroll=P(this.handleScroll,100)}init(){this.currentPath=window.location.pathname+window.location.search,document.addEventListener("mousemove",this.throttledMouseMove),document.addEventListener("click",this.handleClick),window.addEventListener("scroll",this.throttledScroll,{passive:!0}),window.addEventListener("tracker:navigate",this.handleNavigate)}destroy(){document.removeEventListener("mousemove",this.throttledMouseMove),document.removeEventListener("click",this.handleClick),window.removeEventListener("scroll",this.throttledScroll),window.removeEventListener("tracker:navigate",this.handleNavigate)}canRecord(){var t;return((t=this.pointCounts[this.currentPath])!=null?t:0)<this.maxPoints}recordPoint(t){var e;this.canRecord()&&(this.pointCounts[this.currentPath]=((e=this.pointCounts[this.currentPath])!=null?e:0)+1,this.emit({type:"heatmap",data:{...t,path:this.currentPath,timestamp:Date.now()}}))}};var v=class{constructor(t){this.prevOnError=null;this.prevOnUnhandledRejection=null;this.initialized=!1;try{let e=new URL(t.endpoint),i=e.pathname.replace(/\/$/,"").split("/");i.pop(),e.pathname=i.join("/")||"/",this.endpoint=e.toString().replace(/\/$/,"")}catch(e){this.endpoint=t.endpoint}this.sessionId=t.sessionId,this.appId=t.appId,this.authHeaders=t.secretKey?{Authorization:`Bearer ${t.secretKey}`}:{}}init(){typeof window=="undefined"||this.initialized||(this.origInfo=console.info.bind(console),this.origWarn=console.warn.bind(console),this.origError=console.error.bind(console),console.info=(...t)=>{this.origInfo(...t),this.send("info",this.format(t))},console.warn=(...t)=>{this.origWarn(...t),this.send("warn",this.format(t))},console.error=(...t)=>{this.origError(...t);let[e]=t,i=e instanceof Error?e.stack:void 0;this.send("error",this.format(t),{stack:i})},this.prevOnError=window.onerror,window.onerror=(t,e,i,r,s)=>(this.send("error",String(t),{stack:s==null?void 0:s.stack,meta:{src:e,line:i,col:r}}),typeof this.prevOnError=="function"?this.prevOnError(t,e,i,r,s):!1),this.prevOnUnhandledRejection=t=>{let e=t.reason,i=e instanceof Error?e.message:String(e!=null?e:"Unhandled promise rejection");this.send("error",i,{stack:e instanceof Error?e.stack:void 0})},window.addEventListener("unhandledrejection",this.prevOnUnhandledRejection),this.initialized=!0)}destroy(){this.initialized&&(console.info=this.origInfo,console.warn=this.origWarn,console.error=this.origError,window.onerror=this.prevOnError,this.prevOnUnhandledRejection&&window.removeEventListener("unhandledrejection",this.prevOnUnhandledRejection),this.initialized=!1)}capture(t,e,i){this.send(t,e,i)}format(t){return t.map(e=>{if(e instanceof Error)return e.message;if(typeof e=="object")try{return JSON.stringify(e)}catch(i){return String(e)}return String(e)}).join(" ")}send(t,e,i){let r={sessionId:this.sessionId,...this.appId?{appId:this.appId}:{},level:t,message:e,url:typeof window!="undefined"?window.location.href:void 0,stack:i==null?void 0:i.stack,meta:i==null?void 0:i.meta,timestamp:Date.now()},s=`${this.endpoint}/logs/ingest`,o=JSON.stringify(r);fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.authHeaders},body:o,keepalive:!0}).catch(d=>{this.origError&&this.origError("[user-tracker] Failed to send log:",d)})}};var R=K(require("html2canvas")),A=300*1e3,x="__ut_snap_ts__",f=class{constructor(t){this.lastSentPerPath={};var e;this.snapshotUrl=t.endpoint.replace(/\/events$/,"/snapshots"),this.appId=t.appId,this.secretKey=t.secretKey,this.intervalMs=(e=t.intervalMs)!=null?e:A;try{let i=localStorage.getItem(x);i&&(this.lastSentPerPath=JSON.parse(i))}catch(i){}}async capture(t){var i;if(typeof window=="undefined")return;let e=(i=this.lastSentPerPath[t])!=null?i:0;if(!(Date.now()-e<this.intervalMs))try{let r=document.documentElement,s=getComputedStyle(r).backgroundColor||getComputedStyle(document.body).backgroundColor||"#ffffff",o=await(0,R.default)(r,{allowTaint:!0,useCORS:!0,logging:!1,scale:window.devicePixelRatio||1,width:r.scrollWidth,height:r.scrollHeight,windowWidth:window.innerWidth,windowHeight:window.innerHeight,scrollX:0,scrollY:0,x:0,y:0,backgroundColor:s,foreignObjectRendering:!1,removeContainer:!0}),d=await new Promise(E=>o.toBlob(E,"image/png"));if(!d)return;let c=new FormData;c.append("screenshot",d,"screenshot.png"),c.append("path",t),c.append("width",String(r.scrollWidth)),c.append("height",String(r.scrollHeight)),this.appId&&c.append("appId",this.appId);let p={};this.secretKey&&(p.Authorization=`Bearer ${this.secretKey}`),await fetch(this.snapshotUrl,{method:"POST",headers:p,body:c}),this.lastSentPerPath[t]=Date.now();try{localStorage.setItem(x,JSON.stringify(this.lastSentPerPath))}catch(E){}}catch(r){}}destroy(){}};var W="https://api.alphana.ir/api/events",$={endpoint:W,trackNavigation:!0,trackTime:!0,trackHeatmap:!0,trackLogs:!0,trackSnapshots:!0,mouseSampleRate:.3,maxHeatmapPoints:2e3,batchSize:20,flushInterval:5e3},w=class{constructor(t={}){this.initialized=!1;this.subscribers=new Set;this.queue=[];this.flushTimer=null;this.heartbeatTimer=null;this.location=null;this.handleVisibilityChange=()=>{document.visibilityState==="hidden"&&(this.queue.length>0&&this.flushBeacon(),this.sendDeactivate())};this.handlePageHide=()=>{this.queue.length>0&&this.flushBeacon(),this.sendDeactivate()};this.handleNavigate=t=>{var i;let e=(i=t.detail)==null?void 0:i.path;e&&this.snapshot&&this.snapshot.capture(e)};var e;this.cfg={...$,...t};try{new URL(this.cfg.endpoint)}catch(i){throw new Error(`[alpha-tracker] Invalid endpoint URL: "${this.cfg.endpoint}"`)}this.session={id:(e=t.sessionId)!=null?e:T(),startedAt:Date.now(),pageViews:[],timeSpent:{},heatmap:{}}}init(){if(typeof window=="undefined"||this.initialized)return this;let t=this.emit.bind(this),{id:e}=this.session;return this.cfg.trackNavigation&&(this.navigation=new u({emit:t,sessionId:e}),this.navigation.init()),this.cfg.trackTime&&(this.time=new g({emit:t,sessionId:e}),this.time.init()),this.cfg.trackHeatmap&&(this.heatmap=new m({emit:t,sessionId:e,sampleRate:this.cfg.mouseSampleRate,maxPoints:this.cfg.maxHeatmapPoints}),this.heatmap.init()),this.cfg.endpoint&&(this.flushTimer=setInterval(()=>{this.queue.length>0&&this.flush()},this.cfg.flushInterval),I().then(i=>{this.location=i,i&&(this.session.location=i)}),window.addEventListener("visibilitychange",this.handleVisibilityChange),window.addEventListener("pagehide",this.handlePageHide),this.heartbeatTimer=setInterval(()=>{document.visibilityState!=="hidden"&&this.sendHeartbeat()},3e4),this.cfg.trackLogs&&(this.logCapture=new v({endpoint:this.cfg.endpoint,sessionId:this.session.id,secretKey:this.cfg.secretKey,appId:this.cfg.appId}),this.logCapture.init()),this.cfg.trackSnapshots!==!1&&(this.snapshot=new f({endpoint:this.cfg.endpoint,appId:this.cfg.appId,secretKey:this.cfg.secretKey,intervalMs:this.cfg.snapshotIntervalMs}),this.snapshot.capture(window.location.pathname),window.addEventListener("tracker:navigate",this.handleNavigate))),this.initialized=!0,this}destroy(){var t,e,i,r,s;this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.heartbeatTimer!==null&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),typeof window!="undefined"&&(window.removeEventListener("visibilitychange",this.handleVisibilityChange),window.removeEventListener("pagehide",this.handlePageHide)),(t=this.navigation)==null||t.destroy(),(e=this.time)==null||e.destroy(),(i=this.heatmap)==null||i.destroy(),(r=this.logCapture)==null||r.destroy(),(s=this.snapshot)==null||s.destroy(),typeof window!="undefined"&&window.removeEventListener("tracker:navigate",this.handleNavigate),this.queue.length>0&&this.cfg.endpoint&&this.flushBeacon(),this.initialized=!1}async sendHeartbeat(){if(!this.cfg.endpoint)return;let t=`${this.cfg.endpoint}/heartbeat`,e=this.cfg.secretKey?{Authorization:`Bearer ${this.cfg.secretKey}`}:{},i=JSON.stringify({sessionId:this.session.id,path:typeof window!="undefined"?window.location.pathname:"/",active:!0,...this.cfg.appId?{appId:this.cfg.appId}:{},...this.location?{location:this.location}:{}});try{await fetch(t,{method:"POST",headers:{"Content-Type":"application/json",...e},body:i,keepalive:!0})}catch(r){}}sendDeactivate(){if(!this.cfg.endpoint)return;let t=`${this.cfg.endpoint}/heartbeat`,e=this.cfg.secretKey?{Authorization:`Bearer ${this.cfg.secretKey}`}:{},i=JSON.stringify({sessionId:this.session.id,path:typeof window!="undefined"?window.location.pathname:"/",active:!1,...this.cfg.appId?{appId:this.cfg.appId}:{}});typeof navigator!="undefined"&&navigator.sendBeacon?navigator.sendBeacon(t,new Blob([i],{type:"application/json"})):fetch(t,{method:"POST",headers:{"Content-Type":"application/json",...e},body:i,keepalive:!0}).catch(()=>{})}emit(t){var e,i,r;switch(t.type){case"pageview":this.session.pageViews.push(t.data);break;case"timespent":{let s=(e=this.session.timeSpent[t.data.path])!=null?e:0;this.session.timeSpent[t.data.path]=s+t.data.duration;break}case"heatmap":{let s=t.data.path;this.session.heatmap[s]||(this.session.heatmap[s]=[]);let o=this.session.heatmap[s];o.length<this.cfg.maxHeatmapPoints&&o.push(t.data);break}}this.subscribers.forEach(s=>s(t)),(r=(i=this.cfg).onEvent)==null||r.call(i,t),this.cfg.endpoint&&(this.queue.push(t),this.queue.length>=this.cfg.batchSize&&this.flush())}subscribe(t){return this.subscribers.add(t),()=>this.subscribers.delete(t)}trackPageView(t){let e=t!=null?t:typeof window!="undefined"?window.location.pathname+window.location.search:"/";this.emit({type:"pageview",data:{path:e,title:typeof document!="undefined"?document.title:"",timestamp:Date.now(),sessionId:this.session.id,referrer:typeof document!="undefined"&&document.referrer||void 0}}),typeof window!="undefined"&&window.dispatchEvent(new CustomEvent("tracker:navigate",{detail:{path:e,title:document.title}}))}getSession(){return this.session}getPageViews(){return[...this.session.pageViews]}getTimeSpent(){return{...this.session.timeSpent}}getHeatmapData(t){var e;return t!==void 0?[...(e=this.session.heatmap[t])!=null?e:[]]:Object.entries(this.session.heatmap).reduce((i,[r,s])=>(i[r]=[...s],i),{})}async flush(){if(this.queue.length===0)return;let t=this.queue.splice(0);await this.sendBatch(t)}flushBeacon(){if(this.queue.length===0)return;let t=this.queue.splice(0),e=`${this.cfg.endpoint}/batch`,i=new Blob([this.buildBatchBody(t)],{type:"application/json"});typeof navigator!="undefined"&&navigator.sendBeacon?navigator.sendBeacon(e,i):this.sendBatch(t)}buildBatchBody(t){var e;return JSON.stringify({...this.cfg.appId?{appId:this.cfg.appId}:{},location:(e=this.location)!=null?e:void 0,events:t.map(i=>({sessionId:this.session.id,type:i.type,data:i.data}))})}async sendBatch(t){let e=`${this.cfg.endpoint}/batch`,i=this.cfg.secretKey?{Authorization:`Bearer ${this.cfg.secretKey}`}:{};try{await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",...i},body:this.buildBatchBody(t),keepalive:!0})}catch(r){}}};var y=require("react"),b=(0,y.createContext)(null);function h(){return(0,y.useContext)(b)}var L=require("react/jsx-runtime");function C({config:n={},children:t}){let e=(0,k.useRef)(null);return e.current===null&&(e.current=new w(n)),(0,k.useEffect)(()=>{let i=e.current;return i.init(),()=>i.destroy()},[]),(0,L.jsx)(b.Provider,{value:e.current,children:t})}var a=require("react");function H(){return h()}function O(n){let t=h();(0,a.useEffect)(()=>{t&&n!==void 0&&t.trackPageView(n)},[n])}function N(n,t=500){let e=h(),[i,r]=(0,a.useState)([]),s=(0,a.useRef)(!1);return(0,a.useEffect)(()=>{if(!e)return;let o=n!=null?n:typeof window!="undefined"?window.location.pathname:"/",d=()=>{r(e.getHeatmapData(o)),s.current=!1};return d(),e.subscribe(p=>{p.type==="heatmap"&&p.data.path===o&&(s.current||(s.current=!0,setTimeout(d,t)))})},[e,n]),i}function U(){let n=h(),[t,e]=(0,a.useState)([]);return(0,a.useEffect)(()=>n?(e(n.getPageViews()),n.subscribe(r=>{r.type==="pageview"&&e(n.getPageViews())})):void 0,[n]),t}function D(){let n=h(),[t,e]=(0,a.useState)({});return(0,a.useEffect)(()=>n?(e(n.getTimeSpent()),n.subscribe(r=>{r.type==="timespent"&&e(n.getTimeSpent())})):void 0,[n]),t}0&&(module.exports={UserTrackerProvider,useHeatmapData,usePageView,usePageViews,useTimeSpent,useTracker});
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/index.ts","../../src/react/provider.tsx","../../src/utils/session.ts","../../src/utils/geo.ts","../../src/core/navigation.ts","../../src/core/time.ts","../../src/utils/throttle.ts","../../src/core/heatmap.ts","../../src/core/logger.ts","../../src/core/snapshot.ts","../../src/tracker.ts","../../src/react/context.tsx","../../src/react/hooks.ts"],"sourcesContent":["export { UserTrackerProvider } from \"./provider\";\nexport type { UserTrackerProviderProps } from \"./provider\";\nexport {\n useTracker,\n usePageView,\n useHeatmapData,\n usePageViews,\n useTimeSpent,\n} from \"./hooks\";\n","import { useEffect, useRef, type ReactNode } from \"react\";\nimport { UserTracker } from \"../tracker\";\nimport type { TrackerConfig } from \"../types\";\nimport { TrackerContext } from \"./context\";\n\nexport interface UserTrackerProviderProps {\n /** Tracker configuration. Captured on first render — changes are ignored. */\n config?: TrackerConfig;\n children: ReactNode;\n}\n\n/**\n * Wraps your application (or a subtree) and provides a `UserTracker` instance\n * via React context.\n *\n * The tracker is created once, initialized on mount, and destroyed on unmount.\n *\n * **Next.js App Router** — mark your layout wrapper as a Client Component:\n * ```tsx\n * 'use client';\n * import { UserTrackerProvider } from 'user-tracker/react';\n * export default function RootLayout({ children }) {\n * return <UserTrackerProvider config={{ endpoint: '/api/events' }}>{children}</UserTrackerProvider>;\n * }\n * ```\n */\nexport function UserTrackerProvider({\n config = {},\n children,\n}: UserTrackerProviderProps) {\n // Create the tracker instance exactly once (lazy ref initialisation).\n const trackerRef = useRef<UserTracker | null>(null);\n if (trackerRef.current === null) {\n trackerRef.current = new UserTracker(config);\n }\n\n useEffect(() => {\n const tracker = trackerRef.current!;\n tracker.init();\n return () => tracker.destroy();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return (\n <TrackerContext.Provider value={trackerRef.current}>\n {children}\n </TrackerContext.Provider>\n );\n}\n","const SESSION_STORAGE_KEY = \"__ut_sid__\";\n\n/** Generate a RFC-4122 v4 UUID using the native crypto API with a fallback. */\nexport function generateSessionId(): string {\n if (\n typeof crypto !== \"undefined\" &&\n typeof crypto.randomUUID === \"function\"\n ) {\n return crypto.randomUUID();\n }\n // Math.random fallback (not cryptographically secure, but sufficient for analytics)\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Retrieve the session ID from sessionStorage, or create and persist a new one.\n * Falls back to an in-memory ID when sessionStorage is unavailable (e.g. SSR).\n */\nexport function getOrCreateSessionId(): string {\n if (typeof sessionStorage === \"undefined\") return generateSessionId();\n try {\n const existing = sessionStorage.getItem(SESSION_STORAGE_KEY);\n if (existing) return existing;\n const id = generateSessionId();\n sessionStorage.setItem(SESSION_STORAGE_KEY, id);\n return id;\n } catch {\n return generateSessionId();\n }\n}\n","import type { GeoLocation } from \"../types\";\n\n/**\n * Resolves the visitor's approximate location from their public IP address\n * using the ipapi.co free-tier JSON endpoint (no API-key required, up to\n * 1 000 requests/day on the free plan).\n *\n * Runs silently — returns `null` on any network error, rate-limit, or\n * reserved/private IP so that tracking is never blocked.\n */\nexport async function fetchLocation(): Promise<GeoLocation | null> {\n try {\n const res = await fetch(\"https://ipapi.co/json/\", {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n });\n if (!res.ok) return null;\n const d = (await res.json()) as Record<string, unknown>;\n // ipapi returns { \"error\": true, \"reason\": \"...\" } for private/reserved IPs\n if (d[\"error\"]) return null;\n return {\n country: typeof d[\"country_code\"] === \"string\" ? d[\"country_code\"] : \"\",\n countryName:\n typeof d[\"country_name\"] === \"string\" ? d[\"country_name\"] : \"\",\n city: typeof d[\"city\"] === \"string\" ? d[\"city\"] : undefined,\n region: typeof d[\"region\"] === \"string\" ? d[\"region\"] : undefined,\n latitude: typeof d[\"latitude\"] === \"number\" ? d[\"latitude\"] : undefined,\n longitude:\n typeof d[\"longitude\"] === \"number\" ? d[\"longitude\"] : undefined,\n };\n } catch {\n return null;\n }\n}\n","import type { TrackerEvent } from \"../types\";\n\ntype EmitFn = (event: TrackerEvent) => void;\n\ninterface NavigationPluginOptions {\n emit: EmitFn;\n sessionId: string;\n}\n\n/**\n * Tracks SPA route changes by monkey-patching history.pushState /\n * history.replaceState and listening to the popstate event.\n *\n * For every navigation it:\n * 1. Emits a `pageview` event.\n * 2. Dispatches the custom DOM event `tracker:navigate` so that other\n * plugins (TimePlugin, HeatmapPlugin) can react without having to\n * duplicate the pushState patching.\n *\n * Next.js App Router note:\n * The App Router manages navigation internally; use `usePageView(pathname)`\n * from `user-tracker/react` together with `usePathname()` instead.\n */\nexport class NavigationPlugin {\n private readonly emit: EmitFn;\n private readonly sessionId: string;\n private previousPath = \"\";\n private originalPushState: typeof history.pushState | null = null;\n private originalReplaceState: typeof history.replaceState | null = null;\n\n constructor({ emit, sessionId }: NavigationPluginOptions) {\n this.emit = emit;\n this.sessionId = sessionId;\n }\n\n init(): void {\n // Record the initial page view on load.\n this.recordPageView(window.location.pathname + window.location.search);\n\n window.addEventListener(\"popstate\", this.handlePopState);\n\n // Patch pushState\n this.originalPushState = history.pushState.bind(history);\n const origPush = this.originalPushState;\n history.pushState = (state, title, url): void => {\n origPush(state, title, url);\n this.handleNavigation();\n };\n\n // Patch replaceState\n this.originalReplaceState = history.replaceState.bind(history);\n const origReplace = this.originalReplaceState;\n history.replaceState = (state, title, url): void => {\n origReplace(state, title, url);\n this.handleNavigation();\n };\n }\n\n destroy(): void {\n window.removeEventListener(\"popstate\", this.handlePopState);\n if (this.originalPushState) history.pushState = this.originalPushState;\n if (this.originalReplaceState)\n history.replaceState = this.originalReplaceState;\n }\n\n // Arrow property → always bound to `this`, safe to use as event listener.\n private handlePopState = (): void => {\n this.handleNavigation();\n };\n\n private handleNavigation(): void {\n const path = window.location.pathname + window.location.search;\n if (path === this.previousPath) return; // hash-only or duplicate call\n this.recordPageView(path);\n }\n\n private recordPageView(path: string): void {\n this.previousPath = path;\n\n this.emit({\n type: \"pageview\",\n data: {\n path,\n title: document.title,\n timestamp: Date.now(),\n sessionId: this.sessionId,\n referrer: document.referrer || undefined,\n },\n });\n\n // Notify other plugins via a custom DOM event (synchronous dispatch).\n window.dispatchEvent(\n new CustomEvent(\"tracker:navigate\", {\n detail: { path, title: document.title },\n }),\n );\n }\n}\n","import type { TrackerEvent } from \"../types\";\n\ntype EmitFn = (event: TrackerEvent) => void;\n\ninterface TimePluginOptions {\n emit: EmitFn;\n sessionId: string;\n}\n\n/**\n * Tracks the time a user spends on each page.\n *\n * - Starts a timer when the page becomes active (init / tab focus).\n * - Stops and emits a `timespent` event when:\n * • The user navigates away (tracker:navigate)\n * • The tab is hidden (visibilitychange)\n * • The page is unloading (beforeunload / pagehide)\n * - Resumes timing when the tab becomes visible again.\n */\nexport class TimePlugin {\n private readonly emit: EmitFn;\n private readonly sessionId: string;\n private currentPath = \"\";\n private startTime = 0;\n private tracking = false;\n\n constructor({ emit, sessionId }: TimePluginOptions) {\n this.emit = emit;\n this.sessionId = sessionId;\n }\n\n init(): void {\n this.currentPath = window.location.pathname + window.location.search;\n this.startTracking();\n\n window.addEventListener(\"tracker:navigate\", this.handleNavigate);\n document.addEventListener(\"visibilitychange\", this.handleVisibilityChange);\n window.addEventListener(\"beforeunload\", this.handleUnload);\n window.addEventListener(\"pagehide\", this.handleUnload);\n }\n\n destroy(): void {\n this.stopTracking();\n window.removeEventListener(\"tracker:navigate\", this.handleNavigate);\n document.removeEventListener(\n \"visibilitychange\",\n this.handleVisibilityChange,\n );\n window.removeEventListener(\"beforeunload\", this.handleUnload);\n window.removeEventListener(\"pagehide\", this.handleUnload);\n }\n\n private startTracking(): void {\n this.startTime = Date.now();\n this.tracking = true;\n }\n\n private stopTracking(): void {\n if (!this.tracking || !this.currentPath) return;\n const duration = Date.now() - this.startTime;\n if (duration < 100) {\n this.tracking = false;\n return; // Ignore sub-100 ms blips (e.g. rapid navigations).\n }\n this.emit({\n type: \"timespent\",\n data: {\n path: this.currentPath,\n duration,\n sessionId: this.sessionId,\n timestamp: Date.now(),\n },\n });\n this.tracking = false;\n }\n\n private handleNavigate = (e: CustomEvent<{ path: string }>): void => {\n this.stopTracking();\n this.currentPath = e.detail.path;\n this.startTracking();\n };\n\n private handleVisibilityChange = (): void => {\n if (document.hidden) {\n this.stopTracking();\n } else {\n this.startTracking();\n }\n };\n\n private handleUnload = (): void => {\n this.stopTracking();\n };\n}\n","/**\n * Returns a function that invokes `fn` at most once every `delay` ms.\n * The first call in a new window is executed immediately.\n */\nexport function throttle<Args extends unknown[]>(\n fn: (...args: Args) => void,\n delay: number,\n): (...args: Args) => void {\n let lastCall = 0;\n return (...args: Args): void => {\n const now = Date.now();\n if (now - lastCall >= delay) {\n lastCall = now;\n fn(...args);\n }\n };\n}\n","import type { TrackerEvent, HeatmapPoint } from \"../types\";\nimport { throttle } from \"../utils/throttle\";\n\ntype EmitFn = (event: TrackerEvent) => void;\n\ninterface HeatmapPluginOptions {\n emit: EmitFn;\n sessionId: string;\n /** Fraction of mousemove / scroll events to sample (0–1). Default: 0.3 */\n sampleRate?: number;\n /** Maximum points stored per page before recording stops. Default: 2000 */\n maxPoints?: number;\n}\n\n/**\n * Collects mouse-move, click, and scroll positions for heatmap analysis.\n *\n * Coordinates are stored both as absolute pixels and as percentages of the\n * full page dimensions so data stays meaningful across different screen sizes.\n *\n * Mouse moves and scroll events are throttled (50 ms) and further reduced by\n * the configurable `sampleRate`. Clicks are never sampled — each one is always\n * recorded (up to `maxPoints`).\n */\nexport class HeatmapPlugin {\n private readonly emit: EmitFn;\n private readonly sessionId: string;\n private readonly sampleRate: number;\n private readonly maxPoints: number;\n private currentPath = \"\";\n private pointCounts: Record<string, number> = {};\n\n private readonly throttledMouseMove: (e: MouseEvent) => void;\n private readonly throttledScroll: () => void;\n\n constructor({\n emit,\n sessionId,\n sampleRate = 0.3,\n maxPoints = 2000,\n }: HeatmapPluginOptions) {\n this.emit = emit;\n this.sessionId = sessionId;\n this.sampleRate = sampleRate;\n this.maxPoints = maxPoints;\n\n this.throttledMouseMove = throttle(this.handleMouseMove, 50);\n this.throttledScroll = throttle(this.handleScroll, 100);\n }\n\n init(): void {\n this.currentPath = window.location.pathname + window.location.search;\n\n document.addEventListener(\"mousemove\", this.throttledMouseMove);\n document.addEventListener(\"click\", this.handleClick);\n window.addEventListener(\"scroll\", this.throttledScroll, { passive: true });\n window.addEventListener(\"tracker:navigate\", this.handleNavigate);\n }\n\n destroy(): void {\n document.removeEventListener(\"mousemove\", this.throttledMouseMove);\n document.removeEventListener(\"click\", this.handleClick);\n window.removeEventListener(\"scroll\", this.throttledScroll);\n window.removeEventListener(\"tracker:navigate\", this.handleNavigate);\n }\n\n private canRecord(): boolean {\n return (this.pointCounts[this.currentPath] ?? 0) < this.maxPoints;\n }\n\n private recordPoint(\n point: Omit<HeatmapPoint, \"path\" | \"timestamp\" | \"sessionId\">,\n ): void {\n if (!this.canRecord()) return;\n this.pointCounts[this.currentPath] =\n (this.pointCounts[this.currentPath] ?? 0) + 1;\n this.emit({\n type: \"heatmap\",\n data: {\n ...point,\n path: this.currentPath,\n timestamp: Date.now(),\n },\n });\n }\n\n private handleMouseMove = (e: MouseEvent): void => {\n if (Math.random() > this.sampleRate) return;\n const pageWidth = document.documentElement.scrollWidth;\n const pageHeight = document.documentElement.scrollHeight;\n const absY = e.clientY + window.scrollY;\n this.recordPoint({\n x: e.clientX,\n y: absY,\n xPct: pageWidth > 0 ? (e.clientX / pageWidth) * 100 : 0,\n yPct: pageHeight > 0 ? (absY / pageHeight) * 100 : 0,\n type: \"move\",\n });\n };\n\n private handleClick = (e: MouseEvent): void => {\n const pageWidth = document.documentElement.scrollWidth;\n const pageHeight = document.documentElement.scrollHeight;\n const absY = e.clientY + window.scrollY;\n this.recordPoint({\n x: e.clientX,\n y: absY,\n xPct: pageWidth > 0 ? (e.clientX / pageWidth) * 100 : 0,\n yPct: pageHeight > 0 ? (absY / pageHeight) * 100 : 0,\n type: \"click\",\n });\n };\n\n private handleScroll = (): void => {\n if (Math.random() > this.sampleRate) return;\n const pageWidth = document.documentElement.scrollWidth;\n const pageHeight = document.documentElement.scrollHeight;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n // Record the centre of the visible viewport.\n const centerX = window.scrollX + vw / 2;\n const centerY = window.scrollY + vh / 2;\n this.recordPoint({\n x: vw / 2,\n y: centerY,\n xPct: pageWidth > 0 ? (centerX / pageWidth) * 100 : 0,\n yPct: pageHeight > 0 ? (centerY / pageHeight) * 100 : 0,\n type: \"scroll\",\n });\n };\n\n private handleNavigate = (e: CustomEvent<{ path: string }>): void => {\n this.currentPath = e.detail.path;\n };\n}\n","export type LogLevel = \"info\" | \"warn\" | \"error\";\n\nexport interface LogEntry {\n sessionId?: string;\n appId?: string;\n level: LogLevel;\n message: string;\n url?: string;\n stack?: string;\n meta?: Record<string, unknown>;\n timestamp: number;\n}\n\ntype ConsoleFn = (...args: unknown[]) => void;\n\n/**\n * Automatically captures console.info/warn/error output and unhandled errors,\n * then ships them to the backend `/logs/ingest` endpoint.\n *\n * All console methods are restored exactly in `destroy()`.\n */\nexport class LogCapture {\n private readonly endpoint: string;\n private readonly sessionId: string;\n private readonly appId?: string;\n private readonly authHeaders: Record<string, string>;\n\n // Original console methods preserved so we can restore them.\n private origInfo!: ConsoleFn;\n private origWarn!: ConsoleFn;\n private origError!: ConsoleFn;\n\n private prevOnError: OnErrorEventHandler = null;\n private prevOnUnhandledRejection:\n | ((e: PromiseRejectionEvent) => void)\n | null = null;\n\n private initialized = false;\n\n constructor(options: {\n endpoint: string;\n sessionId: string;\n secretKey?: string;\n appId?: string;\n }) {\n // Derive the API base URL by stripping everything from the last path\n // segment that isn't a versioning prefix. The tracker config `endpoint`\n // is the *events* URL (e.g. http://host/api/events), but logs live at\n // http://host/api/logs/ingest, so we walk up until we reach the common\n // base (i.e. remove the final segment).\n try {\n const u = new URL(options.endpoint);\n // Remove the last non-empty path segment (e.g. \"/api/events\" → \"/api\")\n const parts = u.pathname.replace(/\\/$/, \"\").split(\"/\");\n parts.pop();\n u.pathname = parts.join(\"/\") || \"/\";\n this.endpoint = u.toString().replace(/\\/$/, \"\");\n } catch {\n this.endpoint = options.endpoint;\n }\n this.sessionId = options.sessionId;\n this.appId = options.appId;\n this.authHeaders = options.secretKey\n ? { Authorization: `Bearer ${options.secretKey}` }\n : {};\n }\n\n init(): void {\n if (typeof window === \"undefined\" || this.initialized) return;\n\n this.origInfo = console.info.bind(console);\n this.origWarn = console.warn.bind(console);\n this.origError = console.error.bind(console);\n\n console.info = (...args: unknown[]) => {\n this.origInfo(...args);\n this.send(\"info\", this.format(args));\n };\n\n console.warn = (...args: unknown[]) => {\n this.origWarn(...args);\n this.send(\"warn\", this.format(args));\n };\n\n console.error = (...args: unknown[]) => {\n this.origError(...args);\n const [first] = args;\n const stack = first instanceof Error ? first.stack : undefined;\n this.send(\"error\", this.format(args), { stack });\n };\n\n this.prevOnError = window.onerror;\n window.onerror = (msg, src, line, col, err) => {\n this.send(\"error\", String(msg), {\n stack: err?.stack,\n meta: { src, line, col },\n });\n if (typeof this.prevOnError === \"function\") {\n return this.prevOnError(msg, src, line, col, err);\n }\n return false;\n };\n\n this.prevOnUnhandledRejection = (e: PromiseRejectionEvent) => {\n const reason = e.reason;\n const message =\n reason instanceof Error\n ? reason.message\n : String(reason ?? \"Unhandled promise rejection\");\n this.send(\"error\", message, {\n stack: reason instanceof Error ? reason.stack : undefined,\n });\n };\n window.addEventListener(\n \"unhandledrejection\",\n this.prevOnUnhandledRejection,\n );\n\n this.initialized = true;\n }\n\n destroy(): void {\n if (!this.initialized) return;\n console.info = this.origInfo;\n console.warn = this.origWarn;\n console.error = this.origError;\n\n window.onerror = this.prevOnError;\n if (this.prevOnUnhandledRejection) {\n window.removeEventListener(\n \"unhandledrejection\",\n this.prevOnUnhandledRejection,\n );\n }\n this.initialized = false;\n }\n\n /** Manually capture a log entry (e.g. from try/catch). */\n capture(\n level: LogLevel,\n message: string,\n extra?: { stack?: string; meta?: Record<string, unknown> },\n ): void {\n this.send(level, message, extra);\n }\n\n private format(args: unknown[]): string {\n return args\n .map((a) => {\n if (a instanceof Error) return a.message;\n if (typeof a === \"object\") {\n try {\n return JSON.stringify(a);\n } catch {\n return String(a);\n }\n }\n return String(a);\n })\n .join(\" \");\n }\n\n private send(\n level: LogLevel,\n message: string,\n extra?: { stack?: string; meta?: Record<string, unknown> },\n ): void {\n const entry: LogEntry = {\n sessionId: this.sessionId,\n ...(this.appId ? { appId: this.appId } : {}),\n level,\n message,\n url: typeof window !== \"undefined\" ? window.location.href : undefined,\n stack: extra?.stack,\n meta: extra?.meta,\n timestamp: Date.now(),\n };\n const url = `${this.endpoint}/logs/ingest`;\n const body = JSON.stringify(entry);\n\n // Use fetch with keepalive so the request survives page navigation.\n // Errors are logged to the (original, unpatched) console so they are\n // visible in DevTools without creating an infinite log loop.\n void fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...this.authHeaders },\n body,\n keepalive: true,\n }).catch((err: unknown) => {\n // Use the original (pre-patch) error logger to avoid recursion.\n if (this.origError) {\n this.origError(\"[user-tracker] Failed to send log:\", err);\n }\n });\n }\n}\n","import html2canvas from \"html2canvas\";\n\n/**\n * SnapshotPlugin\n *\n * Captures a full-page screenshot via html2canvas and POSTs it to the backend\n * snapshot endpoint once every 5 minutes per page (configurable).\n *\n * Timestamps are persisted in localStorage so the throttle survives\n * page reloads.\n */\n\nconst DEFAULT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\nconst STORAGE_KEY = \"__ut_snap_ts__\";\n\nexport class SnapshotPlugin {\n private lastSentPerPath: Record<string, number> = {};\n private readonly snapshotUrl: string;\n private readonly appId?: string;\n private readonly secretKey?: string;\n private readonly intervalMs: number;\n\n constructor(cfg: {\n /** Base events endpoint, e.g. \"https://api.example.com/api/events\" */\n endpoint: string;\n appId?: string;\n secretKey?: string;\n /**\n * Minimum milliseconds between screenshots for the same path.\n * Defaults to 300 000 (5 minutes).\n */\n intervalMs?: number;\n }) {\n // Derive the snapshot endpoint from the events endpoint.\n this.snapshotUrl = cfg.endpoint.replace(/\\/events$/, \"/snapshots\");\n this.appId = cfg.appId;\n this.secretKey = cfg.secretKey;\n this.intervalMs = cfg.intervalMs ?? DEFAULT_INTERVAL_MS;\n\n try {\n const stored = localStorage.getItem(STORAGE_KEY);\n if (stored)\n this.lastSentPerPath = JSON.parse(stored) as Record<string, number>;\n } catch {\n // localStorage may be unavailable in some contexts — proceed without it.\n }\n }\n\n /**\n * Capture and send a screenshot for the given path.\n * No-ops if a screenshot was already sent within the last 5 minutes.\n */\n async capture(path: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const last = this.lastSentPerPath[path] ?? 0;\n if (Date.now() - last < this.intervalMs) return;\n\n try {\n // Capture the <html> element so backgrounds set on :root / html are included.\n const root = document.documentElement;\n\n // Use the page's own background colour so sections that use\n // `background: transparent` don't fall back to white.\n const pageBg =\n getComputedStyle(root).backgroundColor ||\n getComputedStyle(document.body).backgroundColor ||\n \"#ffffff\";\n\n const canvas = await html2canvas(root, {\n allowTaint: true,\n useCORS: true,\n logging: false,\n // Full device-pixel-ratio so retina screens stay sharp.\n scale: window.devicePixelRatio || 1,\n // Full page dimensions.\n width: root.scrollWidth,\n height: root.scrollHeight,\n // Use real viewport dimensions so CSS units like dvh/vh are resolved\n // correctly. Setting these to scrollHeight would make 100dvh sections\n // expand to the full page height in the capture.\n windowWidth: window.innerWidth,\n windowHeight: window.innerHeight,\n // Capture from the very top-left regardless of current scroll position.\n scrollX: 0,\n scrollY: 0,\n x: 0,\n y: 0,\n // Provide the resolved background colour so transparent areas are filled correctly.\n backgroundColor: pageBg,\n // foreignObjectRendering causes artifacts in many frameworks — keep off.\n foreignObjectRendering: false,\n // Remove the temporary off-screen clone after rendering.\n removeContainer: true,\n });\n\n // Use a Promise wrapper because toBlob is callback-based.\n const blob = await new Promise<Blob | null>((resolve) =>\n canvas.toBlob(resolve, \"image/png\"),\n );\n if (!blob) return;\n\n const form = new FormData();\n form.append(\"screenshot\", blob, \"screenshot.png\");\n form.append(\"path\", path);\n // Report the logical CSS-pixel dimensions (not DPR-scaled canvas size)\n // so heatmap xPct/yPct coordinates remain accurate.\n form.append(\"width\", String(root.scrollWidth));\n form.append(\"height\", String(root.scrollHeight));\n if (this.appId) form.append(\"appId\", this.appId);\n\n const headers: Record<string, string> = {};\n if (this.secretKey) headers.Authorization = `Bearer ${this.secretKey}`;\n\n await fetch(this.snapshotUrl, { method: \"POST\", headers, body: form });\n\n // Persist timestamp so throttle survives page reloads.\n this.lastSentPerPath[path] = Date.now();\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(this.lastSentPerPath));\n } catch {\n // quota exceeded — non-fatal\n }\n } catch {\n // Never propagate — snapshot failures must not affect the tracked site.\n }\n }\n\n destroy(): void {\n // Nothing to clean up — no timers or listeners.\n }\n}\n","import type {\n TrackerConfig,\n TrackerEvent,\n SessionData,\n PageView,\n HeatmapPoint,\n GeoLocation,\n} from \"./types\";\nimport { generateSessionId } from \"./utils/session\";\nimport { fetchLocation } from \"./utils/geo\";\nimport { NavigationPlugin } from \"./core/navigation\";\nimport { TimePlugin } from \"./core/time\";\nimport { HeatmapPlugin } from \"./core/heatmap\";\nimport { LogCapture } from \"./core/logger\";\nimport { SnapshotPlugin } from \"./core/snapshot\";\n\nexport const DEFAULT_ENDPOINT = \"https://api.alphana.ir/api/events\";\n\nconst DEFAULTS = {\n endpoint: DEFAULT_ENDPOINT,\n trackNavigation: true,\n trackTime: true,\n trackHeatmap: true,\n trackLogs: true,\n trackSnapshots: true,\n mouseSampleRate: 0.3,\n maxHeatmapPoints: 2000,\n batchSize: 20,\n flushInterval: 5_000,\n} as const;\n\ntype SubscriberFn = (event: TrackerEvent) => void;\n\n/**\n * Core tracker class. Framework-agnostic — works in any environment that has\n * a browser DOM (React, Next.js Pages Router, Vite, vanilla JS/TS, etc.).\n *\n * Usage:\n * ```ts\n * const tracker = new UserTracker({ endpoint: 'https://my-api.com/events' });\n * tracker.init(); // call once; safe to call in SSR (no-op server-side)\n * ```\n *\n * Destroy when done (e.g. component unmount):\n * ```ts\n * tracker.destroy();\n * ```\n */\ntype ResolvedConfig = Required<\n Pick<\n TrackerConfig,\n | \"endpoint\"\n | \"trackNavigation\"\n | \"trackTime\"\n | \"trackHeatmap\"\n | \"trackLogs\"\n | \"mouseSampleRate\"\n | \"maxHeatmapPoints\"\n | \"batchSize\"\n | \"flushInterval\"\n >\n> &\n TrackerConfig;\n\nexport class UserTracker {\n private readonly cfg: ResolvedConfig;\n private session: SessionData;\n private navigation?: NavigationPlugin;\n private time?: TimePlugin;\n private heatmap?: HeatmapPlugin;\n /** Public so consumers can call logCapture.capture() for manual log entries. */\n logCapture?: LogCapture;\n private snapshot?: SnapshotPlugin;\n private initialized = false;\n private readonly subscribers = new Set<SubscriberFn>();\n\n /** In-memory queue of events waiting to be flushed. */\n private queue: TrackerEvent[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private location: GeoLocation | null = null;\n\n constructor(config: TrackerConfig = {}) {\n this.cfg = { ...DEFAULTS, ...config } as ResolvedConfig;\n\n // Validate endpoint URL up front so the error is thrown at construction\n // time rather than silently failing during a network request.\n try {\n new URL(this.cfg.endpoint);\n } catch {\n throw new Error(\n `[alpha-tracker] Invalid endpoint URL: \"${this.cfg.endpoint}\"`,\n );\n }\n\n this.session = {\n id: config.sessionId ?? generateSessionId(),\n startedAt: Date.now(),\n pageViews: [],\n timeSpent: {},\n heatmap: {},\n };\n }\n\n // ─── Lifecycle ──────────────────────────────────────────────────────────────\n\n /**\n * Attach event listeners and start tracking.\n * Safe to call during SSR — returns `this` immediately if `window` is\n * undefined so it can be chained: `const tracker = new UserTracker(cfg).init()`.\n */\n init(): this {\n if (typeof window === \"undefined\" || this.initialized) return this;\n\n const emit = this.emit.bind(this);\n const { id: sessionId } = this.session;\n\n if (this.cfg.trackNavigation) {\n this.navigation = new NavigationPlugin({ emit, sessionId });\n this.navigation.init();\n }\n\n if (this.cfg.trackTime) {\n this.time = new TimePlugin({ emit, sessionId });\n this.time.init();\n }\n\n if (this.cfg.trackHeatmap) {\n this.heatmap = new HeatmapPlugin({\n emit,\n sessionId,\n sampleRate: this.cfg.mouseSampleRate,\n maxPoints: this.cfg.maxHeatmapPoints,\n });\n this.heatmap.init();\n }\n\n if (this.cfg.endpoint) {\n // Flush on a regular interval — even if the batch threshold isn't hit.\n this.flushTimer = setInterval(() => {\n if (this.queue.length > 0) void this.flush();\n }, this.cfg.flushInterval);\n\n // Resolve visitor location from IP in the background.\n void fetchLocation().then((loc) => {\n this.location = loc;\n if (loc) this.session.location = loc;\n });\n\n // Flush remaining queue when the tab is hidden or the page is unloaded.\n window.addEventListener(\"visibilitychange\", this.handleVisibilityChange);\n window.addEventListener(\"pagehide\", this.handlePageHide);\n\n // Periodic keep-alive heartbeat every 30 s so the backend knows the\n // session is still active and doesn't expire it prematurely.\n this.heartbeatTimer = setInterval(() => {\n if (document.visibilityState !== \"hidden\") void this.sendHeartbeat();\n }, 30_000);\n\n // Auto-capture console logs and unhandled errors.\n if (this.cfg.trackLogs) {\n this.logCapture = new LogCapture({\n endpoint: this.cfg.endpoint,\n sessionId: this.session.id,\n secretKey: this.cfg.secretKey,\n appId: this.cfg.appId,\n });\n this.logCapture.init();\n }\n\n // Screenshot capture for heatmap background.\n if (this.cfg.trackSnapshots !== false) {\n this.snapshot = new SnapshotPlugin({\n endpoint: this.cfg.endpoint,\n appId: this.cfg.appId,\n secretKey: this.cfg.secretKey,\n intervalMs: this.cfg.snapshotIntervalMs,\n });\n // Capture immediately on init.\n void this.snapshot.capture(window.location.pathname);\n // Re-capture after each SPA navigation.\n window.addEventListener(\"tracker:navigate\", this.handleNavigate);\n }\n }\n\n this.initialized = true;\n return this;\n }\n\n /** Remove all event listeners, flush remaining queue, and reset state. */\n destroy(): void {\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n if (this.heartbeatTimer !== null) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\n \"visibilitychange\",\n this.handleVisibilityChange,\n );\n window.removeEventListener(\"pagehide\", this.handlePageHide);\n }\n\n this.navigation?.destroy();\n this.time?.destroy();\n this.heatmap?.destroy();\n this.logCapture?.destroy();\n this.snapshot?.destroy();\n\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"tracker:navigate\", this.handleNavigate);\n }\n\n // Best-effort flush of any remaining queued events.\n if (this.queue.length > 0 && this.cfg.endpoint) {\n this.flushBeacon();\n }\n\n this.initialized = false;\n }\n\n private handleVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") {\n if (this.queue.length > 0) this.flushBeacon();\n this.sendDeactivate();\n }\n };\n\n private handlePageHide = (): void => {\n if (this.queue.length > 0) this.flushBeacon();\n this.sendDeactivate();\n };\n\n private handleNavigate = (e: Event): void => {\n const path = (e as CustomEvent<{ path: string }>).detail?.path;\n if (path && this.snapshot) {\n void this.snapshot.capture(path);\n }\n };\n\n /**\n * Send a keep-alive heartbeat so the backend knows this session is still\n * active. Called every 30 s while the tab is visible.\n */\n private async sendHeartbeat(): Promise<void> {\n if (!this.cfg.endpoint) return;\n const url = `${this.cfg.endpoint}/heartbeat`;\n const authHeaders: Record<string, string> = this.cfg.secretKey\n ? { Authorization: `Bearer ${this.cfg.secretKey}` }\n : {};\n const body = JSON.stringify({\n sessionId: this.session.id,\n path: typeof window !== \"undefined\" ? window.location.pathname : \"/\",\n active: true,\n ...(this.cfg.appId ? { appId: this.cfg.appId } : {}),\n ...(this.location ? { location: this.location } : {}),\n });\n try {\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...authHeaders },\n body,\n keepalive: true,\n });\n } catch {\n // Silent — heartbeat failure should never surface to the user.\n }\n }\n\n /**\n * Send a synchronous beacon to mark this session as inactive.\n * Called on page unload / tab hidden so the dashboard reflects real-time.\n */\n private sendDeactivate(): void {\n if (!this.cfg.endpoint) return;\n const url = `${this.cfg.endpoint}/heartbeat`;\n const authHeaders: Record<string, string> = this.cfg.secretKey\n ? { Authorization: `Bearer ${this.cfg.secretKey}` }\n : {};\n const body = JSON.stringify({\n sessionId: this.session.id,\n path: typeof window !== \"undefined\" ? window.location.pathname : \"/\",\n active: false,\n ...(this.cfg.appId ? { appId: this.cfg.appId } : {}),\n });\n // sendBeacon fires even if the page is being unloaded.\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, new Blob([body], { type: \"application/json\" }));\n } else {\n // Fallback for environments without sendBeacon.\n void fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...authHeaders },\n body,\n keepalive: true,\n }).catch(() => undefined);\n }\n }\n\n // ─── Event pipeline ─────────────────────────────────────────────────────────\n\n /** Emit a tracker event. Also used internally by the plugins. */\n emit(event: TrackerEvent): void {\n // 1 – accumulate into session data\n switch (event.type) {\n case \"pageview\":\n this.session.pageViews.push(event.data);\n break;\n\n case \"timespent\": {\n const prev = this.session.timeSpent[event.data.path] ?? 0;\n this.session.timeSpent[event.data.path] = prev + event.data.duration;\n break;\n }\n\n case \"heatmap\": {\n const key = event.data.path;\n if (!this.session.heatmap[key]) this.session.heatmap[key] = [];\n const pts = this.session.heatmap[key];\n if (pts.length < this.cfg.maxHeatmapPoints) pts.push(event.data);\n break;\n }\n }\n\n // 2 – notify subscribers (used internally by React hooks)\n this.subscribers.forEach((fn) => fn(event));\n\n // 3 – user callback\n this.cfg.onEvent?.(event);\n\n // 4 – enqueue for batched remote sending\n if (this.cfg.endpoint) {\n this.queue.push(event);\n // Auto-flush once the batch size threshold is reached.\n if (this.queue.length >= this.cfg.batchSize) {\n void this.flush();\n }\n }\n }\n\n /**\n * Subscribe to every emitted event. Returns an unsubscribe function.\n *\n * ```ts\n * const unsub = tracker.subscribe(event => console.log(event));\n * // later…\n * unsub();\n * ```\n */\n subscribe(fn: SubscriberFn): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n // ─── Manual tracking helpers ────────────────────────────────────────────────\n\n /**\n * Manually record a page view and dispatch `tracker:navigate`.\n * Required for Next.js App Router — call it inside a `useEffect` that\n * depends on `usePathname()`:\n *\n * ```tsx\n * const pathname = usePathname();\n * useEffect(() => { tracker.trackPageView(pathname); }, [pathname]);\n * ```\n */\n trackPageView(path?: string): void {\n const resolvedPath =\n path ??\n (typeof window !== \"undefined\"\n ? window.location.pathname + window.location.search\n : \"/\");\n\n this.emit({\n type: \"pageview\",\n data: {\n path: resolvedPath,\n title: typeof document !== \"undefined\" ? document.title : \"\",\n timestamp: Date.now(),\n sessionId: this.session.id,\n referrer:\n typeof document !== \"undefined\"\n ? document.referrer || undefined\n : undefined,\n },\n });\n\n if (typeof window !== \"undefined\") {\n window.dispatchEvent(\n new CustomEvent(\"tracker:navigate\", {\n detail: { path: resolvedPath, title: document.title },\n }),\n );\n }\n }\n\n // ─── Data accessors ─────────────────────────────────────────────────────────\n\n /** A read-only snapshot of the current session. */\n getSession(): Readonly<SessionData> {\n return this.session;\n }\n\n /** All page views recorded so far. */\n getPageViews(): PageView[] {\n return [...this.session.pageViews];\n }\n\n /** Cumulative milliseconds spent per path. */\n getTimeSpent(): Record<string, number> {\n return { ...this.session.timeSpent };\n }\n\n /** Heatmap points for a specific path. */\n getHeatmapData(path: string): HeatmapPoint[];\n /** Heatmap points for all tracked paths. */\n getHeatmapData(): Record<string, HeatmapPoint[]>;\n getHeatmapData(\n path?: string,\n ): HeatmapPoint[] | Record<string, HeatmapPoint[]> {\n if (path !== undefined) {\n return [...(this.session.heatmap[path] ?? [])];\n }\n return Object.entries(this.session.heatmap).reduce<\n Record<string, HeatmapPoint[]>\n >((acc, [k, v]) => {\n acc[k] = [...v];\n return acc;\n }, {});\n }\n\n // ─── Network ────────────────────────────────────────────────────────────────\n\n /** Drain the queue and POST all pending events to the batch endpoint. */\n private async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n // Splice atomically so new events emitted during the async request don't\n // get lost — they stay in the queue for the next flush.\n const batch = this.queue.splice(0);\n await this.sendBatch(batch);\n }\n\n /**\n * Synchronous best-effort flush via `navigator.sendBeacon`.\n * Used on `pagehide` / `visibilitychange:hidden` where async fetch may be\n * cancelled by the browser before it completes.\n */\n private flushBeacon(): void {\n if (this.queue.length === 0) return;\n const batch = this.queue.splice(0);\n const url = `${this.cfg.endpoint!}/batch`;\n const blob = new Blob([this.buildBatchBody(batch)], {\n type: \"application/json\",\n });\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, blob);\n } else {\n // Fallback: fire-and-forget fetch (best effort on platforms without sendBeacon)\n void this.sendBatch(batch);\n }\n }\n\n private buildBatchBody(events: TrackerEvent[]): string {\n return JSON.stringify({\n ...(this.cfg.appId ? { appId: this.cfg.appId } : {}),\n location: this.location ?? undefined,\n events: events.map((e) => ({\n sessionId: this.session.id,\n type: e.type,\n data: e.data,\n })),\n });\n }\n\n private async sendBatch(events: TrackerEvent[]): Promise<void> {\n const url = `${this.cfg.endpoint!}/batch`;\n const authHeaders: Record<string, string> = this.cfg.secretKey\n ? { Authorization: `Bearer ${this.cfg.secretKey}` }\n : {};\n try {\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...authHeaders },\n body: this.buildBatchBody(events),\n keepalive: true,\n });\n } catch {\n // Intentionally silent — analytics must never surface errors to users.\n }\n }\n}\n","import { createContext, useContext } from \"react\";\nimport type { UserTracker } from \"../tracker\";\n\nexport const TrackerContext = createContext<UserTracker | null>(null);\n\n/**\n * Returns the nearest `UserTracker` instance from context.\n * Returns `null` when called outside of a `<UserTrackerProvider>`.\n */\nexport function useTrackerContext(): UserTracker | null {\n return useContext(TrackerContext);\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport type { HeatmapPoint, PageView, TrackerEvent } from \"../types\";\nimport { useTrackerContext } from \"./context\";\n\n// ─── useTracker ───────────────────────────────────────────────────────────────\n\n/**\n * Returns the `UserTracker` instance from the nearest `<UserTrackerProvider>`.\n * Returns `null` when called outside of a provider.\n */\nexport function useTracker() {\n return useTrackerContext();\n}\n\n// ─── usePageView ──────────────────────────────────────────────────────────────\n\n/**\n * Manually records a page view whenever `path` changes.\n *\n * Pass the current pathname — particularly useful with Next.js App Router:\n * ```tsx\n * 'use client';\n * import { usePathname } from 'next/navigation';\n * import { usePageView } from 'user-tracker/react';\n *\n * export function NavigationTracker() {\n * usePageView(usePathname());\n * return null;\n * }\n * ```\n * When no `path` is provided the hook is a no-op (automatic tracking via the\n * NavigationPlugin handles it).\n */\nexport function usePageView(path?: string): void {\n const tracker = useTrackerContext();\n useEffect(() => {\n if (tracker && path !== undefined) {\n tracker.trackPageView(path);\n }\n // Re-fire when the path changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [path]);\n}\n\n// ─── useHeatmapData ───────────────────────────────────────────────────────────\n\n/**\n * Returns a live array of `HeatmapPoint` objects for the given path (defaults\n * to `window.location.pathname`).\n *\n * The state is updated in batches — at most once every `refreshMs` ms — to\n * avoid a re-render on every single mouse move.\n *\n * @param path The page path to query. Defaults to the current pathname.\n * @param refreshMs Minimum interval between state updates. Default: 500.\n */\nexport function useHeatmapData(path?: string, refreshMs = 500): HeatmapPoint[] {\n const tracker = useTrackerContext();\n const [data, setData] = useState<HeatmapPoint[]>([]);\n const pendingRef = useRef(false);\n\n useEffect(() => {\n if (!tracker) return;\n\n const targetPath =\n path ?? (typeof window !== \"undefined\" ? window.location.pathname : \"/\");\n\n const refresh = (): void => {\n setData(tracker.getHeatmapData(targetPath) as HeatmapPoint[]);\n pendingRef.current = false;\n };\n\n // Initial read.\n refresh();\n\n // Re-read after each new heatmap point, debounced by refreshMs.\n const unsub = tracker.subscribe((event: TrackerEvent) => {\n if (event.type === \"heatmap\" && event.data.path === targetPath) {\n if (!pendingRef.current) {\n pendingRef.current = true;\n setTimeout(refresh, refreshMs);\n }\n }\n });\n\n return unsub;\n // refreshMs is intentionally excluded — changing it after mount has no effect.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [tracker, path]);\n\n return data;\n}\n\n// ─── usePageViews ─────────────────────────────────────────────────────────────\n\n/**\n * Returns a live array of all page views recorded in the current session.\n */\nexport function usePageViews(): PageView[] {\n const tracker = useTrackerContext();\n const [views, setViews] = useState<PageView[]>([]);\n\n useEffect(() => {\n if (!tracker) return;\n\n setViews(tracker.getPageViews());\n\n const unsub = tracker.subscribe((event: TrackerEvent) => {\n if (event.type === \"pageview\") {\n setViews(tracker.getPageViews());\n }\n });\n\n return unsub;\n }, [tracker]);\n\n return views;\n}\n\n// ─── useTimeSpent ─────────────────────────────────────────────────────────────\n\n/**\n * Returns a live record of cumulative milliseconds spent per path.\n */\nexport function useTimeSpent(): Record<string, number> {\n const tracker = useTrackerContext();\n const [time, setTime] = useState<Record<string, number>>({});\n\n useEffect(() => {\n if (!tracker) return;\n\n setTime(tracker.getTimeSpent());\n\n const unsub = tracker.subscribe((event: TrackerEvent) => {\n if (event.type === \"timespent\") {\n setTime(tracker.getTimeSpent());\n }\n });\n\n return unsub;\n }, [tracker]);\n\n return time;\n}\n"],"mappings":"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","names":["react_exports","__export","UserTrackerProvider","useHeatmapData","usePageView","usePageViews","useTimeSpent","useTracker","__toCommonJS","import_react","generateSessionId","c","r","fetchLocation","res","d","e","NavigationPlugin","emit","sessionId","origPush","state","title","url","origReplace","path","TimePlugin","emit","sessionId","e","duration","throttle","fn","delay","lastCall","args","now","HeatmapPlugin","emit","sessionId","sampleRate","maxPoints","e","pageWidth","pageHeight","absY","vw","vh","centerX","centerY","throttle","_a","point","LogCapture","options","u","parts","args","first","stack","msg","src","line","col","err","e","reason","message","level","extra","a","entry","url","body","import_html2canvas","DEFAULT_INTERVAL_MS","STORAGE_KEY","SnapshotPlugin","cfg","_a","stored","e","path","last","root","pageBg","canvas","html2canvas","blob","resolve","form","headers","DEFAULT_ENDPOINT","DEFAULTS","UserTracker","config","e","_a","path","generateSessionId","emit","sessionId","NavigationPlugin","TimePlugin","HeatmapPlugin","fetchLocation","loc","LogCapture","SnapshotPlugin","_b","_c","_d","_e","url","authHeaders","body","event","prev","key","pts","fn","resolvedPath","acc","k","v","batch","blob","events","import_react","TrackerContext","useTrackerContext","import_jsx_runtime","UserTrackerProvider","config","children","trackerRef","UserTracker","tracker","TrackerContext","import_react","useTracker","useTrackerContext","usePageView","path","tracker","useHeatmapData","refreshMs","data","setData","pendingRef","targetPath","refresh","event","usePageViews","views","setViews","useTimeSpent","time","setTime"]}
@@ -0,0 +1,2 @@
1
+ import{useEffect as H,useRef as O}from"react";function b(){return typeof crypto!="undefined"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,r=>{let t=Math.random()*16|0;return(r==="x"?t:t&3|8).toString(16)})}async function E(){try{let r=await fetch("https://ipapi.co/json/",{method:"GET",headers:{Accept:"application/json"}});if(!r.ok)return null;let t=await r.json();return t.error?null:{country:typeof t.country_code=="string"?t.country_code:"",countryName:typeof t.country_name=="string"?t.country_name:"",city:typeof t.city=="string"?t.city:void 0,region:typeof t.region=="string"?t.region:void 0,latitude:typeof t.latitude=="number"?t.latitude:void 0,longitude:typeof t.longitude=="number"?t.longitude:void 0}}catch(r){return null}}var p=class{constructor({emit:t,sessionId:e}){this.previousPath="";this.originalPushState=null;this.originalReplaceState=null;this.handlePopState=()=>{this.handleNavigation()};this.emit=t,this.sessionId=e}init(){this.recordPageView(window.location.pathname+window.location.search),window.addEventListener("popstate",this.handlePopState),this.originalPushState=history.pushState.bind(history);let t=this.originalPushState;history.pushState=(i,n,s)=>{t(i,n,s),this.handleNavigation()},this.originalReplaceState=history.replaceState.bind(history);let e=this.originalReplaceState;history.replaceState=(i,n,s)=>{e(i,n,s),this.handleNavigation()}}destroy(){window.removeEventListener("popstate",this.handlePopState),this.originalPushState&&(history.pushState=this.originalPushState),this.originalReplaceState&&(history.replaceState=this.originalReplaceState)}handleNavigation(){let t=window.location.pathname+window.location.search;t!==this.previousPath&&this.recordPageView(t)}recordPageView(t){this.previousPath=t,this.emit({type:"pageview",data:{path:t,title:document.title,timestamp:Date.now(),sessionId:this.sessionId,referrer:document.referrer||void 0}}),window.dispatchEvent(new CustomEvent("tracker:navigate",{detail:{path:t,title:document.title}}))}};var l=class{constructor({emit:t,sessionId:e}){this.currentPath="";this.startTime=0;this.tracking=!1;this.handleNavigate=t=>{this.stopTracking(),this.currentPath=t.detail.path,this.startTracking()};this.handleVisibilityChange=()=>{document.hidden?this.stopTracking():this.startTracking()};this.handleUnload=()=>{this.stopTracking()};this.emit=t,this.sessionId=e}init(){this.currentPath=window.location.pathname+window.location.search,this.startTracking(),window.addEventListener("tracker:navigate",this.handleNavigate),document.addEventListener("visibilitychange",this.handleVisibilityChange),window.addEventListener("beforeunload",this.handleUnload),window.addEventListener("pagehide",this.handleUnload)}destroy(){this.stopTracking(),window.removeEventListener("tracker:navigate",this.handleNavigate),document.removeEventListener("visibilitychange",this.handleVisibilityChange),window.removeEventListener("beforeunload",this.handleUnload),window.removeEventListener("pagehide",this.handleUnload)}startTracking(){this.startTime=Date.now(),this.tracking=!0}stopTracking(){if(!this.tracking||!this.currentPath)return;let t=Date.now()-this.startTime;if(t<100){this.tracking=!1;return}this.emit({type:"timespent",data:{path:this.currentPath,duration:t,sessionId:this.sessionId,timestamp:Date.now()}}),this.tracking=!1}};function w(r,t){let e=0;return(...i)=>{let n=Date.now();n-e>=t&&(e=n,r(...i))}}var u=class{constructor({emit:t,sessionId:e,sampleRate:i=.3,maxPoints:n=2e3}){this.currentPath="";this.pointCounts={};this.handleMouseMove=t=>{if(Math.random()>this.sampleRate)return;let e=document.documentElement.scrollWidth,i=document.documentElement.scrollHeight,n=t.clientY+window.scrollY;this.recordPoint({x:t.clientX,y:n,xPct:e>0?t.clientX/e*100:0,yPct:i>0?n/i*100:0,type:"move"})};this.handleClick=t=>{let e=document.documentElement.scrollWidth,i=document.documentElement.scrollHeight,n=t.clientY+window.scrollY;this.recordPoint({x:t.clientX,y:n,xPct:e>0?t.clientX/e*100:0,yPct:i>0?n/i*100:0,type:"click"})};this.handleScroll=()=>{if(Math.random()>this.sampleRate)return;let t=document.documentElement.scrollWidth,e=document.documentElement.scrollHeight,i=window.innerWidth,n=window.innerHeight,s=window.scrollX+i/2,o=window.scrollY+n/2;this.recordPoint({x:i/2,y:o,xPct:t>0?s/t*100:0,yPct:e>0?o/e*100:0,type:"scroll"})};this.handleNavigate=t=>{this.currentPath=t.detail.path};this.emit=t,this.sessionId=e,this.sampleRate=i,this.maxPoints=n,this.throttledMouseMove=w(this.handleMouseMove,50),this.throttledScroll=w(this.handleScroll,100)}init(){this.currentPath=window.location.pathname+window.location.search,document.addEventListener("mousemove",this.throttledMouseMove),document.addEventListener("click",this.handleClick),window.addEventListener("scroll",this.throttledScroll,{passive:!0}),window.addEventListener("tracker:navigate",this.handleNavigate)}destroy(){document.removeEventListener("mousemove",this.throttledMouseMove),document.removeEventListener("click",this.handleClick),window.removeEventListener("scroll",this.throttledScroll),window.removeEventListener("tracker:navigate",this.handleNavigate)}canRecord(){var t;return((t=this.pointCounts[this.currentPath])!=null?t:0)<this.maxPoints}recordPoint(t){var e;this.canRecord()&&(this.pointCounts[this.currentPath]=((e=this.pointCounts[this.currentPath])!=null?e:0)+1,this.emit({type:"heatmap",data:{...t,path:this.currentPath,timestamp:Date.now()}}))}};var g=class{constructor(t){this.prevOnError=null;this.prevOnUnhandledRejection=null;this.initialized=!1;try{let e=new URL(t.endpoint),i=e.pathname.replace(/\/$/,"").split("/");i.pop(),e.pathname=i.join("/")||"/",this.endpoint=e.toString().replace(/\/$/,"")}catch(e){this.endpoint=t.endpoint}this.sessionId=t.sessionId,this.appId=t.appId,this.authHeaders=t.secretKey?{Authorization:`Bearer ${t.secretKey}`}:{}}init(){typeof window=="undefined"||this.initialized||(this.origInfo=console.info.bind(console),this.origWarn=console.warn.bind(console),this.origError=console.error.bind(console),console.info=(...t)=>{this.origInfo(...t),this.send("info",this.format(t))},console.warn=(...t)=>{this.origWarn(...t),this.send("warn",this.format(t))},console.error=(...t)=>{this.origError(...t);let[e]=t,i=e instanceof Error?e.stack:void 0;this.send("error",this.format(t),{stack:i})},this.prevOnError=window.onerror,window.onerror=(t,e,i,n,s)=>(this.send("error",String(t),{stack:s==null?void 0:s.stack,meta:{src:e,line:i,col:n}}),typeof this.prevOnError=="function"?this.prevOnError(t,e,i,n,s):!1),this.prevOnUnhandledRejection=t=>{let e=t.reason,i=e instanceof Error?e.message:String(e!=null?e:"Unhandled promise rejection");this.send("error",i,{stack:e instanceof Error?e.stack:void 0})},window.addEventListener("unhandledrejection",this.prevOnUnhandledRejection),this.initialized=!0)}destroy(){this.initialized&&(console.info=this.origInfo,console.warn=this.origWarn,console.error=this.origError,window.onerror=this.prevOnError,this.prevOnUnhandledRejection&&window.removeEventListener("unhandledrejection",this.prevOnUnhandledRejection),this.initialized=!1)}capture(t,e,i){this.send(t,e,i)}format(t){return t.map(e=>{if(e instanceof Error)return e.message;if(typeof e=="object")try{return JSON.stringify(e)}catch(i){return String(e)}return String(e)}).join(" ")}send(t,e,i){let n={sessionId:this.sessionId,...this.appId?{appId:this.appId}:{},level:t,message:e,url:typeof window!="undefined"?window.location.href:void 0,stack:i==null?void 0:i.stack,meta:i==null?void 0:i.meta,timestamp:Date.now()},s=`${this.endpoint}/logs/ingest`,o=JSON.stringify(n);fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.authHeaders},body:o,keepalive:!0}).catch(a=>{this.origError&&this.origError("[user-tracker] Failed to send log:",a)})}};import T from"html2canvas";var I=300*1e3,S="__ut_snap_ts__",m=class{constructor(t){this.lastSentPerPath={};var e;this.snapshotUrl=t.endpoint.replace(/\/events$/,"/snapshots"),this.appId=t.appId,this.secretKey=t.secretKey,this.intervalMs=(e=t.intervalMs)!=null?e:I;try{let i=localStorage.getItem(S);i&&(this.lastSentPerPath=JSON.parse(i))}catch(i){}}async capture(t){var i;if(typeof window=="undefined")return;let e=(i=this.lastSentPerPath[t])!=null?i:0;if(!(Date.now()-e<this.intervalMs))try{let n=document.documentElement,s=getComputedStyle(n).backgroundColor||getComputedStyle(document.body).backgroundColor||"#ffffff",o=await T(n,{allowTaint:!0,useCORS:!0,logging:!1,scale:window.devicePixelRatio||1,width:n.scrollWidth,height:n.scrollHeight,windowWidth:window.innerWidth,windowHeight:window.innerHeight,scrollX:0,scrollY:0,x:0,y:0,backgroundColor:s,foreignObjectRendering:!1,removeContainer:!0}),a=await new Promise(P=>o.toBlob(P,"image/png"));if(!a)return;let d=new FormData;d.append("screenshot",a,"screenshot.png"),d.append("path",t),d.append("width",String(n.scrollWidth)),d.append("height",String(n.scrollHeight)),this.appId&&d.append("appId",this.appId);let h={};this.secretKey&&(h.Authorization=`Bearer ${this.secretKey}`),await fetch(this.snapshotUrl,{method:"POST",headers:h,body:d}),this.lastSentPerPath[t]=Date.now();try{localStorage.setItem(S,JSON.stringify(this.lastSentPerPath))}catch(P){}}catch(n){}}destroy(){}};var x="https://api.alphana.ir/api/events",R={endpoint:x,trackNavigation:!0,trackTime:!0,trackHeatmap:!0,trackLogs:!0,trackSnapshots:!0,mouseSampleRate:.3,maxHeatmapPoints:2e3,batchSize:20,flushInterval:5e3},v=class{constructor(t={}){this.initialized=!1;this.subscribers=new Set;this.queue=[];this.flushTimer=null;this.heartbeatTimer=null;this.location=null;this.handleVisibilityChange=()=>{document.visibilityState==="hidden"&&(this.queue.length>0&&this.flushBeacon(),this.sendDeactivate())};this.handlePageHide=()=>{this.queue.length>0&&this.flushBeacon(),this.sendDeactivate()};this.handleNavigate=t=>{var i;let e=(i=t.detail)==null?void 0:i.path;e&&this.snapshot&&this.snapshot.capture(e)};var e;this.cfg={...R,...t};try{new URL(this.cfg.endpoint)}catch(i){throw new Error(`[alpha-tracker] Invalid endpoint URL: "${this.cfg.endpoint}"`)}this.session={id:(e=t.sessionId)!=null?e:b(),startedAt:Date.now(),pageViews:[],timeSpent:{},heatmap:{}}}init(){if(typeof window=="undefined"||this.initialized)return this;let t=this.emit.bind(this),{id:e}=this.session;return this.cfg.trackNavigation&&(this.navigation=new p({emit:t,sessionId:e}),this.navigation.init()),this.cfg.trackTime&&(this.time=new l({emit:t,sessionId:e}),this.time.init()),this.cfg.trackHeatmap&&(this.heatmap=new u({emit:t,sessionId:e,sampleRate:this.cfg.mouseSampleRate,maxPoints:this.cfg.maxHeatmapPoints}),this.heatmap.init()),this.cfg.endpoint&&(this.flushTimer=setInterval(()=>{this.queue.length>0&&this.flush()},this.cfg.flushInterval),E().then(i=>{this.location=i,i&&(this.session.location=i)}),window.addEventListener("visibilitychange",this.handleVisibilityChange),window.addEventListener("pagehide",this.handlePageHide),this.heartbeatTimer=setInterval(()=>{document.visibilityState!=="hidden"&&this.sendHeartbeat()},3e4),this.cfg.trackLogs&&(this.logCapture=new g({endpoint:this.cfg.endpoint,sessionId:this.session.id,secretKey:this.cfg.secretKey,appId:this.cfg.appId}),this.logCapture.init()),this.cfg.trackSnapshots!==!1&&(this.snapshot=new m({endpoint:this.cfg.endpoint,appId:this.cfg.appId,secretKey:this.cfg.secretKey,intervalMs:this.cfg.snapshotIntervalMs}),this.snapshot.capture(window.location.pathname),window.addEventListener("tracker:navigate",this.handleNavigate))),this.initialized=!0,this}destroy(){var t,e,i,n,s;this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.heartbeatTimer!==null&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),typeof window!="undefined"&&(window.removeEventListener("visibilitychange",this.handleVisibilityChange),window.removeEventListener("pagehide",this.handlePageHide)),(t=this.navigation)==null||t.destroy(),(e=this.time)==null||e.destroy(),(i=this.heatmap)==null||i.destroy(),(n=this.logCapture)==null||n.destroy(),(s=this.snapshot)==null||s.destroy(),typeof window!="undefined"&&window.removeEventListener("tracker:navigate",this.handleNavigate),this.queue.length>0&&this.cfg.endpoint&&this.flushBeacon(),this.initialized=!1}async sendHeartbeat(){if(!this.cfg.endpoint)return;let t=`${this.cfg.endpoint}/heartbeat`,e=this.cfg.secretKey?{Authorization:`Bearer ${this.cfg.secretKey}`}:{},i=JSON.stringify({sessionId:this.session.id,path:typeof window!="undefined"?window.location.pathname:"/",active:!0,...this.cfg.appId?{appId:this.cfg.appId}:{},...this.location?{location:this.location}:{}});try{await fetch(t,{method:"POST",headers:{"Content-Type":"application/json",...e},body:i,keepalive:!0})}catch(n){}}sendDeactivate(){if(!this.cfg.endpoint)return;let t=`${this.cfg.endpoint}/heartbeat`,e=this.cfg.secretKey?{Authorization:`Bearer ${this.cfg.secretKey}`}:{},i=JSON.stringify({sessionId:this.session.id,path:typeof window!="undefined"?window.location.pathname:"/",active:!1,...this.cfg.appId?{appId:this.cfg.appId}:{}});typeof navigator!="undefined"&&navigator.sendBeacon?navigator.sendBeacon(t,new Blob([i],{type:"application/json"})):fetch(t,{method:"POST",headers:{"Content-Type":"application/json",...e},body:i,keepalive:!0}).catch(()=>{})}emit(t){var e,i,n;switch(t.type){case"pageview":this.session.pageViews.push(t.data);break;case"timespent":{let s=(e=this.session.timeSpent[t.data.path])!=null?e:0;this.session.timeSpent[t.data.path]=s+t.data.duration;break}case"heatmap":{let s=t.data.path;this.session.heatmap[s]||(this.session.heatmap[s]=[]);let o=this.session.heatmap[s];o.length<this.cfg.maxHeatmapPoints&&o.push(t.data);break}}this.subscribers.forEach(s=>s(t)),(n=(i=this.cfg).onEvent)==null||n.call(i,t),this.cfg.endpoint&&(this.queue.push(t),this.queue.length>=this.cfg.batchSize&&this.flush())}subscribe(t){return this.subscribers.add(t),()=>this.subscribers.delete(t)}trackPageView(t){let e=t!=null?t:typeof window!="undefined"?window.location.pathname+window.location.search:"/";this.emit({type:"pageview",data:{path:e,title:typeof document!="undefined"?document.title:"",timestamp:Date.now(),sessionId:this.session.id,referrer:typeof document!="undefined"&&document.referrer||void 0}}),typeof window!="undefined"&&window.dispatchEvent(new CustomEvent("tracker:navigate",{detail:{path:e,title:document.title}}))}getSession(){return this.session}getPageViews(){return[...this.session.pageViews]}getTimeSpent(){return{...this.session.timeSpent}}getHeatmapData(t){var e;return t!==void 0?[...(e=this.session.heatmap[t])!=null?e:[]]:Object.entries(this.session.heatmap).reduce((i,[n,s])=>(i[n]=[...s],i),{})}async flush(){if(this.queue.length===0)return;let t=this.queue.splice(0);await this.sendBatch(t)}flushBeacon(){if(this.queue.length===0)return;let t=this.queue.splice(0),e=`${this.cfg.endpoint}/batch`,i=new Blob([this.buildBatchBody(t)],{type:"application/json"});typeof navigator!="undefined"&&navigator.sendBeacon?navigator.sendBeacon(e,i):this.sendBatch(t)}buildBatchBody(t){var e;return JSON.stringify({...this.cfg.appId?{appId:this.cfg.appId}:{},location:(e=this.location)!=null?e:void 0,events:t.map(i=>({sessionId:this.session.id,type:i.type,data:i.data}))})}async sendBatch(t){let e=`${this.cfg.endpoint}/batch`,i=this.cfg.secretKey?{Authorization:`Bearer ${this.cfg.secretKey}`}:{};try{await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",...i},body:this.buildBatchBody(t),keepalive:!0})}catch(n){}}};import{createContext as C,useContext as L}from"react";var y=C(null);function c(){return L(y)}import{jsx as U}from"react/jsx-runtime";function N({config:r={},children:t}){let e=O(null);return e.current===null&&(e.current=new v(r)),H(()=>{let i=e.current;return i.init(),()=>i.destroy()},[]),U(y.Provider,{value:e.current,children:t})}import{useEffect as f,useRef as D,useState as k}from"react";function V(){return c()}function B(r){let t=c();f(()=>{t&&r!==void 0&&t.trackPageView(r)},[r])}function M(r,t=500){let e=c(),[i,n]=k([]),s=D(!1);return f(()=>{if(!e)return;let o=r!=null?r:typeof window!="undefined"?window.location.pathname:"/",a=()=>{n(e.getHeatmapData(o)),s.current=!1};return a(),e.subscribe(h=>{h.type==="heatmap"&&h.data.path===o&&(s.current||(s.current=!0,setTimeout(a,t)))})},[e,r]),i}function j(){let r=c(),[t,e]=k([]);return f(()=>r?(e(r.getPageViews()),r.subscribe(n=>{n.type==="pageview"&&e(r.getPageViews())})):void 0,[r]),t}function _(){let r=c(),[t,e]=k({});return f(()=>r?(e(r.getTimeSpent()),r.subscribe(n=>{n.type==="timespent"&&e(r.getTimeSpent())})):void 0,[r]),t}export{N as UserTrackerProvider,M as useHeatmapData,B as usePageView,j as usePageViews,_ as useTimeSpent,V as useTracker};
2
+ //# sourceMappingURL=index.mjs.map