@arthurreira/analytics 0.4.0 → 0.6.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 +160 -13
- package/dist/client.js +56 -1
- package/dist/index.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,28 +1,175 @@
|
|
|
1
|
-
|
|
1
|
+
# @arthurreira/analytics
|
|
2
2
|
|
|
3
|
-
Lightweight analytics SDK
|
|
3
|
+
Lightweight analytics SDK for Next.js / React apps. Drop-in component + hook for tracking pageviews, clicks, scrolls, copies, errors, CTAs, and searches — with automatic session management.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Install from npm:
|
|
5
|
+
## Installation
|
|
8
6
|
|
|
7
|
+
```bash
|
|
9
8
|
pnpm add @arthurreira/analytics
|
|
9
|
+
# or
|
|
10
|
+
npm install @arthurreira/analytics
|
|
11
|
+
# or
|
|
12
|
+
yarn add @arthurreira/analytics
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Peer dependencies:** `react ^18 || ^19`, `next ^14 || ^15 || ^16`
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
### Option A — Drop-in `<Analytics />` component (recommended)
|
|
22
|
+
|
|
23
|
+
Add it once in your root layout. It automatically tracks pageviews, clicks, scroll depth milestones (25 / 50 / 75 / 100 %), text copies, and uncaught JS errors with zero extra code.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// app/layout.tsx (Next.js App Router)
|
|
27
|
+
import { Analytics } from '@arthurreira/analytics/client'
|
|
28
|
+
|
|
29
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
30
|
+
return (
|
|
31
|
+
<html>
|
|
32
|
+
<body>
|
|
33
|
+
{children}
|
|
34
|
+
<Analytics
|
|
35
|
+
apiUrl={process.env.NEXT_PUBLIC_ANALYTICS_URL!}
|
|
36
|
+
apiKey={process.env.NEXT_PUBLIC_ANALYTICS_KEY!}
|
|
37
|
+
/>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Option B — `useAnalytics` hook (manual control)
|
|
45
|
+
|
|
46
|
+
Use the hook directly when you need to fire custom events from your own components.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
'use client'
|
|
50
|
+
import { useAnalytics } from '@arthurreira/analytics/client'
|
|
51
|
+
|
|
52
|
+
export function SearchBar() {
|
|
53
|
+
const { trackSearch, trackCTA } = useAnalytics(
|
|
54
|
+
process.env.NEXT_PUBLIC_ANALYTICS_URL!,
|
|
55
|
+
process.env.NEXT_PUBLIC_ANALYTICS_KEY!,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<input
|
|
60
|
+
onKeyDown={(e) => {
|
|
61
|
+
if (e.key === 'Enter') trackSearch(e.currentTarget.value)
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## API reference
|
|
71
|
+
|
|
72
|
+
### `<Analytics apiUrl apiKey />`
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Description |
|
|
75
|
+
| -------- | -------- | --------------------------------------------------------- |
|
|
76
|
+
| `apiUrl` | `string` | Base URL of your af-analytics backend (no trailing slash) |
|
|
77
|
+
| `apiKey` | `string` | Bearer token for your analytics project |
|
|
78
|
+
|
|
79
|
+
Automatically tracks: **pageview**, **click**, **scroll** (depth milestones), **copy**, **error**.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### `useAnalytics(apiUrl, apiKey)`
|
|
84
|
+
|
|
85
|
+
Returns an object with the following tracking functions. Events are queued internally until the session is ready, so it is safe to call them immediately on mount.
|
|
10
86
|
|
|
11
|
-
|
|
87
|
+
| Method | Signature | Description |
|
|
88
|
+
| --------------- | ----------------------------------------------- | -------------------------------------------------------- |
|
|
89
|
+
| `trackPageview` | `(path: string) => void` | Manual pageview (e.g. SPA route changes) |
|
|
90
|
+
| `trackClick` | `(e: MouseEvent, element: HTMLElement) => void` | Click with position + element metadata |
|
|
91
|
+
| `trackScroll` | `(depth: number) => void` | Scroll depth percentage (0–100) |
|
|
92
|
+
| `trackCopy` | `() => void` | Text copy event (captures selected text up to 200 chars) |
|
|
93
|
+
| `trackError` | `(error: Error) => void` | JS error with message + stack trace |
|
|
94
|
+
| `trackCTA` | `(ctaId: string, ctaVariant?: string) => void` | CTA button interaction |
|
|
95
|
+
| `trackSearch` | `(query: string) => void` | Search query |
|
|
12
96
|
|
|
13
|
-
|
|
97
|
+
---
|
|
14
98
|
|
|
15
|
-
Server / build entry
|
|
99
|
+
### Server / build entry
|
|
16
100
|
|
|
17
|
-
|
|
101
|
+
The default entry (`@arthurreira/analytics`) exports the raw API helpers — safe to import in server components or build scripts because it contains no browser globals.
|
|
18
102
|
|
|
19
|
-
|
|
103
|
+
```ts
|
|
104
|
+
import {
|
|
105
|
+
createSession,
|
|
106
|
+
trackPageview,
|
|
107
|
+
trackClick,
|
|
108
|
+
trackScroll,
|
|
109
|
+
trackCopy,
|
|
110
|
+
trackError,
|
|
111
|
+
trackCTA,
|
|
112
|
+
trackSearch,
|
|
113
|
+
} from '@arthurreira/analytics'
|
|
114
|
+
```
|
|
20
115
|
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Session management
|
|
119
|
+
|
|
120
|
+
Sessions are created automatically on first load and stored in `localStorage` (`af_session_id`). A session expires after **30 minutes of inactivity**. When the user leaves or hides the tab the SDK fires a `sendBeacon` to close the session gracefully.
|
|
121
|
+
|
|
122
|
+
Visitor identity is persisted across sessions via `af_analytics_visitor_id` (a random UUID stored in `localStorage`).
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Data collected
|
|
127
|
+
|
|
128
|
+
On session start the SDK sends:
|
|
129
|
+
|
|
130
|
+
- Visitor ID, language, timezone
|
|
131
|
+
- Screen / viewport dimensions
|
|
132
|
+
- Referrer URL, landing page
|
|
133
|
+
- UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`)
|
|
134
|
+
|
|
135
|
+
On each event:
|
|
136
|
+
|
|
137
|
+
- `session_id`, `event_type`, `path`, `page_url`
|
|
138
|
+
- Event-specific fields (position, element, scroll depth, error stack, etc.)
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Environment variables
|
|
143
|
+
|
|
144
|
+
| Variable | Description |
|
|
145
|
+
| --------------------------- | ------------------------------------------------------- |
|
|
146
|
+
| `NEXT_PUBLIC_ANALYTICS_URL` | Backend base URL, e.g. `https://analytics.example.com/api` |
|
|
147
|
+
| `NEXT_PUBLIC_ANALYTICS_KEY` | Project API key (Bearer token) |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Package exports
|
|
152
|
+
|
|
153
|
+
| Import path | Contents | Use in |
|
|
154
|
+
| -------------------------------- | ------------------------------------------- | ----------------- |
|
|
155
|
+
| `@arthurreira/analytics` | Raw API helpers (no browser globals) | Server / build |
|
|
156
|
+
| `@arthurreira/analytics/client` | `Analytics` component + `useAnalytics` hook | Client components |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Building locally
|
|
161
|
+
|
|
162
|
+
```bash
|
|
21
163
|
pnpm install
|
|
22
164
|
pnpm build
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Publishing
|
|
168
|
+
|
|
169
|
+
Tag a release (e.g. `v0.5.0`) after adding an `NPM_TOKEN` secret to the repository to trigger the CI publish workflow.
|
|
23
170
|
|
|
24
|
-
|
|
171
|
+
---
|
|
25
172
|
|
|
26
|
-
|
|
173
|
+
## License
|
|
27
174
|
|
|
28
|
-
|
|
175
|
+
MIT
|
package/dist/client.js
CHANGED
|
@@ -14,6 +14,18 @@ var BASE_FIELDS = (sessionId, eventType, path) => ({
|
|
|
14
14
|
path,
|
|
15
15
|
page_url: typeof window !== "undefined" ? window.location.href : ""
|
|
16
16
|
});
|
|
17
|
+
function getGpu() {
|
|
18
|
+
try {
|
|
19
|
+
const canvas = document.createElement("canvas");
|
|
20
|
+
const gl = canvas.getContext("webgl");
|
|
21
|
+
if (!gl) return null;
|
|
22
|
+
const ext = gl.getExtension("WEBGL_debug_renderer_info");
|
|
23
|
+
if (!ext) return null;
|
|
24
|
+
return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
17
29
|
async function createSession(apiUrl, apiKey, visitorId) {
|
|
18
30
|
const sessionData = { visitor_id: visitorId };
|
|
19
31
|
if (typeof window !== "undefined") {
|
|
@@ -26,6 +38,12 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
26
38
|
sessionData.referrer = document.referrer || null;
|
|
27
39
|
sessionData.landing_page = window.location.pathname || null;
|
|
28
40
|
sessionData.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
41
|
+
sessionData.cpu_threads = navigator.hardwareConcurrency ?? null;
|
|
42
|
+
sessionData.memory_gb = nav.deviceMemory ?? null;
|
|
43
|
+
sessionData.gpu = getGpu();
|
|
44
|
+
const conn = nav.connection ?? nav.mozConnection ?? nav.webkitConnection ?? null;
|
|
45
|
+
sessionData.network_type = conn?.effectiveType ?? null;
|
|
46
|
+
sessionData.connection_speed_mbps = conn?.downlink ?? null;
|
|
29
47
|
const params = new URLSearchParams(window.location.search);
|
|
30
48
|
sessionData.utm_source = params.get("utm_source");
|
|
31
49
|
sessionData.utm_medium = params.get("utm_medium");
|
|
@@ -106,6 +124,23 @@ async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
|
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
// src/hooks/useAnalytics.ts
|
|
127
|
+
var SESSION_EXPIRY_MINUTES = 30;
|
|
128
|
+
async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
129
|
+
const storedSessionId = localStorage.getItem("af_session_id");
|
|
130
|
+
const lastActivity = localStorage.getItem("af_session_last_activity");
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
if (storedSessionId && lastActivity) {
|
|
133
|
+
const minutesSinceActivity = (now - parseInt(lastActivity)) / 1e3 / 60;
|
|
134
|
+
if (minutesSinceActivity < SESSION_EXPIRY_MINUTES) {
|
|
135
|
+
localStorage.setItem("af_session_last_activity", String(now));
|
|
136
|
+
return storedSessionId;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const newSessionId = await createSession(apiUrl, apiKey, visitorId);
|
|
140
|
+
localStorage.setItem("af_session_id", newSessionId);
|
|
141
|
+
localStorage.setItem("af_session_last_activity", String(now));
|
|
142
|
+
return newSessionId;
|
|
143
|
+
}
|
|
109
144
|
function useAnalytics(apiUrl, apiKey) {
|
|
110
145
|
const sessionId = useRef(null);
|
|
111
146
|
const pendingEvents = useRef([]);
|
|
@@ -114,14 +149,34 @@ function useAnalytics(apiUrl, apiKey) {
|
|
|
114
149
|
if (typeof window === "undefined") return;
|
|
115
150
|
const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
|
|
116
151
|
localStorage.setItem("af_analytics_visitor_id", visitor_id);
|
|
117
|
-
|
|
152
|
+
getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
|
|
118
153
|
sessionId.current = id;
|
|
119
154
|
pendingEvents.current.forEach((fn) => fn());
|
|
120
155
|
pendingEvents.current = [];
|
|
121
156
|
});
|
|
122
157
|
}, []);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (typeof window === "undefined") return;
|
|
160
|
+
const sendEnd = () => {
|
|
161
|
+
if (!sessionId.current) return;
|
|
162
|
+
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end`);
|
|
163
|
+
localStorage.removeItem("af_session_id");
|
|
164
|
+
localStorage.removeItem("af_session_last_activity");
|
|
165
|
+
};
|
|
166
|
+
const handleVisibility = () => {
|
|
167
|
+
if (document.visibilityState === "hidden") sendEnd();
|
|
168
|
+
};
|
|
169
|
+
const handleUnload = () => sendEnd();
|
|
170
|
+
document.addEventListener("visibilitychange", handleVisibility);
|
|
171
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
172
|
+
return () => {
|
|
173
|
+
document.removeEventListener("visibilitychange", handleVisibility);
|
|
174
|
+
window.removeEventListener("beforeunload", handleUnload);
|
|
175
|
+
};
|
|
176
|
+
}, [apiUrl]);
|
|
123
177
|
function enqueueOrRun(fn) {
|
|
124
178
|
if (sessionId.current) {
|
|
179
|
+
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
125
180
|
fn();
|
|
126
181
|
} else {
|
|
127
182
|
pendingEvents.current.push(fn);
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,18 @@ var BASE_FIELDS = (sessionId, eventType, path) => ({
|
|
|
5
5
|
path,
|
|
6
6
|
page_url: typeof window !== "undefined" ? window.location.href : ""
|
|
7
7
|
});
|
|
8
|
+
function getGpu() {
|
|
9
|
+
try {
|
|
10
|
+
const canvas = document.createElement("canvas");
|
|
11
|
+
const gl = canvas.getContext("webgl");
|
|
12
|
+
if (!gl) return null;
|
|
13
|
+
const ext = gl.getExtension("WEBGL_debug_renderer_info");
|
|
14
|
+
if (!ext) return null;
|
|
15
|
+
return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
8
20
|
async function createSession(apiUrl, apiKey, visitorId) {
|
|
9
21
|
const sessionData = { visitor_id: visitorId };
|
|
10
22
|
if (typeof window !== "undefined") {
|
|
@@ -17,6 +29,12 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
17
29
|
sessionData.referrer = document.referrer || null;
|
|
18
30
|
sessionData.landing_page = window.location.pathname || null;
|
|
19
31
|
sessionData.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
32
|
+
sessionData.cpu_threads = navigator.hardwareConcurrency ?? null;
|
|
33
|
+
sessionData.memory_gb = nav.deviceMemory ?? null;
|
|
34
|
+
sessionData.gpu = getGpu();
|
|
35
|
+
const conn = nav.connection ?? nav.mozConnection ?? nav.webkitConnection ?? null;
|
|
36
|
+
sessionData.network_type = conn?.effectiveType ?? null;
|
|
37
|
+
sessionData.connection_speed_mbps = conn?.downlink ?? null;
|
|
20
38
|
const params = new URLSearchParams(window.location.search);
|
|
21
39
|
sessionData.utm_source = params.get("utm_source");
|
|
22
40
|
sessionData.utm_medium = params.get("utm_medium");
|
package/package.json
CHANGED