npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pradeeparul2/unisights-core

v0.0.1-beta.5

Published

WebAssembly analytics core — event tracking, session management, web vitals, and rolling key encryption

Downloads

457

Readme

Unisights Core

Rust-powered WebAssembly core for the Unisights analytics engine. Provides a high-performance Tracker with session management, event logging, scroll tracking, web vitals, and time-bucketed rolling key encryption — all compiled to WASM via wasm-pack.

Known Vulnerabilities CodeQL Dependabot npm version license npm downloads

Note: This package is the low-level WASM core. Most users should use @pradeeparul2/unisights instead, which wraps this package with a browser-friendly API and handles WASM initialization automatically.


Features

  • High-performance event tracking — written in Rust, compiled to WASM
  • Session management — tracks asset ID, session ID, UTM params, and device info
  • Event types — clicks, page views, scroll depth, web vitals, custom events, JS errors
  • Rolling key encryption — time-bucketed, stateless, server-verifiable with no secrets in the browser
  • Zero JS dependencies — all logic lives in Rust

Installation

# npm
npm install @pradeeparul2/unisights-core

# pnpm
pnpm add @pradeeparul2/unisights-core

# yarn
yarn add @pradeeparul2/unisights-core

Quick Start

The WASM binary must be initialized before using any exports. Always await init() first.

import init, { Tracker } from "@pradeeparul2/unisights-core";

// 1. Initialize the WASM binary
await init();

// 2. Create a tracker instance
const tracker = new Tracker();

// 3. Set session info
tracker.setSessionInfo(
  "your-asset-id",
  "session-uuid",
  window.location.href,
  { utm_source: "google", utm_medium: "cpc" },
  {
    userAgent: navigator.userAgent,
    platform: navigator.platform,
    os: "macOS",
    screenWidth: screen.width,
    screenHeight: screen.height,
    deviceType: "Desktop",
  },
  await sha256(navigator.userAgent), // ua_hash — required for encryption
);

// 4. Log events
tracker.logEntryPage(window.location.href);
tracker.logPageView(window.location.href, document.title);
tracker.logClick(100, 200);
tracker.updateScroll(75.5);

// 5. Export and send the payload
const payload = tracker.exportEncryptedPayload();
await fetch("https://your-backend.com/events", {
  method: "POST",
  body: JSON.stringify(payload),
});

// 6. Clear sent events
tracker.clearEvents();

API Reference

init(wasmUrl?)

Initializes the WASM binary. Must be called before creating any Tracker instances.

// Auto-resolve (works in ESM / bundler environments)
await init();

// Explicit URL (required for CDN / IIFE usage)
await init("https://cdn.example.com/unisights_core_bg.wasm");

Tracker

The main class. Create one instance per page session.

const tracker = new Tracker();

tracker.setSessionInfo(assetId, sessionId, pageUrl, utmParams, deviceInfo, uaHash?)

Attach session metadata. Call once on page load.

tracker.setSessionInfo(
  "asset-123",
  "session-uuid",
  "https://example.com/page",
  { utm_source: "google", utm_medium: "cpc" },
  {
    userAgent: navigator.userAgent,
    platform: navigator.platform,
    os: "macOS",
    screenWidth: 1920,
    screenHeight: 1080,
    deviceType: "Desktop",
  },
  await sha256(navigator.userAgent), // optional, required when encryption is enabled
);

| Param | Type | Description | | ------------ | ---------------- | -------------------------------------------------------------------- | | assetId | string | Your site/asset identifier | | sessionId | string | Unique session UUID | | pageUrl | string | Current page URL | | utmParams | object \| null | UTM parameters | | deviceInfo | object \| null | Device/browser metadata | | uaHash | string \| null | SHA256 of navigator.userAgent — used for encryption key derivation |


tracker.setEncryptionConfig(enable)

Enable or disable rolling key encryption. When enabled, ua_hash must be set via setSessionInfo.

tracker.setEncryptionConfig(true); // enable
tracker.setEncryptionConfig(false); // disable

tracker.logEntryPage(url)

Log the first page a user lands on. Call once per session.

tracker.logEntryPage(window.location.href);

tracker.logPageView(url, title?)

Log a page view. Call on every navigation, including SPA route changes.

tracker.logPageView(window.location.href, document.title);

tracker.logExitPage(url)

Log the page the user exits from. Best called on pagehide.

window.addEventListener("pagehide", () => {
  tracker.logExitPage(window.location.href);
});

tracker.logClick(x, y)

Log a click with viewport coordinates.

window.addEventListener("click", (e) => {
  tracker.logClick(e.clientX, e.clientY);
});

tracker.updateScroll(percent)

Update the current scroll depth as a percentage (0–100). The tracker keeps the maximum value reached.

window.addEventListener("scroll", () => {
  const percent =
    ((window.scrollY + window.innerHeight) / document.body.scrollHeight) * 100;
  tracker.updateScroll(percent);
});

tracker.logWebVital(name, value, id, rating, delta, entriesCount, navigationType)

Log a Core Web Vital metric.

import { onLCP } from "web-vitals";

onLCP((metric) => {
  tracker.logWebVital(
    metric.name, // "LCP"
    metric.value, // ms
    metric.id,
    metric.rating, // "good" | "needs-improvement" | "poor"
    metric.delta,
    metric.entries.length,
    metric.navigationType ?? "navigate",
  );
});

tracker.logCustomEvent(name, data)

Log a custom named event with a JSON-encoded data string.

tracker.logCustomEvent(
  "add_to_cart",
  JSON.stringify({ sku: "abc123", qty: 2 }),
);

tracker.logError(message, source?, lineno?, colno?)

Log a JavaScript error. Wire up to window.onerror or unhandledrejection.

window.addEventListener("error", (e) => {
  tracker.logError(e.message, e.filename, e.lineno, e.colno);
});

tracker.tick(elapsedSeconds)

Advance the tracker's internal time-on-page clock. Call on a regular interval.

let last = performance.now();

setInterval(() => {
  const now = performance.now();
  tracker.tick((now - last) / 1000);
  last = now;
}, 15_000);

tracker.setPageUrl(url)

Update the current page URL without logging a page view event.

tracker.setPageUrl(window.location.href);

tracker.exportEncryptedPayload()

Export all pending events as a payload object, encrypted if enabled. Throws if there are no events or session info is incomplete.

const payload = tracker.exportEncryptedPayload();
navigator.sendBeacon("/collect", JSON.stringify(payload));

tracker.clearEvents()

Clear all events from the internal queue after a successful flush.

const sent = navigator.sendBeacon("/collect", JSON.stringify(payload));
if (sent) tracker.clearEvents();

Getters

tracker.getScrollDepth(); // → number (max scroll %)
tracker.getTimeOnPage(); // → number (seconds)
tracker.getEventCount(); // → number
tracker.isEncrypted(); // → boolean
tracker.getEntryPage(); // → string | undefined
tracker.getExitPage(); // → string | undefined
tracker.getPageUrl(); // → string | undefined

Payload Format

Unencrypted

{
  "data": {
    "asset_id": "asset-123",
    "session_id": "session-abc",
    "page_url": "https://example.com/page",
    "entry_page": "https://example.com/landing",
    "exit_page": null,
    "utm_params": { "utm_source": "google" },
    "device_info": { "browser": "Chrome" },
    "scroll_depth": 75.5,
    "time_on_page": 42.0,
    "events": [
      {
        "type": "click",
        "data": { "x": 120, "y": 340, "timestamp": 1700000010000 }
      },
      {
        "type": "page_view",
        "data": {
          "location": "https://example.com/about",
          "title": "About Us",
          "timestamp": 1700000015000
        }
      },
      {
        "type": "web_vital",
        "data": {
          "name": "LCP",
          "value": 1200,
          "rating": "good",
          "delta": 1200,
          "id": "v1-abc",
          "entries": 1,
          "navigation_type": "navigate",
          "timestamp": 1700000020000
        }
      },
      {
        "type": "custom",
        "data": {
          "name": "add_to_cart",
          "data": "{\"sku\":\"abc123\"}",
          "timestamp": 1700000025000
        }
      },
      {
        "type": "error",
        "data": {
          "message": "TypeError: null",
          "source": "app.js",
          "lineno": 42,
          "colno": 7,
          "timestamp": 1700000030000
        }
      }
    ]
  },
  "encrypted": false
}

Encrypted

When encryption is enabled, the analytics payload body is encrypted. The envelope contains everything the server needs to verify and decrypt — no server state required.

{
  "data": "<base64 ciphertext>",
  "tag": "<base64 HMAC-SHA256 authentication tag>",
  "bucket": 56666667,
  "site_id": "asset-123",
  "ua_hash": "f9a23b...",
  "encrypted": true
}

Encryption

How it works

The key is derived entirely from public, reproducible inputs. No secret is stored in or transmitted from the browser.

bucket     = floor(timestamp_ms / 30_000)       // rotates every 30s
client_key = SHA256(site_id || ":" || bucket || ":" || ua_hash)
ciphertext = plaintext XOR keystream(client_key)
tag        = HMAC-SHA256(client_key, ciphertext)

The server receives site_id, ua_hash, and bucket in the payload envelope and independently reproduces client_key to verify the tag and decrypt. No session state needed server-side.

For an additional security layer the server can wrap the key:

server_key = HMAC(SERVER_SECRET, client_key)

Server-side decryption (Rust)

If your backend is also Rust, you can use this same crate directly:

use unisights_core::encryption::decrypt;

match decrypt(&ciphertext, &tag, bucket, &site_id, &ua_hash) {
    Ok(plaintext) => {
        let payload: serde_json::Value = serde_json::from_slice(&plaintext)?;
        // process payload
    }
    Err(DecryptError::TagMismatch) => {
        // reject — tampered payload or mismatched inputs
    }
}

Server-side decryption (Python)

import hashlib, hmac as hmac_lib

def decrypt(ciphertext: bytes, tag: bytes, bucket: int, site_id: str, ua_hash: str) -> bytes:
    # Reproduce client_key
    h = hashlib.sha256()
    h.update(site_id.encode())
    h.update(b":")
    h.update(bucket.to_bytes(8, "big"))
    h.update(b":")
    h.update(ua_hash.encode())
    client_key = h.digest()

    # Verify tag before decrypting
    expected_tag = hmac_lib.new(client_key, ciphertext, hashlib.sha256).digest()
    if not hmac_lib.compare_digest(expected_tag, tag):
        raise ValueError("tag mismatch — payload rejected")

    # Decrypt via XOR keystream
    keystream = b""
    chunk = 0
    while len(keystream) < len(ciphertext):
        keystream += hashlib.sha256(client_key + chunk.to_bytes(4, "big")).digest()
        chunk += 1

    return bytes(c ^ k for c, k in zip(ciphertext, keystream))

Building from Source

Requires Rust and wasm-pack.

# Install wasm-pack
cargo install wasm-pack

# Build for bundlers (Vite, webpack, Rollup)
wasm-pack build --target bundler

# Build for browsers (script tag / CDN)
wasm-pack build --target web

# Build for Node.js
wasm-pack build --target nodejs

Output is written to pkg/.


Package Contents

pkg/
├── unisights_core.js           # JS bindings
├── unisights_core.d.ts         # TypeScript types
├── unisights_core_bg.wasm      # Compiled WASM binary
├── unisights_core_bg.wasm.d.ts
└── package.json

Testing

Tests are split by module and all run under wasm-pack test.

# Run all tests
wasm-pack test --headless --chrome

# Run a specific module
wasm-pack test --headless --chrome --test encryption_tests
wasm-pack test --headless --chrome --test event_tests
wasm-pack test --headless --chrome --test session_tests
wasm-pack test --headless --chrome --test tracker_tests

| File | Tests | Coverage | | --------------------------- | ------- | --------------------------------------------------------------------- | | tests/encryption_tests.rs | 34 | bucket, key derivation, XOR, HMAC, encrypt, decrypt, tamper rejection | | tests/event_tests.rs | 16 | EventQueue ops, all 5 event variants | | tests/session_tests.rs | 15 | defaults, is_ready guards, ua_hash serialization skip | | tests/tracker_tests.rs | 23 | events, scroll, time, clear, build_payload | | src/lib.rs | 41 | full WASM API roundtrip | | Total | 129 | |

ChromeDriver note: Requires ChromeDriver matching your installed Chrome version. If you see a status code 404 error, either downgrade ChromeDriver to 114 or upgrade wasm-bindgen to 0.2.100+ in Cargo.toml.


Dependencies

[dependencies]
wasm-bindgen       = "0.2"
js-sys             = "0.3"
serde              = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
serde_json         = "1"
base64             = "0.22"
hmac               = "0.12"
sha2               = "0.10"

[dev-dependencies]
wasm-bindgen-test  = "0.3"

Related


License

MIT © Pradeep Arul