alphana-sdk 0.4.6 → 0.5.9

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/README.md CHANGED
@@ -1,16 +1,18 @@
1
1
  # alphana-sdk
2
2
 
3
- Client-side analytics SDK for React, Next.js, Vite, and vanilla JS/TS projects. Tracks navigation, time-on-page, heatmaps, error logs, and periodic page screenshots — all without cookies or third parties.
3
+ [![npm](https://img.shields.io/npm/v/alphana-sdk.svg)](https://www.npmjs.com/package/alphana-sdk)
4
+
5
+ Client-side analytics SDK for React, Next.js, Vite, and vanilla JS/TS. Tracks SPA navigation, time-on-page, heatmaps (with optional path allowlists), console and runtime errors, session heartbeats, **feature flags** (with user targeting), and behavioral signals such as **rage clicks** and quick **U-turns** — without third-party scripts.
6
+
7
+ - **Package:** [alphana-sdk on npm](https://www.npmjs.com/package/alphana-sdk)
8
+ - **Exports:** `alphana-sdk` (core) and `alphana-sdk/react` (provider + hooks)
4
9
 
5
10
  ## Installation
6
11
 
7
12
  ```bash
8
13
  npm install alphana-sdk
9
- # pnpm
10
14
  pnpm add alphana-sdk
11
- # yarn
12
15
  yarn add alphana-sdk
13
- # bun
14
16
  bun add alphana-sdk
15
17
  ```
16
18
 
@@ -22,12 +24,12 @@ bun add alphana-sdk
22
24
 
23
25
  ```tsx
24
26
  // main.tsx
25
- import { StrictMode } from 'react';
26
- import { createRoot } from 'react-dom/client';
27
- import { UserTrackerProvider } from 'alphana-sdk/react';
28
- import App from './App';
27
+ import { StrictMode } from "react";
28
+ import { createRoot } from "react-dom/client";
29
+ import { UserTrackerProvider } from "alphana-sdk/react";
30
+ import App from "./App";
29
31
 
30
- createRoot(document.getElementById('root')!).render(
32
+ createRoot(document.getElementById("root")!).render(
31
33
  <StrictMode>
32
34
  <UserTrackerProvider
33
35
  config={{
@@ -49,13 +51,13 @@ VITE_TRACKER_SECRET=your_secret_key
49
51
 
50
52
  ### Next.js App Router
51
53
 
52
- Because App Router renders server-side by default, wrap the provider in a Client Component:
54
+ Wrap the provider in a Client Component, then use `usePageView` with `usePathname()` so App Router navigations are recorded (History API hooks alone do not always fire).
53
55
 
54
56
  ```tsx
55
57
  // app/providers.tsx
56
- 'use client';
57
- import { UserTrackerProvider } from 'alphana-sdk/react';
58
- import type { ReactNode } from 'react';
58
+ "use client";
59
+ import { UserTrackerProvider } from "alphana-sdk/react";
60
+ import type { ReactNode } from "react";
59
61
 
60
62
  export function Providers({ children }: { children: ReactNode }) {
61
63
  return (
@@ -73,76 +75,78 @@ export function Providers({ children }: { children: ReactNode }) {
73
75
 
74
76
  ```tsx
75
77
  // app/layout.tsx
76
- import { Providers } from './providers';
78
+ import { Providers } from "./providers";
79
+ import { NavigationTracker } from "./navigation-tracker";
77
80
 
78
81
  export default function RootLayout({ children }: { children: React.ReactNode }) {
79
82
  return (
80
83
  <html lang="en">
81
84
  <body>
82
- <Providers>{children}</Providers>
85
+ <Providers>
86
+ <NavigationTracker />
87
+ {children}
88
+ </Providers>
83
89
  </body>
84
90
  </html>
85
91
  );
86
92
  }
87
93
  ```
88
94
 
95
+ ```tsx
96
+ // app/navigation-tracker.tsx
97
+ "use client";
98
+ import { usePathname } from "next/navigation";
99
+ import { usePageView } from "alphana-sdk/react";
100
+
101
+ export function NavigationTracker() {
102
+ usePageView(usePathname());
103
+ return null;
104
+ }
105
+ ```
106
+
89
107
  ```bash
90
108
  # .env.local
91
109
  NEXT_PUBLIC_TRACKER_APP_ID=your_app_id
92
110
  NEXT_PUBLIC_TRACKER_SECRET=your_secret_key
93
111
  ```
94
112
 
95
- > **App Router page tracking:** The provider automatically intercepts `pushState` / `popstate`. For App Router you can optionally add an explicit page-view call using the `usePageView` hook in a Client Component:
96
- >
97
- > ```tsx
98
- > // app/components/NavigationTracker.tsx
99
- > 'use client';
100
- > import { usePathname } from 'next/navigation';
101
- > import { usePageView } from 'alphana-sdk/react';
102
- >
103
- > export function NavigationTracker() {
104
- > usePageView(usePathname());
105
- > return null;
106
- > }
107
- > ```
108
-
109
113
  ### Vanilla JS / TypeScript
110
114
 
111
115
  ```ts
112
- import { UserTracker } from 'alphana-sdk';
116
+ import { UserTracker } from "alphana-sdk";
113
117
 
114
118
  const tracker = new UserTracker({
115
- appId: 'YOUR_APP_ID',
116
- secretKey: 'YOUR_SECRET_KEY',
119
+ appId: "YOUR_APP_ID",
120
+ secretKey: "YOUR_SECRET_KEY",
117
121
  });
118
122
 
119
- tracker.init(); // attach listeners; no-op in SSR environments
123
+ tracker.init(); // safe no-op during SSR (`window` undefined)
120
124
 
121
- // Call on teardown / logout
122
- tracker.destroy();
125
+ tracker.destroy(); // teardown, timers, best-effort final send
123
126
  ```
124
127
 
125
128
  ---
126
129
 
127
130
  ## Configuration reference
128
131
 
129
- | Option | Type | Default | Description |
130
- | -------------------- | ------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------- |
131
- | `appId` | `string` | — | **Required.** Your app ID from the Alphana dashboard. |
132
- | `secretKey` | `string` | — | **Required.** SDK secret key sent as `Authorization: Bearer`. |
133
- | `endpoint` | `string` | `https://api.alphana.ir/api/events` | Override only when self-hosting. |
134
- | `sessionId` | `string` | auto UUID | Override the auto-generated session identifier. |
135
- | `trackNavigation` | `boolean` | `true` | Intercept `pushState` / `popstate` for SPA route changes. |
136
- | `trackTime` | `boolean` | `true` | Measure time spent on each page. |
137
- | `trackHeatmap` | `boolean` | `true` | Record mouse-move, click, and scroll positions. |
138
- | `trackLogs` | `boolean` | `true` | Capture `console.info/warn/error`, `window.onerror`, and `unhandledrejection`. |
139
- | `trackSnapshots` | `boolean` | `true` | Send a full-page screenshot every interval (requires `html2canvas`). |
140
- | `snapshotIntervalMs` | `number` (ms) | `300000` (5 min) | How often to capture a snapshot when `trackSnapshots` is enabled. |
141
- | `mouseSampleRate` | `number` (0–1) | `0.3` | Fraction of mouse/scroll events to record. |
142
- | `maxHeatmapPoints` | `number` | `2000` | Maximum in-memory heatmap points per page. |
143
- | `batchSize` | `number` | `20` | Events queued before an automatic batch flush. |
144
- | `flushInterval` | `number` (ms) | `5000` | Milliseconds between automatic flushes regardless of queue size. |
145
- | `onEvent` | `(event: TrackerEvent) => void` | — | Callback invoked synchronously for every emitted event. |
132
+ | Option | Type | Default | Description |
133
+ | ------------------ | ------------------------------- | ----------------------------------- | ----------- |
134
+ | `appId` | `string` | — | **Required for Alphana Cloud.** App ID from the dashboard; included in batch payloads. |
135
+ | `secretKey` | `string` | — | **Required for Alphana Cloud** (and for feature flags). Sent as `Authorization: Bearer …` on API calls. |
136
+ | `endpoint` | `string` | `https://api.alphana.ir/api/events` | Events base URL for Alphana Cloud. Batching uses `{endpoint}/batch`, heartbeats `{endpoint}/heartbeat`, logs derived under the same API prefix. |
137
+ | `sessionId` | `string` | auto UUID | Optional fixed session id. |
138
+ | `trackNavigation` | `boolean` | `true` | Intercept `pushState` / `replaceState` / `popstate` for SPA routes; also emits U-turn detection. |
139
+ | `trackTime` | `boolean` | `true` | Cumulative time per path. |
140
+ | `trackHeatmap` | `boolean` | `true` | Mouse move, click, scroll sampling; rage-click bursts. |
141
+ | `heatmapPages` | `string[]` | | If set, heatmap events are only collected on these paths (e.g. from dashboard “Heatmap Pages”). If omitted, all pages are eligible (backend may enforce limits). |
142
+ | `trackLogs` | `boolean` | `true` | Patches `console.info/warn/error`, `window.onerror`, `unhandledrejection`; sends to `/logs/ingest`. |
143
+ | `mouseSampleRate` | `number` (0–1) | `0.3` | Fraction of move/scroll events kept. |
144
+ | `maxHeatmapPoints` | `number` | `2000` | Max in-memory heatmap points per path in the session snapshot. |
145
+ | `batchSize` | `number` | `20` | Queue size before an automatic flush. |
146
+ | `flushInterval` | `number` (ms) | `5000` | Timer-based flush when the queue is non-empty. |
147
+ | `onEvent` | `(event: TrackerEvent) => void` | — | Synchronous callback for every emitted event. |
148
+
149
+ TypeScript marks `appId` / `secretKey` as optional on `TrackerConfig`, but you should always pass them when using the hosted Alphana API.
146
150
 
147
151
  ---
148
152
 
@@ -151,70 +155,80 @@ tracker.destroy();
151
155
  ### `UserTracker`
152
156
 
153
157
  ```ts
154
- import { UserTracker } from 'alphana-sdk';
158
+ import { UserTracker } from "alphana-sdk";
159
+
160
+ const tracker = new UserTracker({ appId: "id", secretKey: "sk" });
155
161
 
156
- const tracker = new UserTracker({ appId: 'id', secretKey: 'sk' });
162
+ tracker.init();
163
+ tracker.destroy();
164
+
165
+ tracker.trackPageView(path?: string);
157
166
 
158
- tracker.init(); // attach listeners; no-op in SSR; returns `this`
159
- tracker.destroy(); // remove all listeners and timers, flush remaining queue
167
+ tracker.flush(); // POST queued events to `/batch` (fire-and-forget)
160
168
 
161
- tracker.trackPageView(path?: string); // manually record a page view
169
+ tracker.getSession(); // read-only SessionData
170
+ tracker.getPageViews();
171
+ tracker.getTimeSpent(); // Record<path, milliseconds>
172
+ tracker.getHeatmapData(path: string): HeatmapPoint[];
173
+ tracker.getHeatmapData(): Record<string, HeatmapPoint[]>;
162
174
 
163
- tracker.getSession(); // SessionData snapshot for the current session
164
- tracker.getPageViews(); // PageView[] recorded this session
165
- tracker.getTimeSpent(); // Record<path, ms> cumulative time per path
166
- tracker.getHeatmapData(path?: string); // HeatmapPoint[] for path, or all paths if omitted
175
+ tracker.subscribe(fn); // returns unsubscribe
167
176
 
168
- tracker.flush(); // immediately POST all queued events
169
- tracker.subscribe(fn); // register an event listener; returns an unsubscribe fn
177
+ // Feature flags (requires `secretKey` + valid `endpoint`)
178
+ tracker.identify({ email: "u@example.com", plan: "pro" });
179
+ tracker.getFlags();
180
+ tracker.isFeatureEnabled("my-flag");
181
+ tracker.onFlagsChange((flags) => { /* ... */ }); // unsubscribe fn
182
+ void tracker.fetchFlags();
170
183
  ```
171
184
 
172
- **Heartbeat:** The SDK emits a `session:heartbeat` event every 30 seconds to keep the session alive.
185
+ **Heartbeat:** Every 30s while the tab is visible, a POST is sent to `{endpoint}/heartbeat` with session and visitor ids.
186
+
187
+ **Deactivate:** On tab hide / `pagehide`, a beacon marks the session inactive and flushes queued analytics via `sendBeacon` when possible.
173
188
 
174
- **Page-hide flush:** On `visibilitychange` (tab hidden or browser minimised) the SDK calls `navigator.sendBeacon` to ensure no events are dropped.
189
+ **Batching:** Analytics events are POSTed as JSON to `{endpoint}/batch` with `visitorId`, optional `location`, and an `events` array.
175
190
 
176
191
  ### `LogCapture`
177
192
 
178
- Automatically used by `UserTracker` when `trackLogs: true`. Can also be used standalone:
193
+ Used internally when `trackLogs: true`. Sends structured entries to `{apiBase}/logs/ingest` (API base is derived by stripping the last path segment from your `endpoint`, e.g. `…/api/events` → `…/api`).
179
194
 
180
195
  ```ts
181
- import { LogCapture } from 'alphana-sdk';
196
+ import { LogCapture } from "alphana-sdk";
182
197
 
183
198
  const capture = new LogCapture({
184
- endpoint: 'https://your-backend.com/api/events',
185
- sessionId: 'ses_abc123',
186
- appId: 'YOUR_APP_ID',
187
- secretKey: 'sk_...',
199
+ endpoint: "https://api.alphana.ir/api/events",
200
+ sessionId: "ses_abc123",
201
+ appId: "YOUR_APP_ID",
202
+ secretKey: "sk_...",
188
203
  });
189
204
 
190
- capture.init(); // patches console.info/warn/error, window.onerror, unhandledrejection
191
- capture.destroy(); // restores original methods
205
+ capture.init();
206
+ capture.capture("error", "Something failed", { stack: err.stack });
207
+ capture.destroy();
192
208
  ```
193
209
 
194
210
  ### `renderHeatmap`
195
211
 
196
- Renders a `HeatmapPoint[]` array onto a `<canvas>` element using a blue → red color palette.
212
+ Draws `HeatmapPoint[]` on a `<canvas>` (blue → red).
197
213
 
198
214
  ```ts
199
- import { renderHeatmap } from 'alphana-sdk';
215
+ import { renderHeatmap } from "alphana-sdk";
200
216
 
201
- const canvas = document.getElementById('heatmap') as HTMLCanvasElement;
202
- canvas.width = 1280;
217
+ const canvas = document.getElementById("heatmap") as HTMLCanvasElement;
218
+ canvas.width = 1280;
203
219
  canvas.height = 720;
204
220
 
205
221
  renderHeatmap(canvas, points, {
206
- radius: 25, // blur radius in px (default: 25)
207
- maxOpacity: 0.85, // max alpha for hotspots (default: 0.85)
208
- minOpacity: 0, // min alpha for cold areas (default: 0)
222
+ radius: 25,
223
+ maxOpacity: 0.85,
224
+ minOpacity: 0,
209
225
  });
210
226
  ```
211
227
 
212
228
  ### `DEFAULT_ENDPOINT`
213
229
 
214
- The default API URL is exported if you need it:
215
-
216
230
  ```ts
217
- import { DEFAULT_ENDPOINT } from 'alphana-sdk';
231
+ import { DEFAULT_ENDPOINT } from "alphana-sdk";
218
232
  // "https://api.alphana.ir/api/events"
219
233
  ```
220
234
 
@@ -222,29 +236,39 @@ import { DEFAULT_ENDPOINT } from 'alphana-sdk';
222
236
 
223
237
  ## React hooks
224
238
 
225
- All hooks are exported from `alphana/react`.
239
+ All hooks are exported from **`alphana-sdk/react`**.
240
+
241
+ `useTracker()` returns `UserTracker | null` (null outside a provider).
226
242
 
227
- | Hook | Returns | Description |
228
- | ------------------------------------ | ------------------ | ------------------------------------------------------------------ |
229
- | `useTracker()` | `UserTracker` | Access the tracker instance from context. |
230
- | `usePageView(path?)` | `void` | Record a page view when `path` changes (App Router helper). |
231
- | `useHeatmapData(path?, refreshMs?)` | `HeatmapPoint[]` | Live heatmap points for a path, polled every `refreshMs` (500 ms). |
232
- | `usePageViews()` | `PageView[]` | All page views recorded in the current session. |
233
- | `useTimeSpent()` | `number` (seconds) | Total time spent on the current page (updates every second). |
243
+ | Hook | Returns | Description |
244
+ | ---- | ------- | ----------- |
245
+ | `useTracker()` | `UserTracker \| null` | Tracker from context. |
246
+ | `usePageView(path?)` | `void` | When `path` is defined, calls `trackPageView(path)` on change. No-op if `path` is omitted. |
247
+ | `useHeatmapData(path?, refreshMs?)` | `HeatmapPoint[]` | Live points for a path; optional debounced refresh (default 500 ms). |
248
+ | `usePageViews()` | `PageView[]` | Page views in the current session. |
249
+ | `useTimeSpent()` | `Record<string, number>` | Cumulative **milliseconds** per path. |
250
+ | `useFeatureFlags()` | `Record<string, boolean>` | Evaluated flags; updates after `identify()`. |
251
+ | `useFeatureFlagEnabled(key)` | `boolean` | Whether `key` is enabled. |
234
252
 
235
253
  ```tsx
236
- import { useTracker, useTimeSpent, useHeatmapData } from 'alphana-sdk/react';
254
+ import {
255
+ useTracker,
256
+ useTimeSpent,
257
+ useHeatmapData,
258
+ } from "alphana-sdk/react";
237
259
 
238
260
  export function DebugPanel() {
239
- const tracker = useTracker();
240
- const timeSpent = useTimeSpent(); // seconds on this page
241
- const points = useHeatmapData(); // HeatmapPoint[]
261
+ const tracker = useTracker();
262
+ const timeByPath = useTimeSpent();
263
+ const points = useHeatmapData();
264
+
265
+ if (!tracker) return null;
242
266
 
243
267
  return (
244
268
  <div>
245
- <p>Time on page: {timeSpent}s</p>
246
- <p>Heatmap points: {points.length}</p>
247
- <button onClick={() => tracker.flush()}>Flush now</button>
269
+ <p>Paths tracked: {Object.keys(timeByPath).length}</p>
270
+ <p>Heatmap points (this page): {points.length}</p>
271
+ <button type="button" onClick={() => tracker.flush()}>Flush now</button>
248
272
  </div>
249
273
  );
250
274
  }
@@ -263,31 +287,22 @@ import type {
263
287
  HeatmapPoint,
264
288
  SessionData,
265
289
  GeoLocation,
290
+ HeatmapRenderOptions,
291
+ RageClick,
292
+ UTurn,
266
293
  LogLevel,
267
294
  LogEntry,
268
- HeatmapRenderOptions,
269
- } from 'alphana-sdk';
295
+ } from "alphana-sdk";
270
296
  ```
271
297
 
272
298
  ---
273
299
 
274
- ## Self-hosting
300
+ ## WordPress
275
301
 
276
- The full backend, dashboard, and landing page are open source. To run on your own infrastructure:
302
+ For WordPress sites, use the **Alphana Tracker** plugin (ZIP):
277
303
 
278
- ```bash
279
- git clone https://github.com/teokamalipour/alphana-sdk.git
280
- cd alphana-sdk
281
- cp .env.example .env # fill in your values
282
- docker compose up -d # starts MongoDB, NestJS backend, and React dashboard
283
- ```
284
-
285
- Then point the SDK at your server:
304
+ **Download:** [https://storage.alphana.ir/cdn/alphana-tracker.zip](https://storage.alphana.ir/cdn/alphana-tracker.zip)
286
305
 
287
- ```ts
288
- new UserTracker({
289
- appId: 'YOUR_APP_ID',
290
- secretKey: 'YOUR_SECRET_KEY',
291
- endpoint: 'https://your-server.example.com/api/events',
292
- });
293
- ```
306
+ 1. In WordPress admin: **Plugins → Add New → Upload Plugin** and choose the ZIP, or unzip into `wp-content/plugins/alphana-tracker/`.
307
+ 2. Activate **Alphana Tracker**.
308
+ 3. Open **Settings → Alphana Tracker**, enter **App ID** and **Secret Key** from the Alphana dashboard, enable tracking, and save.
@@ -1 +1 @@
1
- "use strict";var AlphanaSDK=(()=>{var I=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var G=(o,t)=>{for(var e in t)I(o,e,{get:t[e],enumerable:!0})},W=(o,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of F(t))!V.call(o,n)&&n!==e&&I(o,n,{get:()=>t[n],enumerable:!(i=j(t,n))||i.enumerable});return o};var z=o=>W(I({},"__esModule",{value:!0}),o);var Y={};G(Y,{DEFAULT_ENDPOINT:()=>x,LogCapture:()=>g,UserTracker:()=>T,renderHeatmap:()=>_});var O="__ut_vid__";function y(){return typeof crypto!="undefined"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,o=>{let t=Math.random()*16|0;return(o==="x"?t:t&3|8).toString(16)})}function D(){if(typeof localStorage=="undefined")return y();try{let o=localStorage.getItem(O);if(o)return o;let t=y();return localStorage.setItem(O,t),t}catch(o){return y()}}async function M(){try{let o=await fetch("https://ipapi.co/json/",{method:"GET",headers:{Accept:"application/json"}});if(!o.ok)return null;let t=await o.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(o){return null}}var b=class b{constructor({emit:t,sessionId:e}){this.previousPath="";this.originalPushState=null;this.originalReplaceState=null;this.pageEntryTime=0;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;if(t!==this.previousPath){if(this.previousPath&&this.pageEntryTime>0){let e=Date.now()-this.pageEntryTime;e<=b.UTURN_THRESHOLD_MS&&this.emit({type:"uturn",data:{fromPath:this.previousPath,toPath:t,timeOnPageMs:e,timestamp:Date.now(),sessionId:this.sessionId}})}this.recordPageView(t)}}recordPageView(t){this.previousPath=t,this.pageEntryTime=Date.now(),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}}))}};b.UTURN_THRESHOLD_MS=5e3;var k=b;var P=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 R(o,t){let e=0;return(...i)=>{let n=Date.now();n-e>=t&&(e=n,o(...i))}}var h=class h{constructor({emit:t,sessionId:e,sampleRate:i=.3,maxPoints:n=2e3,allowedPaths:s}){this.currentPath="";this.pointCounts={};this.recentClicks=[];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,pw:e,ph:i,type:"move"})};this.handleClick=t=>{if(Date.now()-this.lastTouchTime<500)return;let e=document.documentElement.scrollWidth,i=document.documentElement.scrollHeight,n=t.clientY+window.scrollY,s=this.getClickTarget(t);this.recordPoint({x:t.clientX,y:n,xPct:e>0?t.clientX/e*100:0,yPct:i>0?n/i*100:0,pw:e,ph:i,type:"click",...s?{target:s}:{}}),this.checkRageClick(t.clientX,n,this.currentPath)};this.lastTouchTime=0;this.handleTouchEnd=t=>{let e=t.changedTouches[0];if(!e)return;let i=document.documentElement.scrollWidth,n=document.documentElement.scrollHeight,s=e.clientY+window.scrollY;this.lastTouchTime=Date.now(),this.recordPoint({x:e.clientX,y:s,xPct:i>0?e.clientX/i*100:0,yPct:n>0?s/n*100:0,pw:i,ph:n,type:"click"}),this.checkRageClick(e.clientX,s,this.currentPath)};this.handleScroll=()=>{if(Math.random()>this.sampleRate)return;let t=document.documentElement.scrollWidth,e=document.documentElement.scrollHeight,i=window.innerWidth,n=window.scrollX,s=window.scrollY,r=n+i/2;this.recordPoint({x:i/2,y:s,xPct:t>0?r/t*100:50,yPct:e>0?s/e*100:0,pw:t,ph:e,type:"scroll"})};this.handleNavigate=t=>{this.currentPath=t.detail.path};this.emit=t,this.sessionId=e,this.sampleRate=i,this.maxPoints=n,this.allowedPaths=s&&s.length>0?new Set(s):null,this.throttledMouseMove=R(this.handleMouseMove,50),this.throttledScroll=R(this.handleScroll,100)}init(){this.currentPath=window.location.pathname+window.location.search,document.addEventListener("mousemove",this.throttledMouseMove),document.addEventListener("click",this.handleClick),document.addEventListener("touchend",this.handleTouchEnd,{passive:!0}),window.addEventListener("scroll",this.throttledScroll,{passive:!0}),window.addEventListener("tracker:navigate",this.handleNavigate)}destroy(){document.removeEventListener("mousemove",this.throttledMouseMove),document.removeEventListener("click",this.handleClick),document.removeEventListener("touchend",this.handleTouchEnd),window.removeEventListener("scroll",this.throttledScroll),window.removeEventListener("tracker:navigate",this.handleNavigate)}canRecord(){var t;return this.allowedPaths!==null&&!this.allowedPaths.has(this.currentPath)?!1:((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()}}))}getClickTarget(t){var n,s,r,a;let e=t.target;return e&&(e.getAttribute("aria-label")||((n=e.closest("[aria-label]"))==null?void 0:n.getAttribute("aria-label"))||e.getAttribute("data-track-label")||e.getAttribute("id")||(e instanceof HTMLButtonElement||e instanceof HTMLAnchorElement?(s=e.innerText)==null?void 0:s.trim().slice(0,60):(a=(r=e.closest("button, a"))==null?void 0:r.textContent)==null?void 0:a.trim().slice(0,60)))||void 0}checkRageClick(t,e,i){let n=Date.now();this.recentClicks=this.recentClicks.filter(r=>n-r.t<h.RAGE_WINDOW_MS),this.recentClicks.push({x:t,y:e,t:n});let s=this.recentClicks.filter(r=>Math.hypot(r.x-t,r.y-e)<=h.RAGE_RADIUS_PX);s.length>=h.RAGE_THRESHOLD&&(this.emit({type:"rageclik",data:{path:i,x:t,y:e,count:s.length,timestamp:n,sessionId:this.sessionId}}),this.recentClicks=[])}};h.RAGE_THRESHOLD=3,h.RAGE_WINDOW_MS=1e3,h.RAGE_RADIUS_PX=50;var S=h;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`,r=JSON.stringify(n);fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.authHeaders},body:r,keepalive:!0}).catch(a=>{this.origError&&this.origError("[user-tracker] Failed to send log:",a)})}};var x="https://api.alphana.ir/api/events",K={endpoint:x,trackNavigation:!0,trackTime:!0,trackHeatmap:!0,trackLogs:!0,mouseSampleRate:.3,maxHeatmapPoints:2e3,batchSize:20,flushInterval:5e3},T=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 e;this.cfg={...K,...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:y(),visitorId:D(),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 k({emit:t,sessionId:e}),this.navigation.init()),this.cfg.trackTime&&(this.time=new P({emit:t,sessionId:e}),this.time.init()),this.cfg.trackHeatmap&&(this.heatmap=new S({emit:t,sessionId:e,sampleRate:this.cfg.mouseSampleRate,maxPoints:this.cfg.maxHeatmapPoints,allowedPaths:this.cfg.heatmapPages}),this.heatmap.init()),this.cfg.endpoint&&(this.flushTimer=setInterval(()=>{this.queue.length>0&&this.flush()},this.cfg.flushInterval),M().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.initialized=!0,this}destroy(){var t,e,i,n;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(),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,visitorId:this.session.visitorId,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 r=this.session.heatmap[s];r.length<this.cfg.maxHeatmapPoints&&r.push(t.data);break}case"rageclik":case"uturn":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}:{},visitorId:this.session.visitorId,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){}}};function X(){let o=[[0,0,255],[0,255,255],[0,255,0],[255,255,0],[255,128,0],[255,0,0]],t=[],e=51;for(let i=0;i<o.length-1;i++){let[n,s,r]=o[i],[a,l,m]=o[i+1];for(let d=0;d<e;d++){let v=d/e;t.push([Math.round(n+(a-n)*v),Math.round(s+(l-s)*v),Math.round(r+(m-r)*v)])}}return t}var N=X();function _(o,t,e={}){let{radius:i=25,maxOpacity:n=.85,minOpacity:s=0}=e,r=o.getContext("2d");if(!r)return;let{width:a,height:l}=o;if(r.clearRect(0,0,a,l),t.length===0)return;let m=document.createElement("canvas");m.width=a,m.height=l;let d=m.getContext("2d");if(!d)return;for(let c of t){let p=c.xPct/100*a,u=c.yPct/100*l,E=c.type==="click"?i*1.6:i,f=d.createRadialGradient(p,u,0,p,u,E);f.addColorStop(0,"rgba(0,0,0,0.15)"),f.addColorStop(1,"rgba(0,0,0,0)"),d.fillStyle=f,d.beginPath(),d.arc(p,u,E,0,Math.PI*2),d.fill()}let v=d.getImageData(0,0,a,l),L=r.createImageData(a,l),C=v.data,w=L.data,H=N.length-1;for(let c=0;c<C.length;c+=4){let p=C[c+3];if(p===0)continue;let u=p/255,E=Math.min(Math.floor(u*H),H),[f,A,B]=N[E],U=s+u*(n-s);w[c]=f,w[c+1]=A,w[c+2]=B,w[c+3]=Math.round(U*255)}r.putImageData(L,0,0)}return z(Y);})();
1
+ "use strict";var AlphanaSDK=(()=>{var m=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var S=(r,t)=>{for(var e in t)m(r,e,{get:t[e],enumerable:!0})},T=(r,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of k(t))!P.call(r,n)&&n!==e&&m(r,n,{get:()=>t[n],enumerable:!(i=b(t,n))||i.enumerable});return r};var I=r=>T(m({},"__esModule",{value:!0}),r);var x={};S(x,{DEFAULT_ENDPOINT:()=>y,LogCapture:()=>d,UserTracker:()=>v});var w="__ut_vid__";function h(){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)})}function E(){if(typeof localStorage=="undefined")return h();try{let r=localStorage.getItem(w);if(r)return r;let t=h();return localStorage.setItem(w,t),t}catch(r){return h()}}var p=class p{constructor({emit:t,sessionId:e}){this.previousPath="";this.originalPushState=null;this.originalReplaceState=null;this.pageEntryTime=0;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;if(t!==this.previousPath){if(this.previousPath&&this.pageEntryTime>0){let e=Date.now()-this.pageEntryTime;e<=p.UTURN_THRESHOLD_MS&&this.emit({type:"uturn",data:{fromPath:this.previousPath,toPath:t,timeOnPageMs:e,timestamp:Date.now(),sessionId:this.sessionId}})}this.recordPageView(t)}}recordPageView(t){this.previousPath=t,this.pageEntryTime=Date.now(),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}}))}};p.UTURN_THRESHOLD_MS=5e3;var l=p;var u=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 f(r,t){let e=0;return(...i)=>{let n=Date.now();n-e>=t&&(e=n,r(...i))}}var o=class o{constructor({emit:t,sessionId:e,sampleRate:i=.3,maxPoints:n=2e3,allowedPaths:s}){this.currentPath="";this.pointCounts={};this.recentClicks=[];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,pw:e,ph:i,type:"move"})};this.handleClick=t=>{if(Date.now()-this.lastTouchTime<500)return;let e=document.documentElement.scrollWidth,i=document.documentElement.scrollHeight,n=t.clientY+window.scrollY,s=this.getClickTarget(t);this.recordPoint({x:t.clientX,y:n,xPct:e>0?t.clientX/e*100:0,yPct:i>0?n/i*100:0,pw:e,ph:i,type:"click",...s?{target:s}:{}}),this.checkRageClick(t.clientX,n,this.currentPath)};this.lastTouchTime=0;this.handleTouchEnd=t=>{let e=t.changedTouches[0];if(!e)return;let i=document.documentElement.scrollWidth,n=document.documentElement.scrollHeight,s=e.clientY+window.scrollY;this.lastTouchTime=Date.now(),this.recordPoint({x:e.clientX,y:s,xPct:i>0?e.clientX/i*100:0,yPct:n>0?s/n*100:0,pw:i,ph:n,type:"click"}),this.checkRageClick(e.clientX,s,this.currentPath)};this.handleScroll=()=>{if(Math.random()>this.sampleRate)return;let t=document.documentElement.scrollWidth,e=document.documentElement.scrollHeight,i=window.innerWidth,n=window.scrollX,s=window.scrollY,a=n+i/2;this.recordPoint({x:i/2,y:s,xPct:t>0?a/t*100:50,yPct:e>0?s/e*100:0,pw:t,ph:e,type:"scroll"})};this.handleNavigate=t=>{this.currentPath=t.detail.path};this.emit=t,this.sessionId=e,this.sampleRate=i,this.maxPoints=n,this.allowedPaths=s&&s.length>0?new Set(s):null,this.throttledMouseMove=f(this.handleMouseMove,50),this.throttledScroll=f(this.handleScroll,100)}init(){this.currentPath=window.location.pathname+window.location.search,document.addEventListener("mousemove",this.throttledMouseMove),document.addEventListener("click",this.handleClick),document.addEventListener("touchend",this.handleTouchEnd,{passive:!0}),window.addEventListener("scroll",this.throttledScroll,{passive:!0}),window.addEventListener("tracker:navigate",this.handleNavigate)}destroy(){document.removeEventListener("mousemove",this.throttledMouseMove),document.removeEventListener("click",this.handleClick),document.removeEventListener("touchend",this.handleTouchEnd),window.removeEventListener("scroll",this.throttledScroll),window.removeEventListener("tracker:navigate",this.handleNavigate)}canRecord(){var t;return this.allowedPaths!==null&&!this.allowedPaths.has(this.currentPath)?!1:((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()}}))}getClickTarget(t){var n,s,a,c;let e=t.target;return e&&(e.getAttribute("aria-label")||((n=e.closest("[aria-label]"))==null?void 0:n.getAttribute("aria-label"))||e.getAttribute("data-track-label")||e.getAttribute("id")||(e instanceof HTMLButtonElement||e instanceof HTMLAnchorElement?(s=e.innerText)==null?void 0:s.trim().slice(0,60):(c=(a=e.closest("button, a"))==null?void 0:a.textContent)==null?void 0:c.trim().slice(0,60)))||void 0}checkRageClick(t,e,i){let n=Date.now();this.recentClicks=this.recentClicks.filter(a=>n-a.t<o.RAGE_WINDOW_MS),this.recentClicks.push({x:t,y:e,t:n});let s=this.recentClicks.filter(a=>Math.hypot(a.x-t,a.y-e)<=o.RAGE_RADIUS_PX);s.length>=o.RAGE_THRESHOLD&&(this.emit({type:"rageclik",data:{path:i,x:t,y:e,count:s.length,timestamp:n,sessionId:this.sessionId}}),this.recentClicks=[])}};o.RAGE_THRESHOLD=3,o.RAGE_WINDOW_MS=1e3,o.RAGE_RADIUS_PX=50;var g=o;var d=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`,a=JSON.stringify(n);fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.authHeaders},body:a,keepalive:!0}).catch(c=>{this.origError&&this.origError("[user-tracker] Failed to send log:",c)})}};var y="https://api.alphana.ir/api/events",R={endpoint:y,trackNavigation:!0,trackTime:!0,trackHeatmap:!0,trackLogs:!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.userProperties={};this.flags={};this.flagSubscribers=new Set;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 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:h(),visitorId:E(),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 l({emit:t,sessionId:e}),this.navigation.init()),this.cfg.trackTime&&(this.time=new u({emit:t,sessionId:e}),this.time.init()),this.cfg.trackHeatmap&&(this.heatmap=new g({emit:t,sessionId:e,sampleRate:this.cfg.mouseSampleRate,maxPoints:this.cfg.maxHeatmapPoints,allowedPaths:this.cfg.heatmapPages}),this.heatmap.init()),this.cfg.endpoint&&(this.flushTimer=setInterval(()=>{this.queue.length>0&&this.flushQueue()},this.cfg.flushInterval),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 d({endpoint:this.cfg.endpoint,sessionId:this.session.id,secretKey:this.cfg.secretKey,appId:this.cfg.appId}),this.logCapture.init())),this.initialized=!0,this.fetchFlags(),this}destroy(){var t,e,i,n;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(),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,visitorId:this.session.visitorId,path:typeof window!="undefined"?window.location.pathname:"/",active:!0,...this.cfg.appId?{appId:this.cfg.appId}:{}});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 a=this.session.heatmap[s];a.length<this.cfg.maxHeatmapPoints&&a.push(t.data);break}case"rageclik":case"uturn":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.flushQueue())}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}}))}identify(t){this.userProperties={...this.userProperties,...t},this.fetchFlags()}getFlags(){return{...this.flags}}isFeatureEnabled(t){return this.flags[t]===!0}onFlagsChange(t){return this.flagSubscribers.add(t),()=>this.flagSubscribers.delete(t)}async fetchFlags(){if(!this.cfg.endpoint||!this.cfg.secretKey)return;let t=this.cfg.endpoint.replace(/\/events$/,"")+"/feature-flags/evaluate";try{let e=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.cfg.secretKey}`},body:JSON.stringify({userProperties:this.userProperties})});if(!e.ok)return;let i=await e.json();this.flags=i,this.flagSubscribers.forEach(n=>n({...this.flags}))}catch(e){}}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),{})}flush(){this.flushQueue()}async flushQueue(){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){return JSON.stringify({...this.cfg.appId?{appId:this.cfg.appId}:{},visitorId:this.session.visitorId,events:t.map(e=>({sessionId:this.session.id,type:e.type,data:e.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){}}};return I(x);})();
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  interface TrackerConfig {
2
2
  /**
3
3
  * URL to POST events to.
4
- * Defaults to `https://api.alphana.ir/api/events` override only if self-hosting.
4
+ * Defaults to `https://api.alphana.ir/api/events` (Alphana Cloud).
5
5
  */
6
6
  endpoint?: string;
7
7
  /**
@@ -214,6 +214,7 @@ declare class LogCapture {
214
214
 
215
215
  declare const DEFAULT_ENDPOINT = "https://api.alphana.ir/api/events";
216
216
  type SubscriberFn = (event: TrackerEvent) => void;
217
+ type FlagSubscriberFn = (flags: Record<string, boolean>) => void;
217
218
  declare class UserTracker {
218
219
  private readonly cfg;
219
220
  private session;
@@ -228,7 +229,9 @@ declare class UserTracker {
228
229
  private queue;
229
230
  private flushTimer;
230
231
  private heartbeatTimer;
231
- private location;
232
+ private userProperties;
233
+ private flags;
234
+ private readonly flagSubscribers;
232
235
  constructor(config?: TrackerConfig);
233
236
  /**
234
237
  * Attach event listeners and start tracking.
@@ -274,6 +277,38 @@ declare class UserTracker {
274
277
  * ```
275
278
  */
276
279
  trackPageView(path?: string): void;
280
+ /**
281
+ * Identify the current user with a set of properties.
282
+ * Properties are merged with any previously set ones and used for feature
283
+ * flag targeting (e.g. `email`, `plan`, `country`).
284
+ * Automatically triggers a flag re-evaluation.
285
+ *
286
+ * ```ts
287
+ * tracker.identify({ email: 'user@example.com', plan: 'pro' });
288
+ * ```
289
+ */
290
+ identify(properties: Record<string, string>): void;
291
+ /**
292
+ * Returns the latest evaluated feature flags as `{ [key]: boolean }`.
293
+ * Flags are fetched on `init()` and after every `identify()` call.
294
+ */
295
+ getFlags(): Record<string, boolean>;
296
+ /**
297
+ * Returns `true` if the given flag is currently enabled for this user.
298
+ * Returns `false` for unknown flags or before flags have loaded.
299
+ */
300
+ isFeatureEnabled(flagKey: string): boolean;
301
+ /**
302
+ * Subscribe to flag changes. Called whenever flags are re-evaluated.
303
+ * Returns an unsubscribe function.
304
+ *
305
+ * ```ts
306
+ * const unsub = tracker.onFlagsChange(flags => console.log(flags));
307
+ * ```
308
+ */
309
+ onFlagsChange(fn: FlagSubscriberFn): () => void;
310
+ /** Fetch (or re-fetch) flags from the backend evaluate endpoint. */
311
+ fetchFlags(): Promise<void>;
277
312
  /** A read-only snapshot of the current session. */
278
313
  getSession(): Readonly<SessionData>;
279
314
  /** All page views recorded so far. */
@@ -284,8 +319,13 @@ declare class UserTracker {
284
319
  getHeatmapData(path: string): HeatmapPoint[];
285
320
  /** Heatmap points for all tracked paths. */
286
321
  getHeatmapData(): Record<string, HeatmapPoint[]>;
322
+ /**
323
+ * Immediately drain the event queue and POST pending events to `/batch`.
324
+ * Fire-and-forget; errors are intentionally swallowed like other analytics calls.
325
+ */
326
+ flush(): void;
287
327
  /** Drain the queue and POST all pending events to the batch endpoint. */
288
- private flush;
328
+ private flushQueue;
289
329
  /**
290
330
  * Synchronous best-effort flush via `navigator.sendBeacon`.
291
331
  * Used on `pagehide` / `visibilitychange:hidden` where async fetch may be
@@ -296,24 +336,4 @@ declare class UserTracker {
296
336
  private sendBatch;
297
337
  }
298
338
 
299
- /**
300
- * Renders `points` onto `canvas` as a color heatmap.
301
- *
302
- * Algorithm:
303
- * 1. Draw each point as a soft radial gradient on an off-screen canvas,
304
- * accumulating "heat" in the alpha channel.
305
- * 2. Map each pixel's accumulated alpha value through the color palette
306
- * (blue → cyan → green → yellow → orange → red) and write to the
307
- * destination canvas.
308
- *
309
- * Coordinates in `HeatmapPoint` are percentages (0–100) of page dimensions,
310
- * which are scaled to the canvas size at render time — making it resolution
311
- * independent.
312
- *
313
- * @param canvas Target canvas element (will NOT be resized automatically).
314
- * @param points Array of heatmap points to render.
315
- * @param options Visual tuning options.
316
- */
317
- declare function renderHeatmap(canvas: HTMLCanvasElement, points: HeatmapPoint[], options?: HeatmapRenderOptions): void;
318
-
319
- export { DEFAULT_ENDPOINT, type GeoLocation, type HeatmapPoint, type HeatmapRenderOptions, LogCapture, type LogEntry, type LogLevel, type PageView, type SessionData, type TimeSpent, type TrackerConfig, type TrackerEvent, UserTracker, renderHeatmap };
339
+ export { DEFAULT_ENDPOINT, type GeoLocation, type HeatmapPoint, type HeatmapRenderOptions, LogCapture, type LogEntry, type LogLevel, type PageView, type RageClick, type SessionData, type TimeSpent, type TrackerConfig, type TrackerEvent, type UTurn, UserTracker };