alphana-sdk 0.5.6 → 0.5.10
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 +132 -117
- package/dist/alphana-sdk.global.js +1 -1
- package/dist/index.d.mts +8 -24
- package/dist/index.d.ts +8 -24
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +7 -3
- package/dist/react/index.d.ts +7 -3
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1 -1
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# alphana-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](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
|
|
26
|
-
import { createRoot } from
|
|
27
|
-
import { UserTrackerProvider } from
|
|
28
|
-
import App from
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
import { UserTrackerProvider } from
|
|
58
|
-
import type { ReactNode } from
|
|
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
|
|
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>
|
|
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
|
|
116
|
+
import { UserTracker } from "alphana-sdk";
|
|
113
117
|
|
|
114
118
|
const tracker = new UserTracker({
|
|
115
|
-
appId:
|
|
116
|
-
secretKey:
|
|
119
|
+
appId: "YOUR_APP_ID",
|
|
120
|
+
secretKey: "YOUR_SECRET_KEY",
|
|
117
121
|
});
|
|
118
122
|
|
|
119
|
-
tracker.init(); //
|
|
123
|
+
tracker.init(); // safe no-op during SSR (`window` undefined)
|
|
120
124
|
|
|
121
|
-
//
|
|
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
|
|
130
|
-
|
|
|
131
|
-
| `appId`
|
|
132
|
-
| `secretKey`
|
|
133
|
-
| `endpoint`
|
|
134
|
-
| `sessionId`
|
|
135
|
-
| `trackNavigation`
|
|
136
|
-
| `trackTime`
|
|
137
|
-
| `trackHeatmap`
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
|
|
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
|
|
158
|
+
import { UserTracker } from "alphana-sdk";
|
|
159
|
+
|
|
160
|
+
const tracker = new UserTracker({ appId: "id", secretKey: "sk" });
|
|
155
161
|
|
|
156
|
-
|
|
162
|
+
tracker.init();
|
|
163
|
+
tracker.destroy();
|
|
164
|
+
|
|
165
|
+
tracker.trackPageView(path?: string);
|
|
157
166
|
|
|
158
|
-
tracker.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
169
|
-
tracker.
|
|
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:**
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
196
|
+
import { LogCapture } from "alphana-sdk";
|
|
182
197
|
|
|
183
198
|
const capture = new LogCapture({
|
|
184
|
-
endpoint:
|
|
185
|
-
sessionId:
|
|
186
|
-
appId:
|
|
187
|
-
secretKey:
|
|
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();
|
|
191
|
-
capture.
|
|
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
|
-
|
|
212
|
+
Draws `HeatmapPoint[]` on a `<canvas>` (blue → red).
|
|
197
213
|
|
|
198
214
|
```ts
|
|
199
|
-
import { renderHeatmap } from
|
|
215
|
+
import { renderHeatmap } from "alphana-sdk";
|
|
200
216
|
|
|
201
|
-
const canvas = document.getElementById(
|
|
202
|
-
canvas.width
|
|
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:
|
|
207
|
-
maxOpacity: 0.85,
|
|
208
|
-
minOpacity: 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
|
|
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
|
|
239
|
+
All hooks are exported from **`alphana-sdk/react`**.
|
|
240
|
+
|
|
241
|
+
`useTracker()` returns `UserTracker | null` (null outside a provider).
|
|
226
242
|
|
|
227
|
-
| Hook
|
|
228
|
-
|
|
|
229
|
-
| `useTracker()`
|
|
230
|
-
| `usePageView(path?)`
|
|
231
|
-
| `useHeatmapData(path?, refreshMs?)`
|
|
232
|
-
| `usePageViews()`
|
|
233
|
-
| `useTimeSpent()`
|
|
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 {
|
|
254
|
+
import {
|
|
255
|
+
useTracker,
|
|
256
|
+
useTimeSpent,
|
|
257
|
+
useHeatmapData,
|
|
258
|
+
} from "alphana-sdk/react";
|
|
237
259
|
|
|
238
260
|
export function DebugPanel() {
|
|
239
|
-
const tracker
|
|
240
|
-
const
|
|
241
|
-
const points
|
|
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>
|
|
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
|
-
|
|
269
|
-
} from 'alphana-sdk';
|
|
295
|
+
} from "alphana-sdk";
|
|
270
296
|
```
|
|
271
297
|
|
|
272
298
|
---
|
|
273
299
|
|
|
274
|
-
##
|
|
300
|
+
## WordPress
|
|
275
301
|
|
|
276
|
-
|
|
302
|
+
For WordPress sites, use the **Alphana Tracker** plugin (ZIP):
|
|
277
303
|
|
|
278
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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 U=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var G=(r,t)=>{for(var e in t)I(r,e,{get:t[e],enumerable:!0})},z=(r,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of j(t))!V.call(r,n)&&n!==e&&I(r,n,{get:()=>t[n],enumerable:!(i=U(t,n))||i.enumerable});return r};var W=r=>z(I({},"__esModule",{value:!0}),r);var X={};G(X,{DEFAULT_ENDPOINT:()=>x,LogCapture:()=>g,UserTracker:()=>T,renderHeatmap:()=>F});var O="__ut_vid__";function y(){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 D(){if(typeof localStorage=="undefined")return y();try{let r=localStorage.getItem(O);if(r)return r;let t=y();return localStorage.setItem(O,t),t}catch(r){return y()}}async function M(){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 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 E=P;var k=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(r,t){let e=0;return(...i)=>{let n=Date.now();n-e>=t&&(e=n,r(...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,o=n+i/2;this.recordPoint({x:i/2,y:s,xPct:t>0?o/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,o,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=(o=e.closest("button, a"))==null?void 0:o.textContent)==null?void 0:a.trim().slice(0,60)))||void 0}checkRageClick(t,e,i){let n=Date.now();this.recentClicks=this.recentClicks.filter(o=>n-o.t<h.RAGE_WINDOW_MS),this.recentClicks.push({x:t,y:e,t:n});let s=this.recentClicks.filter(o=>Math.hypot(o.x-t,o.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`,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)})}};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.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={...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 E({emit:t,sessionId:e}),this.navigation.init()),this.cfg.trackTime&&(this.time=new k({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.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}:{},...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}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}}))}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),{})}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 $(){let r=[[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<r.length-1;i++){let[n,s,o]=r[i],[a,l,m]=r[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(o+(m-o)*v)])}}return t}var N=$();function F(r,t,e={}){let{radius:i=25,maxOpacity:n=.85,minOpacity:s=0}=e,o=r.getContext("2d");if(!o)return;let{width:a,height:l}=r;if(o.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,b=c.type==="click"?i*1.6:i,f=d.createRadialGradient(p,u,0,p,u,b);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,b,0,Math.PI*2),d.fill()}let v=d.getImageData(0,0,a,l),L=o.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,b=Math.min(Math.floor(u*H),H),[f,_,A]=N[b],B=s+u*(n-s);w[c]=f,w[c+1]=_,w[c+2]=A,w[c+3]=Math.round(B*255)}o.putImageData(L,0,0)}return W(X);})();
|
|
1
|
+
"use strict";var AlphanaSDK=(()=>{var m=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var x=(r,t)=>{for(var e in t)m(r,e,{get:t[e],enumerable:!0})},L=(r,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of I(t))!R.call(r,n)&&n!==e&&m(r,n,{get:()=>t[n],enumerable:!(i=T(t,n))||i.enumerable});return r};var C=r=>L(m({},"__esModule",{value:!0}),r);var D={};x(D,{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 k="__ut_last_pv_path__",P="__ut_last_pv_entry_ms__";function b(r){if(typeof sessionStorage=="undefined")return null;try{return sessionStorage.getItem(r)}catch(t){return null}}function S(r,t){if(typeof sessionStorage!="undefined")try{sessionStorage.setItem(r,t)}catch(e){}}function H(r,t){S(k,r),S(P,String(t))}function _(){if(typeof performance=="undefined")return;let t=performance.getEntriesByType("navigation")[0];if(t!=null&&t.type)return t.type;let e=performance.navigation;if((e==null?void 0:e.type)===2)return"back_forward"}var c=class c{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(){let t=window.location.pathname+window.location.search;_()==="back_forward"&&this.tryEmitMpaHistoryTraversalUturn(t),this.recordPageView(t),window.addEventListener("popstate",this.handlePopState),this.originalPushState=history.pushState.bind(history);let e=this.originalPushState;history.pushState=(n,s,a)=>{e(n,s,a),this.handleNavigation()},this.originalReplaceState=history.replaceState.bind(history);let i=this.originalReplaceState;history.replaceState=(n,s,a)=>{i(n,s,a),this.handleNavigation()}}destroy(){window.removeEventListener("popstate",this.handlePopState),this.originalPushState&&(history.pushState=this.originalPushState),this.originalReplaceState&&(history.replaceState=this.originalReplaceState)}tryEmitMpaHistoryTraversalUturn(t){let e=b(k),i=b(P),n=i?Number(i):0;if(!e||!Number.isFinite(n)||n<=0||e===t)return;let s=Date.now()-n;s<0||s>c.UTURN_THRESHOLD_MS||this.emitUturn(e,t,s)}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<=c.UTURN_THRESHOLD_MS&&this.emitUturn(this.previousPath,t,e)}this.recordPageView(t)}}emitUturn(t,e,i){this.emit({type:"uturn",data:{fromPath:t,toPath:e,timeOnPageMs:i,timestamp:Date.now(),sessionId:this.sessionId}})}recordPageView(t){this.previousPath=t,this.pageEntryTime=Date.now(),H(t,this.pageEntryTime),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}}))}};c.UTURN_THRESHOLD_MS=5e3;var p=c;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,l;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):(l=(a=e.closest("button, a"))==null?void 0:a.textContent)==null?void 0:l.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(l=>{this.origError&&this.origError("[user-tracker] Failed to send log:",l)})}};var y="https://api.alphana.ir/api/events",O={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={...O,...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 p({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 C(D);})();
|
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`
|
|
4
|
+
* Defaults to `https://api.alphana.ir/api/events` (Alphana Cloud).
|
|
5
5
|
*/
|
|
6
6
|
endpoint?: string;
|
|
7
7
|
/**
|
|
@@ -229,7 +229,6 @@ declare class UserTracker {
|
|
|
229
229
|
private queue;
|
|
230
230
|
private flushTimer;
|
|
231
231
|
private heartbeatTimer;
|
|
232
|
-
private location;
|
|
233
232
|
private userProperties;
|
|
234
233
|
private flags;
|
|
235
234
|
private readonly flagSubscribers;
|
|
@@ -320,8 +319,13 @@ declare class UserTracker {
|
|
|
320
319
|
getHeatmapData(path: string): HeatmapPoint[];
|
|
321
320
|
/** Heatmap points for all tracked paths. */
|
|
322
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;
|
|
323
327
|
/** Drain the queue and POST all pending events to the batch endpoint. */
|
|
324
|
-
private
|
|
328
|
+
private flushQueue;
|
|
325
329
|
/**
|
|
326
330
|
* Synchronous best-effort flush via `navigator.sendBeacon`.
|
|
327
331
|
* Used on `pagehide` / `visibilitychange:hidden` where async fetch may be
|
|
@@ -332,24 +336,4 @@ declare class UserTracker {
|
|
|
332
336
|
private sendBatch;
|
|
333
337
|
}
|
|
334
338
|
|
|
335
|
-
|
|
336
|
-
* Renders `points` onto `canvas` as a color heatmap.
|
|
337
|
-
*
|
|
338
|
-
* Algorithm:
|
|
339
|
-
* 1. Draw each point as a soft radial gradient on an off-screen canvas,
|
|
340
|
-
* accumulating "heat" in the alpha channel.
|
|
341
|
-
* 2. Map each pixel's accumulated alpha value through the color palette
|
|
342
|
-
* (blue → cyan → green → yellow → orange → red) and write to the
|
|
343
|
-
* destination canvas.
|
|
344
|
-
*
|
|
345
|
-
* Coordinates in `HeatmapPoint` are percentages (0–100) of page dimensions,
|
|
346
|
-
* which are scaled to the canvas size at render time — making it resolution
|
|
347
|
-
* independent.
|
|
348
|
-
*
|
|
349
|
-
* @param canvas Target canvas element (will NOT be resized automatically).
|
|
350
|
-
* @param points Array of heatmap points to render.
|
|
351
|
-
* @param options Visual tuning options.
|
|
352
|
-
*/
|
|
353
|
-
declare function renderHeatmap(canvas: HTMLCanvasElement, points: HeatmapPoint[], options?: HeatmapRenderOptions): void;
|
|
354
|
-
|
|
355
|
-
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 };
|
package/dist/index.d.ts
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`
|
|
4
|
+
* Defaults to `https://api.alphana.ir/api/events` (Alphana Cloud).
|
|
5
5
|
*/
|
|
6
6
|
endpoint?: string;
|
|
7
7
|
/**
|
|
@@ -229,7 +229,6 @@ declare class UserTracker {
|
|
|
229
229
|
private queue;
|
|
230
230
|
private flushTimer;
|
|
231
231
|
private heartbeatTimer;
|
|
232
|
-
private location;
|
|
233
232
|
private userProperties;
|
|
234
233
|
private flags;
|
|
235
234
|
private readonly flagSubscribers;
|
|
@@ -320,8 +319,13 @@ declare class UserTracker {
|
|
|
320
319
|
getHeatmapData(path: string): HeatmapPoint[];
|
|
321
320
|
/** Heatmap points for all tracked paths. */
|
|
322
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;
|
|
323
327
|
/** Drain the queue and POST all pending events to the batch endpoint. */
|
|
324
|
-
private
|
|
328
|
+
private flushQueue;
|
|
325
329
|
/**
|
|
326
330
|
* Synchronous best-effort flush via `navigator.sendBeacon`.
|
|
327
331
|
* Used on `pagehide` / `visibilitychange:hidden` where async fetch may be
|
|
@@ -332,24 +336,4 @@ declare class UserTracker {
|
|
|
332
336
|
private sendBatch;
|
|
333
337
|
}
|
|
334
338
|
|
|
335
|
-
|
|
336
|
-
* Renders `points` onto `canvas` as a color heatmap.
|
|
337
|
-
*
|
|
338
|
-
* Algorithm:
|
|
339
|
-
* 1. Draw each point as a soft radial gradient on an off-screen canvas,
|
|
340
|
-
* accumulating "heat" in the alpha channel.
|
|
341
|
-
* 2. Map each pixel's accumulated alpha value through the color palette
|
|
342
|
-
* (blue → cyan → green → yellow → orange → red) and write to the
|
|
343
|
-
* destination canvas.
|
|
344
|
-
*
|
|
345
|
-
* Coordinates in `HeatmapPoint` are percentages (0–100) of page dimensions,
|
|
346
|
-
* which are scaled to the canvas size at render time — making it resolution
|
|
347
|
-
* independent.
|
|
348
|
-
*
|
|
349
|
-
* @param canvas Target canvas element (will NOT be resized automatically).
|
|
350
|
-
* @param points Array of heatmap points to render.
|
|
351
|
-
* @param options Visual tuning options.
|
|
352
|
-
*/
|
|
353
|
-
declare function renderHeatmap(canvas: HTMLCanvasElement, points: HeatmapPoint[], options?: HeatmapRenderOptions): void;
|
|
354
|
-
|
|
355
|
-
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 };
|