@arthurreira/analytics 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -50
- package/dist/af-analytics.umd.js +245 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,29 +1,95 @@
|
|
|
1
1
|
# @arthurreira/analytics
|
|
2
2
|
|
|
3
|
-
Lightweight analytics SDK
|
|
3
|
+
Lightweight analytics SDK with three usage modes:
|
|
4
|
+
|
|
5
|
+
| Mode | Use when |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `<Analytics />` component | Next.js / React apps — zero-config, drop it in the root layout |
|
|
8
|
+
| `useAnalytics` hook | React, need custom events from individual components |
|
|
9
|
+
| Vanilla script tag | Any HTML page — no framework required |
|
|
10
|
+
|
|
11
|
+
Tracks pageviews, clicks, scroll depth, text copies, JS errors, CTAs, searches, and session duration — with automatic session management and optional real-time presence via WebSocket.
|
|
12
|
+
|
|
13
|
+
---
|
|
4
14
|
|
|
5
15
|
## Installation
|
|
6
16
|
|
|
17
|
+
### React / Next.js
|
|
18
|
+
|
|
7
19
|
```bash
|
|
8
20
|
pnpm add @arthurreira/analytics
|
|
9
21
|
# or
|
|
10
22
|
npm install @arthurreira/analytics
|
|
11
|
-
# or
|
|
12
|
-
yarn add @arthurreira/analytics
|
|
13
23
|
```
|
|
14
24
|
|
|
15
25
|
**Peer dependencies:** `react ^18 || ^19`, `next ^14 || ^15 || ^16`
|
|
16
26
|
|
|
27
|
+
### Vanilla JS (no package manager)
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<script
|
|
31
|
+
src="https://unpkg.com/@arthurreira/analytics@0.12.0/dist/af-analytics.umd.js"
|
|
32
|
+
data-api-key="proj_xxx"
|
|
33
|
+
data-url="https://your-edge-worker.dev"
|
|
34
|
+
></script>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Drops a global `AfAnalytics` object and **auto-initialises on `DOMContentLoaded`** — no other code needed.
|
|
38
|
+
|
|
39
|
+
### WordPress
|
|
40
|
+
|
|
41
|
+
#### Option A — `functions.php` (recommended for theme/child-theme)
|
|
42
|
+
|
|
43
|
+
Add to your theme's `functions.php`:
|
|
44
|
+
|
|
45
|
+
```php
|
|
46
|
+
function af_analytics_enqueue() {
|
|
47
|
+
wp_enqueue_script(
|
|
48
|
+
'af-analytics',
|
|
49
|
+
'https://unpkg.com/@arthurreira/analytics@0.12.0/dist/af-analytics.umd.js',
|
|
50
|
+
[],
|
|
51
|
+
null,
|
|
52
|
+
true // load in footer
|
|
53
|
+
);
|
|
54
|
+
// Attach data attributes after the script tag is output
|
|
55
|
+
add_filter( 'script_loader_tag', function ( $tag, $handle ) {
|
|
56
|
+
if ( $handle !== 'af-analytics' ) return $tag;
|
|
57
|
+
return str_replace(
|
|
58
|
+
'<script ',
|
|
59
|
+
'<script data-api-key="proj_xxx" data-url="https://your-edge-worker.dev" ',
|
|
60
|
+
$tag
|
|
61
|
+
);
|
|
62
|
+
}, 10, 2 );
|
|
63
|
+
}
|
|
64
|
+
add_action( 'wp_enqueue_scripts', 'af_analytics_enqueue' );
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### Option B — "Insert Headers and Footers" plugin (no code)
|
|
68
|
+
|
|
69
|
+
1. Install the [Insert Headers and Footers](https://wordpress.org/plugins/insert-headers-and-footers/) plugin.
|
|
70
|
+
2. Go to **Settings → Insert Headers and Footers**.
|
|
71
|
+
3. Paste the snippet below into the **Scripts in Footer** box and save:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<script
|
|
75
|
+
src="https://unpkg.com/@arthurreira/analytics@0.12.0/dist/af-analytics.umd.js"
|
|
76
|
+
data-api-key="proj_xxx"
|
|
77
|
+
data-url="https://your-edge-worker.dev"
|
|
78
|
+
></script>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Replace `proj_xxx` with your project API key and `https://your-edge-worker.dev` with your Edge Worker URL. The script auto-tracks pageviews, clicks, scroll depth, and JS errors with no further configuration.
|
|
82
|
+
|
|
17
83
|
---
|
|
18
84
|
|
|
19
85
|
## Quick start
|
|
20
86
|
|
|
21
|
-
### Option A —
|
|
87
|
+
### Option A — `<Analytics />` component (Next.js, recommended)
|
|
22
88
|
|
|
23
|
-
Add it once in your root layout.
|
|
89
|
+
Add it once in your root layout. Automatically tracks pageviews, clicks, scroll-depth milestones (25 / 50 / 75 / 100 %), text copies, and uncaught JS errors.
|
|
24
90
|
|
|
25
91
|
```tsx
|
|
26
|
-
// app/layout.tsx
|
|
92
|
+
// app/layout.tsx
|
|
27
93
|
import { Analytics } from '@arthurreira/analytics/client'
|
|
28
94
|
|
|
29
95
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
@@ -41,9 +107,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
41
107
|
}
|
|
42
108
|
```
|
|
43
109
|
|
|
44
|
-
### Option B — `useAnalytics` hook (
|
|
110
|
+
### Option B — `useAnalytics` hook (custom events)
|
|
45
111
|
|
|
46
|
-
Use the hook
|
|
112
|
+
Use the hook when you need to fire custom events from your own components.
|
|
47
113
|
|
|
48
114
|
```tsx
|
|
49
115
|
'use client'
|
|
@@ -65,40 +131,59 @@ export function SearchBar() {
|
|
|
65
131
|
}
|
|
66
132
|
```
|
|
67
133
|
|
|
134
|
+
### Option C — Vanilla JS script tag
|
|
135
|
+
|
|
136
|
+
Works with any HTML page or server-rendered site. Config is read from `data-*` attributes on the script tag itself.
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<script
|
|
140
|
+
src="/sdk/af-analytics.umd.js"
|
|
141
|
+
data-api-key="proj_xxx"
|
|
142
|
+
data-url="https://your-edge-worker.dev"
|
|
143
|
+
></script>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Auto-tracks the same events as the React component. No configuration code required. Call `AfAnalytics.init()` manually if you need to reinitialise after the script was loaded dynamically.
|
|
147
|
+
|
|
68
148
|
---
|
|
69
149
|
|
|
70
150
|
## API reference
|
|
71
151
|
|
|
72
|
-
### `<Analytics
|
|
152
|
+
### `<Analytics />` props
|
|
73
153
|
|
|
74
|
-
| Prop
|
|
75
|
-
|
|
|
76
|
-
| `apiUrl` | `string` | Base URL of your
|
|
77
|
-
| `apiKey` | `string` |
|
|
154
|
+
| Prop | Type | Required | Description |
|
|
155
|
+
| --- | --- | --- | --- |
|
|
156
|
+
| `apiUrl` | `string` | yes | Base URL of your analytics backend (no trailing slash) |
|
|
157
|
+
| `apiKey` | `string` | yes | Project API key |
|
|
158
|
+
| `wsUrl` | `string` | no | WebSocket URL for real-time presence (default: `wss://edge.arthurreira.dev/realtime`) |
|
|
78
159
|
|
|
79
|
-
|
|
160
|
+
Auto-tracks: **pageview**, **click**, **scroll** (depth milestones), **copy**, **error**.
|
|
80
161
|
|
|
81
162
|
---
|
|
82
163
|
|
|
83
|
-
### `useAnalytics(apiUrl, apiKey)`
|
|
164
|
+
### `useAnalytics(apiUrl, apiKey, options?)`
|
|
84
165
|
|
|
85
|
-
|
|
166
|
+
| Option | Type | Description |
|
|
167
|
+
| --- | --- | --- |
|
|
168
|
+
| `options.wsUrl` | `string` | Override the default presence WebSocket URL |
|
|
86
169
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
170
|
+
Events are queued internally until the session is ready — safe to call immediately on mount.
|
|
171
|
+
|
|
172
|
+
| Method | Signature | Description |
|
|
173
|
+
| --- | --- | --- |
|
|
174
|
+
| `trackPageview` | `(path: string) => void` | Manual pageview (SPA route changes) |
|
|
175
|
+
| `trackClick` | `(e: MouseEvent, element: HTMLElement) => void` | Click with position + element metadata |
|
|
176
|
+
| `trackScroll` | `(depth: number) => void` | Scroll depth percentage (0–100) |
|
|
177
|
+
| `trackCopy` | `() => void` | Text copy event (captures selected text, up to 200 chars) |
|
|
178
|
+
| `trackError` | `(error: Error) => void` | JS error with message + stack trace |
|
|
179
|
+
| `trackCTA` | `(ctaId: string, ctaVariant?: string) => void` | CTA button interaction |
|
|
180
|
+
| `trackSearch` | `(query: string) => void` | Search query |
|
|
96
181
|
|
|
97
182
|
---
|
|
98
183
|
|
|
99
|
-
###
|
|
184
|
+
### Raw API helpers (server / build safe)
|
|
100
185
|
|
|
101
|
-
The default entry
|
|
186
|
+
The default entry exports the low-level functions with no browser globals — safe to import in server components or Node.js scripts.
|
|
102
187
|
|
|
103
188
|
```ts
|
|
104
189
|
import {
|
|
@@ -113,47 +198,89 @@ import {
|
|
|
113
198
|
} from '@arthurreira/analytics'
|
|
114
199
|
```
|
|
115
200
|
|
|
201
|
+
All helpers take `(apiUrl, apiKey, sessionId, ...)` as their first arguments and call your backend directly via `fetch`.
|
|
202
|
+
|
|
116
203
|
---
|
|
117
204
|
|
|
118
|
-
|
|
205
|
+
### Presence (real-time WebSocket)
|
|
206
|
+
|
|
207
|
+
Optional live-visitor presence tracking over WebSocket.
|
|
119
208
|
|
|
120
|
-
|
|
209
|
+
```ts
|
|
210
|
+
import { connectPresence, DEFAULT_WS_URL } from '@arthurreira/analytics'
|
|
211
|
+
import type { PresenceConnection } from '@arthurreira/analytics'
|
|
121
212
|
|
|
122
|
-
|
|
213
|
+
const conn: PresenceConnection = connectPresence(apiKey, sessionId, DEFAULT_WS_URL)
|
|
214
|
+
|
|
215
|
+
// later
|
|
216
|
+
conn.disconnect()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The connection retries up to 3 times with exponential back-off on unexpected close. When used via the `<Analytics />` component or `useAnalytics`, presence connects automatically and disconnects on session end.
|
|
123
220
|
|
|
124
221
|
---
|
|
125
222
|
|
|
126
|
-
##
|
|
223
|
+
## Package exports
|
|
127
224
|
|
|
128
|
-
|
|
225
|
+
| Import path | Format | Contents | Use in |
|
|
226
|
+
| --- | --- | --- | --- |
|
|
227
|
+
| `@arthurreira/analytics` | ESM | Raw API helpers + presence (no browser globals) | Server / Node |
|
|
228
|
+
| `@arthurreira/analytics/client` | ESM | `Analytics` component + `useAnalytics` hook | React client |
|
|
229
|
+
| `dist/af-analytics.umd.js` | IIFE | Self-contained auto-tracking script | Any HTML page |
|
|
129
230
|
|
|
130
|
-
|
|
131
|
-
- Screen / viewport dimensions
|
|
132
|
-
- Referrer URL, landing page
|
|
133
|
-
- UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`)
|
|
231
|
+
---
|
|
134
232
|
|
|
135
|
-
|
|
233
|
+
## Session management
|
|
136
234
|
|
|
137
|
-
-
|
|
138
|
-
-
|
|
235
|
+
- A session is created on first load and stored in `localStorage` (`af_session_id`).
|
|
236
|
+
- Sessions expire after **30 minutes of inactivity**.
|
|
237
|
+
- On tab hide (`visibilitychange → hidden`) or page unload (`beforeunload`) the SDK calls `navigator.sendBeacon` to close the session gracefully — no events are dropped.
|
|
238
|
+
- Visitor identity persists across sessions via `af_analytics_visitor_id` (a random UUID in `localStorage`).
|
|
139
239
|
|
|
140
240
|
---
|
|
141
241
|
|
|
142
|
-
##
|
|
242
|
+
## Data collected
|
|
143
243
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
|
147
|
-
|
|
|
244
|
+
### On session start
|
|
245
|
+
|
|
246
|
+
| Field | Description |
|
|
247
|
+
| --- | --- |
|
|
248
|
+
| `visitor_id` | Persistent anonymous UUID |
|
|
249
|
+
| `language` | `navigator.language` |
|
|
250
|
+
| `screen_width / height` | Physical screen resolution |
|
|
251
|
+
| `viewport_width / height` | Current browser viewport |
|
|
252
|
+
| `referrer` | `document.referrer` |
|
|
253
|
+
| `landing_page` | `window.location.pathname` |
|
|
254
|
+
| `timezone` | IANA timezone string |
|
|
255
|
+
| `cpu_threads` | `navigator.hardwareConcurrency` |
|
|
256
|
+
| `memory_gb` | `navigator.deviceMemory` (Chrome only) |
|
|
257
|
+
| `gpu` | WebGL unmasked renderer string |
|
|
258
|
+
| `network_type` | `effectiveType` from Network Information API |
|
|
259
|
+
| `connection_speed_mbps` | `downlink` from Network Information API |
|
|
260
|
+
| `utm_source/medium/campaign/term/content` | UTM parameters from the landing URL |
|
|
261
|
+
|
|
262
|
+
### On each event
|
|
263
|
+
|
|
264
|
+
`session_id`, `event_type`, `path`, `page_url`, plus event-specific fields:
|
|
265
|
+
|
|
266
|
+
| Event | Extra fields |
|
|
267
|
+
| --- | --- |
|
|
268
|
+
| `pageview` | `page_title`, `referrer`, `scroll_depth: 0` |
|
|
269
|
+
| `click` | `x_position`, `y_position`, `element_id`, `element_class`, `element_text` |
|
|
270
|
+
| `scroll` | `scroll_depth` (25 / 50 / 75 / 100) |
|
|
271
|
+
| `copy` | `copied_text` (up to 200 chars) |
|
|
272
|
+
| `error` | `error_message`, `stack_trace` |
|
|
273
|
+
| `cta_click` | `cta_id`, `cta_variant` |
|
|
274
|
+
| `search` | `search_query` |
|
|
148
275
|
|
|
149
276
|
---
|
|
150
277
|
|
|
151
|
-
##
|
|
278
|
+
## Environment variables (Next.js)
|
|
152
279
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
280
|
+
| Variable | Description |
|
|
281
|
+
| --- | --- |
|
|
282
|
+
| `NEXT_PUBLIC_ANALYTICS_URL` | Backend base URL, e.g. `https://analytics.example.com` |
|
|
283
|
+
| `NEXT_PUBLIC_ANALYTICS_KEY` | Project API key |
|
|
157
284
|
|
|
158
285
|
---
|
|
159
286
|
|
|
@@ -164,9 +291,17 @@ pnpm install
|
|
|
164
291
|
pnpm build
|
|
165
292
|
```
|
|
166
293
|
|
|
294
|
+
Outputs:
|
|
295
|
+
|
|
296
|
+
| File | Format | Entry |
|
|
297
|
+
| --- | --- | --- |
|
|
298
|
+
| `dist/index.js` + `dist/index.d.ts` | ESM | `src/index.ts` |
|
|
299
|
+
| `dist/client.js` + `dist/client.d.ts` | ESM | `src/client.ts` |
|
|
300
|
+
| `dist/af-analytics.umd.js` | IIFE/UMD | `src/vanilla.ts` |
|
|
301
|
+
|
|
167
302
|
## Publishing
|
|
168
303
|
|
|
169
|
-
Tag a release
|
|
304
|
+
Tag a release after adding an `NPM_TOKEN` secret to the repository to trigger the CI publish workflow.
|
|
170
305
|
|
|
171
306
|
---
|
|
172
307
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var AfAnalytics = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defProps = Object.defineProperties;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
11
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
12
|
+
var __spreadValues = (a, b) => {
|
|
13
|
+
for (var prop in b || (b = {}))
|
|
14
|
+
if (__hasOwnProp.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
if (__getOwnPropSymbols)
|
|
17
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
18
|
+
if (__propIsEnum.call(b, prop))
|
|
19
|
+
__defNormalProp(a, prop, b[prop]);
|
|
20
|
+
}
|
|
21
|
+
return a;
|
|
22
|
+
};
|
|
23
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
24
|
+
var __export = (target, all) => {
|
|
25
|
+
for (var name in all)
|
|
26
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
27
|
+
};
|
|
28
|
+
var __copyProps = (to, from, except, desc) => {
|
|
29
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
30
|
+
for (let key of __getOwnPropNames(from))
|
|
31
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
32
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
33
|
+
}
|
|
34
|
+
return to;
|
|
35
|
+
};
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
|
|
38
|
+
// src/vanilla.ts
|
|
39
|
+
var vanilla_exports = {};
|
|
40
|
+
__export(vanilla_exports, {
|
|
41
|
+
init: () => init
|
|
42
|
+
});
|
|
43
|
+
var _currentScript = document.currentScript;
|
|
44
|
+
var SESSION_EXPIRY_MS = 30 * 60 * 1e3;
|
|
45
|
+
function readConfig() {
|
|
46
|
+
var _a, _b, _c, _d;
|
|
47
|
+
const script = _currentScript != null ? _currentScript : document.querySelector("script[data-api-key]");
|
|
48
|
+
const apiKey = (_b = (_a = script == null ? void 0 : script.dataset.apiKey) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
49
|
+
const apiUrl = ((_d = (_c = script == null ? void 0 : script.dataset.url) == null ? void 0 : _c.trim()) != null ? _d : "").replace(/\/$/, "");
|
|
50
|
+
if (!apiKey || !apiUrl) return null;
|
|
51
|
+
return { apiKey, apiUrl };
|
|
52
|
+
}
|
|
53
|
+
function generateId() {
|
|
54
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
55
|
+
return crypto.randomUUID();
|
|
56
|
+
}
|
|
57
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
58
|
+
const r = Math.random() * 16 | 0;
|
|
59
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
60
|
+
return v.toString(16);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function getVisitorId() {
|
|
64
|
+
const stored = localStorage.getItem("af_analytics_visitor_id");
|
|
65
|
+
if (stored) return stored;
|
|
66
|
+
const id = generateId();
|
|
67
|
+
localStorage.setItem("af_analytics_visitor_id", id);
|
|
68
|
+
return id;
|
|
69
|
+
}
|
|
70
|
+
function getStoredSession() {
|
|
71
|
+
const id = localStorage.getItem("af_session_id");
|
|
72
|
+
const lastActivity = localStorage.getItem("af_session_last_activity");
|
|
73
|
+
if (!id || !lastActivity) return null;
|
|
74
|
+
if (Date.now() - parseInt(lastActivity, 10) < SESSION_EXPIRY_MS) return id;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function touchSession() {
|
|
78
|
+
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
79
|
+
}
|
|
80
|
+
function clearSession() {
|
|
81
|
+
localStorage.removeItem("af_session_id");
|
|
82
|
+
localStorage.removeItem("af_session_last_activity");
|
|
83
|
+
}
|
|
84
|
+
function getGpu() {
|
|
85
|
+
try {
|
|
86
|
+
const canvas = document.createElement("canvas");
|
|
87
|
+
const gl = canvas.getContext("webgl");
|
|
88
|
+
if (!gl) return null;
|
|
89
|
+
const ext = gl.getExtension("WEBGL_debug_renderer_info");
|
|
90
|
+
if (!ext) return null;
|
|
91
|
+
return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function send(apiUrl, apiKey, payload) {
|
|
97
|
+
fetch(`${apiUrl}/events`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify(payload),
|
|
101
|
+
keepalive: true
|
|
102
|
+
}).catch(() => {
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function baseFields(sessionId, eventType) {
|
|
106
|
+
return {
|
|
107
|
+
session_id: sessionId,
|
|
108
|
+
event_type: eventType,
|
|
109
|
+
path: window.location.pathname,
|
|
110
|
+
page_url: window.location.href
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function createRemoteSession(apiUrl, apiKey, visitorId) {
|
|
114
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
115
|
+
const nav = navigator;
|
|
116
|
+
const conn = (_c = (_b = (_a = nav["connection"]) != null ? _a : nav["mozConnection"]) != null ? _b : nav["webkitConnection"]) != null ? _c : null;
|
|
117
|
+
const params = new URLSearchParams(window.location.search);
|
|
118
|
+
const body = {
|
|
119
|
+
visitor_id: visitorId,
|
|
120
|
+
language: (_d = navigator.language) != null ? _d : null,
|
|
121
|
+
screen_width: (_e = screen.width) != null ? _e : null,
|
|
122
|
+
screen_height: (_f = screen.height) != null ? _f : null,
|
|
123
|
+
viewport_width: (_g = window.innerWidth) != null ? _g : null,
|
|
124
|
+
viewport_height: (_h = window.innerHeight) != null ? _h : null,
|
|
125
|
+
referrer: document.referrer || null,
|
|
126
|
+
landing_page: window.location.pathname,
|
|
127
|
+
timezone: (_i = Intl.DateTimeFormat().resolvedOptions().timeZone) != null ? _i : null,
|
|
128
|
+
cpu_threads: (_j = navigator.hardwareConcurrency) != null ? _j : null,
|
|
129
|
+
memory_gb: (_k = nav["deviceMemory"]) != null ? _k : null,
|
|
130
|
+
gpu: getGpu(),
|
|
131
|
+
network_type: (_l = conn == null ? void 0 : conn["effectiveType"]) != null ? _l : null,
|
|
132
|
+
connection_speed_mbps: (_m = conn == null ? void 0 : conn["downlink"]) != null ? _m : null,
|
|
133
|
+
utm_source: params.get("utm_source"),
|
|
134
|
+
utm_medium: params.get("utm_medium"),
|
|
135
|
+
utm_campaign: params.get("utm_campaign"),
|
|
136
|
+
utm_term: params.get("utm_term"),
|
|
137
|
+
utm_content: params.get("utm_content")
|
|
138
|
+
};
|
|
139
|
+
const res = await fetch(`${apiUrl}/sessions`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify(body)
|
|
143
|
+
});
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
return data.id;
|
|
146
|
+
}
|
|
147
|
+
async function getOrCreateSession(apiUrl, apiKey) {
|
|
148
|
+
const stored = getStoredSession();
|
|
149
|
+
if (stored) {
|
|
150
|
+
touchSession();
|
|
151
|
+
return stored;
|
|
152
|
+
}
|
|
153
|
+
const id = await createRemoteSession(apiUrl, apiKey, getVisitorId());
|
|
154
|
+
localStorage.setItem("af_session_id", id);
|
|
155
|
+
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
156
|
+
return id;
|
|
157
|
+
}
|
|
158
|
+
function trackPageview(apiUrl, apiKey, sessionId) {
|
|
159
|
+
send(apiUrl, apiKey, __spreadProps(__spreadValues({}, baseFields(sessionId, "pageview")), {
|
|
160
|
+
page_title: document.title || null,
|
|
161
|
+
referrer: document.referrer || null,
|
|
162
|
+
scroll_depth: 0
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
function trackClick(apiUrl, apiKey, sessionId, e) {
|
|
166
|
+
var _a;
|
|
167
|
+
const el = e.target;
|
|
168
|
+
send(apiUrl, apiKey, __spreadProps(__spreadValues({}, baseFields(sessionId, "click")), {
|
|
169
|
+
x_position: e.clientX,
|
|
170
|
+
y_position: e.clientY,
|
|
171
|
+
element_id: el.id || null,
|
|
172
|
+
element_class: el.className || null,
|
|
173
|
+
element_text: ((_a = el.innerText) == null ? void 0 : _a.slice(0, 100)) || null
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
function trackScroll(apiUrl, apiKey, sessionId, depth) {
|
|
177
|
+
send(apiUrl, apiKey, __spreadProps(__spreadValues({}, baseFields(sessionId, "scroll")), { scroll_depth: depth }));
|
|
178
|
+
}
|
|
179
|
+
function trackError(apiUrl, apiKey, sessionId, error) {
|
|
180
|
+
send(apiUrl, apiKey, __spreadProps(__spreadValues({}, baseFields(sessionId, "error")), {
|
|
181
|
+
error_message: error.message,
|
|
182
|
+
stack_trace: error.stack || null
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
function init() {
|
|
186
|
+
const config = readConfig();
|
|
187
|
+
if (!config) return;
|
|
188
|
+
const { apiKey, apiUrl } = config;
|
|
189
|
+
let sessionId = null;
|
|
190
|
+
const pending = [];
|
|
191
|
+
let sessionEnded = false;
|
|
192
|
+
let lastScrollDepth = 0;
|
|
193
|
+
function enqueue(fn) {
|
|
194
|
+
if (sessionId) {
|
|
195
|
+
touchSession();
|
|
196
|
+
fn(sessionId);
|
|
197
|
+
} else {
|
|
198
|
+
pending.push(fn);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
getOrCreateSession(apiUrl, apiKey).then((id) => {
|
|
202
|
+
sessionId = id;
|
|
203
|
+
pending.splice(0).forEach((fn) => fn(id));
|
|
204
|
+
}).catch(() => {
|
|
205
|
+
});
|
|
206
|
+
enqueue((id) => trackPageview(apiUrl, apiKey, id));
|
|
207
|
+
window.addEventListener("click", (e) => {
|
|
208
|
+
enqueue((id) => trackClick(apiUrl, apiKey, id, e));
|
|
209
|
+
});
|
|
210
|
+
window.addEventListener(
|
|
211
|
+
"scroll",
|
|
212
|
+
() => {
|
|
213
|
+
const total = document.documentElement.scrollHeight - window.innerHeight;
|
|
214
|
+
if (total <= 0) return;
|
|
215
|
+
const depth = Math.round(window.scrollY / total * 100);
|
|
216
|
+
for (const milestone of [25, 50, 75, 100]) {
|
|
217
|
+
if (depth >= milestone && lastScrollDepth < milestone) {
|
|
218
|
+
lastScrollDepth = milestone;
|
|
219
|
+
enqueue((id) => trackScroll(apiUrl, apiKey, id, milestone));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{ passive: true }
|
|
224
|
+
);
|
|
225
|
+
window.addEventListener("error", (e) => {
|
|
226
|
+
enqueue((id) => trackError(apiUrl, apiKey, id, new Error(e.message)));
|
|
227
|
+
});
|
|
228
|
+
const endSession = () => {
|
|
229
|
+
if (!sessionId || sessionEnded) return;
|
|
230
|
+
sessionEnded = true;
|
|
231
|
+
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end`);
|
|
232
|
+
clearSession();
|
|
233
|
+
};
|
|
234
|
+
document.addEventListener("visibilitychange", () => {
|
|
235
|
+
if (document.visibilityState === "hidden") endSession();
|
|
236
|
+
});
|
|
237
|
+
window.addEventListener("beforeunload", endSession);
|
|
238
|
+
}
|
|
239
|
+
if (document.readyState === "loading") {
|
|
240
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
241
|
+
} else {
|
|
242
|
+
init();
|
|
243
|
+
}
|
|
244
|
+
return __toCommonJS(vanilla_exports);
|
|
245
|
+
})();
|
package/package.json
CHANGED