@pradeeparul2/unisights 0.0.1-beta.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,506 @@
1
+ # Unisights
2
+
3
+ > Privacy-first, WebAssembly-powered analytics that runs entirely in the browser — no servers required for tracking logic.
4
+
5
+ ---
6
+
7
+ ## What is Unisights?
8
+
9
+ Unisights is an open-source analytics library built on **Rust + WebAssembly**. All event processing, session management, and optional payload encryption happens inside a WASM binary compiled directly into the bundle — not on a remote server, not in a third-party cloud.
10
+
11
+ You get full analytics coverage (page views, clicks, scroll, web vitals, errors, rage clicks, engagement time, and more) with a single script tag or npm install, and you own every byte of data that leaves the browser.
12
+
13
+ ---
14
+
15
+ ## Core Idea
16
+
17
+ Most analytics tools work like this:
18
+
19
+ ```
20
+ Browser → Third-party SDK → Their servers → Your dashboard
21
+ ```
22
+
23
+ Unisights works like this:
24
+
25
+ ```
26
+ Browser → WASM core (your bundle) → Your endpoint → Your database
27
+ ```
28
+
29
+ The tracking logic — session handling, event buffering, encryption, payload serialization — runs in a Rust-compiled WASM module embedded in the JS bundle. There are no external fetches to analytics infrastructure. Your endpoint receives structured JSON payloads via `navigator.sendBeacon`, and you decide what to store, aggregate, and display.
30
+
31
+ ---
32
+
33
+ ## How It's Different
34
+
35
+ Most open-source analytics libraries are simple pixel trackers —
36
+ they count page views and send raw data to your server.
37
+ Unisights is the only one with a WASM core and client-side encryption.
38
+
39
+ | Feature | Unisights | Plausible | Umami | analytics.js | Fathom |
40
+ | --------------------------- | --------- | --------- | ----- | ------------ | ------ |
41
+ | WASM core | ✅ | ❌ | ❌ | ❌ | ❌ |
42
+ | Client-side encryption | ✅ | ❌ | ❌ | ❌ | ❌ |
43
+ | No secret stored in browser | ✅ | ✅ | ✅ | ✅ | ✅ |
44
+ | Web Vitals built-in | ✅ | ❌ | ❌ | ❌ | ❌ |
45
+ | SPA navigation | ✅ | ✅ | ✅ | ✅ | ❌ |
46
+ | Session tracking | ✅ | ❌ | ❌ | ❌ | ❌ |
47
+ | Scroll depth | ✅ | ❌ | ❌ | ❌ | ❌ |
48
+ | Click coordinates | ✅ | ❌ | ❌ | ❌ | ❌ |
49
+ | Rage click detection | ✅ | ❌ | ❌ | ❌ | ❌ |
50
+ | Custom events | ✅ | ✅ | ✅ | ✅ | ✅ |
51
+ | No cookies | ✅ | ✅ | ✅ | ✅ | ✅ |
52
+ | Self-hostable | ✅ | ✅ | ✅ | ✅ | ✅ |
53
+ | Bundle size (gzip) | ~86KB | ~1KB | ~8KB | ~3KB | ~2KB |
54
+
55
+ **The tradeoff is honest** — Plausible and Umami are far smaller
56
+ because they do far less in the browser. Unisights trades bundle
57
+ size for richer data collection and client-side security guarantees.
58
+
59
+ ---
60
+
61
+ ## Installation
62
+
63
+ ### npm / pnpm / yarn
64
+
65
+ ```bash
66
+ # npm
67
+ npm install @pradeeparul/unisights
68
+
69
+ # pnpm
70
+ pnpm add @pradeeparul/unisights
71
+
72
+ # yarn
73
+ yarn add @pradeeparul/unisights
74
+ ```
75
+
76
+ ### Packages
77
+
78
+ | Package | Description |
79
+ | ----------------------------- | --------------------------------------------- |
80
+ | `@pradeeparul/unisights` | Main analytics library |
81
+ | `@pradeeparul/unisights-core` | Rust/WASM core (auto-installed as dependency) |
82
+
83
+ ---
84
+
85
+ ## Usage
86
+
87
+ ### CDN (Script Tag)
88
+
89
+ The simplest way — no build tools required. Drop this into your HTML `<head>`:
90
+
91
+ ```html
92
+ <script
93
+ src="https://cdn.jsdelivr.net/npm/@pradeeparul/unisights/dist/index.global.js"
94
+ data-insights-id="YOUR_INSIGHTS_ID"
95
+ data-analytics-config='{"trackPageViews": true, "trackClicks": true}'
96
+ async
97
+ ></script>
98
+ ```
99
+
100
+ With encryption enabled:
101
+
102
+ ```html
103
+ <script
104
+ src="https://cdn.jsdelivr.net/npm/@pradeeparul/unisights/dist/index.global.js"
105
+ data-insights-id="YOUR_INSIGHTS_ID"
106
+ data-analytics-config='{"encrypt": true, "endpoint": "https://your-api.com/events"}'
107
+ async
108
+ ></script>
109
+ ```
110
+
111
+ Pre-init queue — safe to call before the script loads:
112
+
113
+ ```html
114
+ <script>
115
+ window.unisightsq = window.unisightsq || [];
116
+ window.unisightsq.push(() => {
117
+ window.unisights.log("app_loaded", { version: "1.0.0" });
118
+ });
119
+ </script>
120
+ ```
121
+
122
+ ---
123
+
124
+ ### npm (ESM)
125
+
126
+ ```ts
127
+ import { init } from "@pradeeparul/unisights";
128
+
129
+ await init({
130
+ endpoint: "https://your-api.com/events",
131
+ debug: true,
132
+ trackPageViews: true,
133
+ trackClicks: true,
134
+ trackScroll: true,
135
+ trackErrors: true,
136
+ });
137
+ ```
138
+
139
+ ---
140
+
141
+ ### React
142
+
143
+ ```tsx
144
+ // analytics.ts
145
+ import { init } from "@pradeeparul/unisights";
146
+
147
+ let initialized = false;
148
+
149
+ export async function initAnalytics() {
150
+ if (initialized) return;
151
+ initialized = true;
152
+ await init({
153
+ endpoint: process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT!,
154
+ trackPageViews: true,
155
+ trackClicks: true,
156
+ trackRageClicks: true,
157
+ trackErrors: true,
158
+ });
159
+ }
160
+ ```
161
+
162
+ ```tsx
163
+ // app/layout.tsx or _app.tsx
164
+ import { useEffect } from "react";
165
+ import { initAnalytics } from "./analytics";
166
+
167
+ export default function App({ Component, pageProps }) {
168
+ useEffect(() => {
169
+ initAnalytics();
170
+ }, []);
171
+
172
+ return <Component {...pageProps} />;
173
+ }
174
+ ```
175
+
176
+ ---
177
+
178
+ ### Next.js
179
+
180
+ Unisights handles SPA navigation automatically via `pushState`/`popstate` interception — no additional router integration needed.
181
+
182
+ ```tsx
183
+ // app/layout.tsx (App Router)
184
+ "use client";
185
+
186
+ import { useEffect } from "react";
187
+ import { init } from "@pradeeparul/unisights";
188
+
189
+ export default function RootLayout({
190
+ children,
191
+ }: {
192
+ children: React.ReactNode;
193
+ }) {
194
+ useEffect(() => {
195
+ init({
196
+ endpoint: process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT!,
197
+ trackPageViews: true,
198
+ trackClicks: true,
199
+ trackScroll: true,
200
+ trackErrors: true,
201
+ trackRageClicks: true,
202
+ trackEngagementTime: true,
203
+ });
204
+ }, []);
205
+
206
+ return (
207
+ <html>
208
+ <body>{children}</body>
209
+ </html>
210
+ );
211
+ }
212
+ ```
213
+
214
+ Add WASM support to `next.config.js`:
215
+
216
+ ```js
217
+ /** @type {import('next').NextConfig} */
218
+ const nextConfig = {
219
+ webpack(config) {
220
+ config.experiments = { ...config.experiments, asyncWebAssembly: true };
221
+ return config;
222
+ },
223
+ };
224
+
225
+ module.exports = nextConfig;
226
+ ```
227
+
228
+ ---
229
+
230
+ ## API Reference
231
+
232
+ ### `init(config?)`
233
+
234
+ Initializes the tracker. Must be called once. Subsequent calls are no-ops.
235
+
236
+ ```ts
237
+ await init({
238
+ endpoint: "https://your-api.com/events", // required — where payloads are sent
239
+ debug: false, // logs events to console
240
+ encrypt: false, // enables rolling key encryption
241
+ flushIntervalMs: 15000, // how often to flush (ms)
242
+
243
+ // Page
244
+ trackPageViews: true, // entry page + page views + SPA navigation
245
+
246
+ // Interactions
247
+ trackClicks: true, // x/y coordinates of every click
248
+ trackScroll: true, // scroll depth percentage
249
+ trackRageClicks: true, // 3+ rapid clicks in same area
250
+ trackDeadClicks: true, // clicks on non-interactive elements
251
+ trackOutboundLinks: true, // clicks on external links
252
+ trackFileDownloads: true, // clicks on pdf/zip/docx/xlsx etc
253
+ trackCopyPaste: false, // copy and paste events
254
+
255
+ // Errors
256
+ trackErrors: true, // window.onerror + unhandledrejection
257
+
258
+ // Engagement
259
+ trackEngagementTime: true, // actual active time on page
260
+ trackTabFocus: true, // tab focus / blur events
261
+
262
+ // Performance (opt-in)
263
+ trackNetworkErrors: false, // failed fetch requests
264
+ trackLongTasks: false, // JS tasks blocking >50ms
265
+ trackResourceTiming: false, // resources taking >1s to load
266
+ });
267
+ ```
268
+
269
+ ---
270
+
271
+ ### `window.unisights.log(name, data)`
272
+
273
+ Log a custom event at any time after `init()`.
274
+
275
+ ```ts
276
+ window.unisights.log("purchase", {
277
+ productId: "prod_123",
278
+ amount: 49.99,
279
+ currency: "USD",
280
+ });
281
+
282
+ window.unisights.log("signup_completed", { plan: "pro" });
283
+ ```
284
+
285
+ ---
286
+
287
+ ### `window.unisights.flushNow()`
288
+
289
+ Immediately send all buffered events to your endpoint. Useful before critical navigation.
290
+
291
+ ```ts
292
+ window.unisights.flushNow();
293
+ ```
294
+
295
+ ---
296
+
297
+ ### `window.unisights.registerEvent(eventType, handler)`
298
+
299
+ Attach a custom DOM event listener and log the result as a custom event.
300
+
301
+ ```ts
302
+ const logFormSubmit = window.unisights.registerEvent("submit", (e) => e);
303
+
304
+ // Later, inside your form submit handler:
305
+ logFormSubmit("form_submit", { formId: "contact" });
306
+ ```
307
+
308
+ ---
309
+
310
+ ### Script Tag Attributes
311
+
312
+ | Attribute | Required | Description |
313
+ | ----------------------- | -------- | --------------------------------------- |
314
+ | `data-insights-id` | ✅ | Your unique project identifier |
315
+ | `data-analytics-config` | ❌ | JSON config object (overrides defaults) |
316
+
317
+ ---
318
+
319
+ ## Payload Format
320
+
321
+ Unisights sends JSON to your endpoint via `navigator.sendBeacon`. All field names are **snake_case**.
322
+
323
+ ### Unencrypted
324
+
325
+ ```json
326
+ {
327
+ "data": {
328
+ "asset_id": "YOUR_INSIGHTS_ID",
329
+ "session_id": "uuid-v4",
330
+ "page_url": "https://yoursite.com/page",
331
+ "entry_page": "https://yoursite.com/landing",
332
+ "exit_page": null,
333
+ "utm_params": { "utm_source": "google", "utm_medium": "cpc" },
334
+ "device_info": {
335
+ "browser": "Chrome",
336
+ "os": "macOS",
337
+ "device_type": "Desktop"
338
+ },
339
+ "scroll_depth": 75.5,
340
+ "time_on_page": 42.0,
341
+ "events": [
342
+ {
343
+ "type": "page_view",
344
+ "data": {
345
+ "location": "https://yoursite.com/page",
346
+ "title": "Page Title",
347
+ "timestamp": 1710000000000
348
+ }
349
+ },
350
+ {
351
+ "type": "click",
352
+ "data": { "x": 340, "y": 210, "timestamp": 1710000005000 }
353
+ },
354
+ {
355
+ "type": "web_vital",
356
+ "data": {
357
+ "name": "LCP",
358
+ "value": 1200,
359
+ "rating": "good",
360
+ "delta": 1200,
361
+ "id": "v1-abc",
362
+ "entries": 1,
363
+ "navigation_type": "navigate",
364
+ "timestamp": 1710000010000
365
+ }
366
+ },
367
+ {
368
+ "type": "custom",
369
+ "data": {
370
+ "name": "purchase",
371
+ "data": "{\"amount\":49.99}",
372
+ "timestamp": 1710000015000
373
+ }
374
+ },
375
+ {
376
+ "type": "error",
377
+ "data": {
378
+ "message": "TypeError: null",
379
+ "source": "app.js",
380
+ "lineno": 42,
381
+ "colno": 7,
382
+ "timestamp": 1710000020000
383
+ }
384
+ }
385
+ ]
386
+ },
387
+ "encrypted": false
388
+ }
389
+ ```
390
+
391
+ ### Encrypted
392
+
393
+ When `encrypt: true` is set, the analytics payload is encrypted using a **stateless rolling key** before sending. The envelope contains everything your server needs to verify and decrypt — no server-side session state required.
394
+
395
+ ```json
396
+ {
397
+ "data": "<base64 ciphertext>",
398
+ "tag": "<base64 HMAC-SHA256 authentication tag>",
399
+ "bucket": 56666667,
400
+ "site_id": "YOUR_INSIGHTS_ID",
401
+ "ua_hash": "f9a23b...",
402
+ "encrypted": true
403
+ }
404
+ ```
405
+
406
+ ---
407
+
408
+ ## Encryption
409
+
410
+ ### How it works
411
+
412
+ The encryption key is derived entirely from public, reproducible inputs. **No secret is stored in or transmitted from the browser.**
413
+
414
+ ```
415
+ bucket = floor(timestamp_ms / 30_000) // rotates every 30 seconds
416
+ client_key = SHA256(site_id || ":" || bucket || ":" || ua_hash)
417
+ ciphertext = plaintext XOR keystream(client_key)
418
+ tag = HMAC-SHA256(client_key, ciphertext)
419
+ ```
420
+
421
+ `ua_hash` is a SHA256 hash of `navigator.userAgent`, computed by the SDK automatically. The server receives `site_id`, `ua_hash`, and `bucket` in the envelope and can independently reproduce `client_key` to verify the tag and decrypt — statelessly, with no stored keys.
422
+
423
+ For an additional security layer, the server can wrap the key:
424
+
425
+ ```
426
+ server_key = HMAC(SERVER_SECRET, client_key)
427
+ ```
428
+
429
+ ### Server-side decryption (Python)
430
+
431
+ ```python
432
+ import hashlib, hmac as hmac_lib, base64
433
+
434
+ def decrypt_payload(payload: dict) -> dict:
435
+ ciphertext = base64.b64decode(payload["data"])
436
+ tag = base64.b64decode(payload["tag"])
437
+ bucket = payload["bucket"]
438
+ site_id = payload["site_id"]
439
+ ua_hash = payload["ua_hash"]
440
+
441
+ # Reproduce client_key from public inputs
442
+ h = hashlib.sha256()
443
+ h.update(site_id.encode())
444
+ h.update(b":")
445
+ h.update(bucket.to_bytes(8, "big"))
446
+ h.update(b":")
447
+ h.update(ua_hash.encode())
448
+ client_key = h.digest()
449
+
450
+ # Verify tag before decrypting
451
+ expected_tag = hmac_lib.new(client_key, ciphertext, hashlib.sha256).digest()
452
+ if not hmac_lib.compare_digest(expected_tag, tag):
453
+ raise ValueError("tag mismatch — payload rejected")
454
+
455
+ # Decrypt via XOR keystream
456
+ keystream, chunk = b"", 0
457
+ while len(keystream) < len(ciphertext):
458
+ keystream += hashlib.sha256(client_key + chunk.to_bytes(4, "big")).digest()
459
+ chunk += 1
460
+
461
+ plaintext = bytes(c ^ k for c, k in zip(ciphertext, keystream))
462
+ return json.loads(plaintext)
463
+ ```
464
+
465
+ ### Server-side decryption (Rust)
466
+
467
+ If your backend is Rust, use the core crate directly:
468
+
469
+ ```rust
470
+ use unisights_core::encryption::decrypt;
471
+
472
+ match decrypt(&ciphertext, &tag, bucket, &site_id, &ua_hash) {
473
+ Ok(plaintext) => {
474
+ let payload: serde_json::Value = serde_json::from_slice(&plaintext)?;
475
+ }
476
+ Err(DecryptError::TagMismatch) => {
477
+ // reject — tampered payload or mismatched inputs
478
+ }
479
+ }
480
+ ```
481
+
482
+ ---
483
+
484
+ ## License
485
+
486
+ MIT License
487
+
488
+ Copyright (c) 2024 Pradeep Arul
489
+
490
+ Permission is hereby granted, free of charge, to any person obtaining a copy
491
+ of this software and associated documentation files (the "Software"), to deal
492
+ in the Software without restriction, including without limitation the rights
493
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
494
+ copies of the Software, and to permit persons to whom the Software is
495
+ furnished to do so, subject to the following conditions:
496
+
497
+ The above copyright notice and this permission notice shall be included in all
498
+ copies or substantial portions of the Software.
499
+
500
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
501
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
502
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
503
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
504
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
505
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
506
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ type EventHandler = ((event: Event) => void) | (() => (event: Event) => void);
2
+ interface Unisights {
3
+ init: (config?: Partial<UnisightsConfig>) => Promise<void>;
4
+ registerEvent: (eventType: string, handler: EventHandler) => (name: string, data: unknown) => void;
5
+ flushNow: () => void;
6
+ log: (name: string, data: unknown) => void;
7
+ }
8
+ interface DeviceData {
9
+ userAgent: string;
10
+ platform: string;
11
+ os: string;
12
+ screenWidth: number;
13
+ screenHeight: number;
14
+ deviceType: string;
15
+ }
16
+ interface UnisightsConfig {
17
+ endpoint: string;
18
+ insightsId?: string;
19
+ debug?: boolean;
20
+ encrypt?: boolean;
21
+ flushIntervalMs?: number;
22
+ trackPageViews?: boolean;
23
+ trackClicks?: boolean;
24
+ trackScroll?: boolean;
25
+ trackRageClicks?: boolean;
26
+ trackDeadClicks?: boolean;
27
+ trackOutboundLinks?: boolean;
28
+ trackFileDownloads?: boolean;
29
+ trackCopyPaste?: boolean;
30
+ trackErrors?: boolean;
31
+ trackEngagementTime?: boolean;
32
+ trackTabFocus?: boolean;
33
+ trackNetworkErrors?: boolean;
34
+ trackLongTasks?: boolean;
35
+ trackResourceTiming?: boolean;
36
+ }
37
+ declare global {
38
+ interface Window {
39
+ unisights?: Unisights;
40
+ unisightsq?: Array<() => void>;
41
+ }
42
+ }
43
+
44
+ declare function init(userConfig?: Partial<UnisightsConfig>): Promise<void>;
45
+
46
+ export { type DeviceData, type EventHandler, type Unisights, type UnisightsConfig, init };