@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 +21 -0
- package/README.md +506 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.global.js +2 -0
- package/dist/index.js +858 -0
- package/dist/index.mjs +823 -0
- package/package.json +48 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|