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

v1.0.0

Published

> Privacy-first, WebAssembly-powered analytics that runs entirely in the browser — no servers required for tracking logic.

Readme

Unisights

Privacy-first, WebAssembly-powered analytics that runs entirely in the browser — no servers required for tracking logic.

Known Vulnerabilities CodeQL Dependabot npm version license npm downloads

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.

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.


Core Idea

Most analytics tools work like this:

Browser → Third-party SDK → Their servers → Your dashboard

Unisights works like this:

Browser → WASM core (your bundle) → Your endpoint → Your database

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.


How It's Different

Most open-source analytics libraries are simple pixel trackers — they count page views and send raw data to your server. Unisights is the only one with a WASM core and client-side encryption.

| Feature | Unisights | Plausible | Umami | analytics.js | Fathom | | --------------------------- | --------- | --------- | ----- | ------------ | ------ | | WASM core | ✅ | ❌ | ❌ | ❌ | ❌ | | Client-side encryption | ✅ | ❌ | ❌ | ❌ | ❌ | | No secret stored in browser | ✅ | ✅ | ✅ | ✅ | ✅ | | Web Vitals built-in | ✅ | ❌ | ❌ | ❌ | ❌ | | SPA navigation | ✅ | ✅ | ✅ | ✅ | ❌ | | Session tracking | ✅ | ❌ | ❌ | ❌ | ❌ | | Scroll depth | ✅ | ❌ | ❌ | ❌ | ❌ | | Click coordinates | ✅ | ❌ | ❌ | ❌ | ❌ | | Rage click detection | ✅ | ❌ | ❌ | ❌ | ❌ | | Custom events | ✅ | ✅ | ✅ | ✅ | ✅ | | No cookies | ✅ | ✅ | ✅ | ✅ | ✅ | | Self-hostable | ✅ | ✅ | ✅ | ✅ | ✅ | | Bundle size (gzip) | ~86KB | ~1KB | ~8KB | ~3KB | ~2KB |

The tradeoff is honest — Plausible and Umami are far smaller because they do far less in the browser. Unisights trades bundle size for richer data collection and client-side security guarantees.


Installation

CDN (Recommended)

The easiest way to get started — no build tools, no npm, no configuration. Just drop this script tag into your HTML <head>:

<script
  src="https://cdn.jsdelivr.net/npm/@pradeeparul2/unisights/dist/index.global.js"
  data-insights-id="YOUR_SITE_ID"
  data-endpoint="https://your-api.com/collect"
  data-encrypt="true"
  data-track-page-views="true"
  data-track-clicks="true"
  data-track-scroll="true"
  data-track-errors="true"
  data-track-rage-clicks="true"
  data-track-engagement-time="true"
  async
></script>

That's it! The script auto-initializes and starts tracking immediately.

npm / pnpm / yarn (Advanced)

For bundler-based apps (React, Vue, Next.js):

# npm
npm install @pradeeparul2/unisights

# pnpm
pnpm add @pradeeparul2/unisights

# yarn
yarn add @pradeeparul2/unisights

Usage

CDN (Auto-init) - Recommended

The script automatically initializes when it detects data-insights-id and data-endpoint attributes. All configuration is done via data attributes:

Required Attributes

| Attribute | Description | Example | | ------------------ | --------------------------- | -------------------------------- | | data-insights-id | Your unique site identifier | "my-blog" | | data-endpoint | Endpoint to receive events | "https://api.site.com/collect" |

Optional Attributes

| Attribute | Default | Description | | ---------------------------- | ------- | ---------------------------------------- | | data-encrypt | false | Enable client-side encryption | | data-debug | false | Log events to console | | data-track-page-views | true | Track page views and SPA navigation | | data-track-clicks | true | Track click coordinates | | data-track-scroll | true | Track scroll depth | | data-track-errors | true | Track JavaScript errors | | data-track-rage-clicks | true | Track rage click patterns | | data-track-dead-clicks | false | Track clicks on non-interactive elements | | data-track-outbound-links | false | Track external link clicks | | data-track-file-downloads | false | Track file download clicks | | data-track-copy-paste | false | Track copy/paste events | | data-track-engagement-time | true | Track actual time on page | | data-track-tab-focus | false | Track tab focus/blur events | | data-track-network-errors | false | Track failed network requests | | data-track-long-tasks | false | Track JS tasks blocking >50ms | | data-track-resource-timing | false | Track slow resource loads | | data-flush-interval-ms | 15000 | Flush interval in milliseconds |

Complete Example

<script
  src="https://cdn.jsdelivr.net/npm/@pradeeparul2/unisights/dist/index.global.js"
  data-insights-id="my-blog"
  data-endpoint="https://api.mysite.com/collect"
  data-encrypt="true"
  data-debug="false"
  data-track-page-views="true"
  data-track-clicks="true"
  data-track-scroll="true"
  data-track-errors="true"
  data-track-rage-clicks="true"
  data-track-engagement-time="true"
  data-flush-interval-ms="10000"
  async
></script>

Disable Auto-init (Manual Mode)

If you need manual control (e.g., GDPR consent, conditional init):

<script
  src="https://cdn.jsdelivr.net/npm/@pradeeparul2/unisights/dist/index.global.js"
  data-no-auto-init="true"
></script>

<script>
  // Initialize manually after user consent
  if (userGaveConsent) {
    window.unisights.init({
      endpoint: "https://api.mysite.com/collect",
      insightsId: "my-blog",
      encrypt: true,
      trackPageViews: true,
      trackClicks: true,
    });
  }
</script>

npm (Manual Init)

For React, Vue, Next.js, or any bundler-based setup:

import { init } from "@pradeeparul2/unisights";

await init({
  endpoint: "https://your-api.com/events",
  insightsId: "your-site-id",
  debug: true,
  encrypt: true,
  trackPageViews: true,
  trackClicks: true,
  trackScroll: true,
  trackErrors: true,
  trackRageClicks: true,
  trackEngagementTime: true,
});

React

// analytics.ts
import { init } from "@pradeeparul2/unisights";

let initialized = false;

export async function initAnalytics() {
  if (initialized) return;
  initialized = true;
  await init({
    endpoint: process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT!,
    insightsId: "my-app",
    trackPageViews: true,
    trackClicks: true,
    trackRageClicks: true,
    trackErrors: true,
  });
}
// app/layout.tsx or _app.tsx
import { useEffect } from "react";
import { initAnalytics } from "./analytics";

export default function App({ Component, pageProps }) {
  useEffect(() => {
    initAnalytics();
  }, []);

  return <Component {...pageProps} />;
}

Next.js

Unisights handles SPA navigation automatically via pushState/popstate interception — no additional router integration needed.

// app/layout.tsx (App Router)
"use client";

import { useEffect } from "react";
import { init } from "@pradeeparul2/unisights";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  useEffect(() => {
    init({
      endpoint: process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT!,
      insightsId: "my-nextjs-app",
      trackPageViews: true,
      trackClicks: true,
      trackScroll: true,
      trackErrors: true,
      trackRageClicks: true,
      trackEngagementTime: true,
    });
  }, []);

  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

Add WASM support to next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack(config) {
    config.experiments = { ...config.experiments, asyncWebAssembly: true };
    return config;
  },
};

module.exports = nextConfig;

API Reference

window.unisights.init(config)

Manually initialize the tracker. Required only if using data-no-auto-init="true" or npm install.

await window.unisights.init({
  endpoint: "https://your-api.com/events", // required
  insightsId: "your-site-id", // required
  debug: false,
  encrypt: false,
  flushIntervalMs: 15000,
  trackPageViews: true,
  trackClicks: true,
  trackScroll: true,
  trackRageClicks: true,
  trackErrors: true,
  trackEngagementTime: true,
  // ... all other options from data attributes
});

window.unisights.log(name, data)

Log a custom event at any time after initialization.

window.unisights.log("purchase", {
  productId: "prod_123",
  amount: 49.99,
  currency: "USD",
});

window.unisights.log("signup_completed", { plan: "pro" });

window.unisights.flushNow()

Immediately send all buffered events to your endpoint. Useful before critical navigation.

window.unisights.flushNow();

window.unisights.registerEvent(eventType, handler)

Attach a custom DOM event listener and log the result as a custom event.

const logFormSubmit = window.unisights.registerEvent("submit", (e) => e);

// Later, inside your form submit handler:
logFormSubmit("form_submit", { formId: "contact" });

Payload Format

Unisights sends JSON to your endpoint via navigator.sendBeacon. All field names are snake_case.

Unencrypted

{
  "data": {
    "asset_id": "YOUR_INSIGHTS_ID",
    "session_id": "uuid-v4",
    "page_url": "https://yoursite.com/page",
    "entry_page": "https://yoursite.com/landing",
    "exit_page": null,
    "utm_params": { "utm_source": "google", "utm_medium": "cpc" },
    "device_info": {
      "browser": "Chrome",
      "os": "macOS",
      "device_type": "Desktop"
    },
    "scroll_depth": 75.5,
    "time_on_page": 42.0,
    "events": [
      {
        "type": "page_view",
        "data": {
          "location": "https://yoursite.com/page",
          "title": "Page Title",
          "timestamp": 1710000000000
        }
      },
      {
        "type": "click",
        "data": { "x": 340, "y": 210, "timestamp": 1710000005000 }
      },
      {
        "type": "web_vital",
        "data": {
          "name": "LCP",
          "value": 1200,
          "rating": "good",
          "delta": 1200,
          "id": "v1-abc",
          "entries": 1,
          "navigation_type": "navigate",
          "timestamp": 1710000010000
        }
      },
      {
        "type": "custom",
        "data": {
          "name": "purchase",
          "data": "{\"amount\":49.99}",
          "timestamp": 1710000015000
        }
      },
      {
        "type": "error",
        "data": {
          "message": "TypeError: null",
          "source": "app.js",
          "lineno": 42,
          "colno": 7,
          "timestamp": 1710000020000
        }
      }
    ]
  },
  "encrypted": false
}

Encrypted

When data-encrypt="true" or 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.

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

After decryption, your handler receives the standard payload structure with encrypted: true flag preserved.


Encryption

How it works

The encryption 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 30 seconds
client_key = SHA256(site_id || ":" || bucket || ":" || ua_hash)
ciphertext = plaintext XOR keystream(client_key)
tag        = HMAC-SHA256(client_key, ciphertext)

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.

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

server_key = HMAC(SERVER_SECRET, client_key)

Server-side decryption (Python)

import hashlib, hmac as hmac_lib, base64, json

def decrypt_payload(payload: dict) -> dict:
    ciphertext = base64.b64decode(payload["data"])
    tag        = base64.b64decode(payload["tag"])
    bucket     = payload["bucket"]
    site_id    = payload["site_id"]
    ua_hash    = payload["ua_hash"]

    # Reproduce client_key from public inputs
    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, chunk = b"", 0
    while len(keystream) < len(ciphertext):
        keystream += hashlib.sha256(client_key + chunk.to_bytes(4, "big")).digest()
        chunk += 1

    plaintext = bytes(c ^ k for c, k in zip(ciphertext, keystream))
    decrypted_data = json.loads(plaintext)

    # Return with encrypted flag preserved
    return {"data": decrypted_data, "encrypted": True}

Server-side decryption (Rust)

If your backend is Rust, use the core 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)?;
    }
    Err(DecryptError::TagMismatch) => {
        // reject — tampered payload or mismatched inputs
    }
}

Why CDN Script Tag is Recommended

  1. Zero Configuration - Works out of the box, no build tools needed
  2. Non-Technical Friendly - Marketing teams can install without developer help
  3. Fewer Integration Bugs - No manual init code to write
  4. Faster Deployment - Copy-paste solution for 95% of use cases
  5. Industry Standard - Same pattern as Google Analytics, Plausible, Mixpanel

Use manual npm install only for:

  • Complex GDPR consent flows requiring conditional initialization
  • A/B testing different tracking configurations
  • Framework-specific integrations (React hooks, Vue plugins)

License

MIT License

Copyright (c) 2024 Pradeep Arul

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.