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

webeye-libs

v0.1.2

Published

Lightweight website tracker — active time & click tracking. Vanilla, React, Astro.

Readme

webeye-libs

Lightweight website tracker — active-time and click tracking for Vanilla, React, and Astro.

webeye-libs collects basic visitor device info, identifies the user + tab session, measures active time on the tab, and counts clicks on opted-in ("scoped") elements. It reports to a small edge server that persists to a libSQL/SQLite database.

The tracker is defensive by design: it wraps everything in try/catch, is a no-op during SSR, and never throws into or breaks the host website.


Install

npm install webeye-libs

React and Astro are optional peer dependencies — install them only for the framework you use; the core (Vanilla) entry has no runtime dependencies.

Setting up with Claude Code

The package ships a Claude Code skill that walks through the whole setup — obtaining a key pair, wiring the right framework, and verifying that data actually reaches the database. Copy it into your project once:

mkdir -p .claude/skills
cp -r node_modules/webeye-libs/.claude/skills/webeye-setup .claude/skills/

Then ask Claude to "set up webeye", or invoke /webeye-setup.


Quick start

You need three things from your deployment:

  • endpoint — the base URL of your webeye server, with no trailing slash (e.g. https://webeye.b-cdn.net).
  • website — the site identifier the key pair was issued for. It must match exactly, or every request is rejected.
  • publicKey — the site's pk_… key, generated in the admin panel. It is safe to ship in front-end code; it only ever buys a 15-minute, write-only access token for this one website.

Vanilla

Initialize once, as early as possible, then mark the elements you want to count with a data-webeye-scope attribute.

import { init } from 'webeye-libs'

init({
  endpoint: 'https://webeye.b-cdn.net',
  website: 'my-site',
  publicKey: 'pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
})
<!-- Any click that lands inside a [data-webeye-scope] element is counted under that scope. -->
<button data-webeye-scope="signup">Sign up</button>
<a data-webeye-scope="pricing-cta" href="/pricing">See pricing</a>

A single delegated click listener on document handles all scoped elements — you can add data-webeye-scope to elements rendered at any time. Scope values are automatically slugified (lowercased, dash-separated, alphanumeric).

For programmatic counting, call trackClick:

import { trackClick } from 'webeye-libs'

trackClick('checkout-complete')

Use either data-webeye-scope or a manual trackClick() on a given element — never both, or the click is counted twice.

React

Wrap your app in WebeyeProvider (it calls init once, client-side). Use <Track> for declarative counting and useTrackClick for imperative counting.

import { WebeyeProvider, Track, useTrackClick } from 'webeye-libs/react'

function App() {
  return (
    <WebeyeProvider
      config={{
        endpoint: 'https://webeye.b-cdn.net',
        website: 'my-site',
        publicKey: 'pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
      }}
    >
      <Home />
    </WebeyeProvider>
  )
}

function Home() {
  // Declarative: <Track> renders a wrapper with data-webeye-scope; the core
  // delegated listener does the counting. Defaults to a <span>; override with `as`.
  // Imperative: useTrackClick returns a memoized handler.
  const onCheckout = useTrackClick('checkout-complete')

  return (
    <>
      <Track scope="signup" as="button" onClick={handleSignup}>
        Sign up
      </Track>

      <button onClick={onCheckout}>Complete checkout</button>
    </>
  )
}

<Track> counts via the data-webeye-scope attribute only — do not additionally call trackClick/useTrackClick for the same element.

You can also read the instance with useWebeye() (returns Webeye | null).

Astro

Drop <Webeye> into your layout once (it injects the client init script), and wrap countable elements with <Track>.

---
import Webeye from 'webeye-libs/astro/Webeye.astro'
import Track from 'webeye-libs/astro/Track.astro'
---
<html>
  <head>
    <Webeye
      endpoint="https://webeye.b-cdn.net"
      website="my-site"
      publicKey="pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    />
  </head>
  <body>
    <Track scope="signup" as="button">Sign up</Track>
  </body>
</html>

<Track> renders the given tag (default <span>) with a slugified data-webeye-scope, wrapping its slot. Counting is handled by the delegated listener installed by <Webeye>.


Config options

Passed to init(config) (Vanilla), <WebeyeProvider config={…}> (React), or as props on <Webeye> (Astro).

| Option | Type | Required | Default | Description | |-------------|-----------|----------|---------|-------------| | endpoint | string | yes | — | Server base URL, no trailing slash (e.g. https://webeye.b-cdn.net). | | website | string | yes | — | Site identifier; must match the website the key pair was issued for. | | publicKey | string | yes | — | The site's pk_… public key. Exchanged for a 15-minute access token. | | idleMs | number | no | 60000 | Inactivity threshold in ms. Time only counts while the user has been active within this window. | | flushMs | number | no | 3000 | How often (ms) the active duration is flushed to the server while active. | | autoTrack | boolean | no | true | Install the delegated [data-webeye-scope] click listener. Set false to count only via trackClick. | | debug | boolean | no | false | Log diagnostics to the console. |


Storage keys

webeye-libs persists a small amount of state under namespaced keys so identity and progress survive reloads.

| Key | Storage | Value | |------------------------|------------------|-------| | webeye:visitor_id | localStorage | The visitor id (server-generated). Identifies a distinct visitor across sessions. | | webeye:session_id | sessionStorage | The current tab session id (server-generated). One per tab session. | | webeye:duration | sessionStorage | Accumulated active time in whole seconds (integer). | | webeye:clicks | sessionStorage | Per-scope click counts as JSON: { [scope: string]: number }. | | webeye:token | sessionStorage | The current 15-minute access token. | | webeye:token_exp | sessionStorage | That token's expiry as epoch milliseconds. |

All storage access is wrapped in try/catch, so private-mode restrictions never break the page.


How active time & clicks work

Authentication

  • On the first write, the library exchanges publicKey + website at POST /token for an access token valid for 15 minutes, and caches it in memory + sessionStorage.
  • Every tracking request carries that token as Authorization: Bearer …. The server verifies it by signature alone — no database read — so token exchange is the only DB read the tracker causes, at most once per 15 minutes per tab.
  • The token is refreshed automatically once it is within 60 seconds of expiry, and concurrent sends share a single refresh. A 401 invalidates the cached token so the next send re-exchanges.
  • Unload flushes go through navigator.sendBeacon, which cannot set headers, so the token is placed in the JSON body as access_token instead — the server accepts both.
  • If the public key is wrong, or is used on a website it wasn't issued for, the exchange returns 401 and the library simply stops sending. It never throws into the host page.

Active time

  • Identity first. On init, webeye-libs ensures a visitor_id (from localStorage, else POST /visitors with device info) and then a session_id (from sessionStorage, else POST /sessions). If the key already exists it is reused with no server call.
  • Activity signal. The library listens (passively) for mousemove, pointermove, touchstart, keydown, scroll, and click, recording the timestamp of the last interaction. This broadens the classic "recent mouse move" rule so keyboard and touch users count too.
  • Ticking. Once per second the library checks whether the visitor is active — the tab is visible and the last activity was less than idleMs ago. Each active second increments the accumulated duration (persisted to webeye:duration).
  • Paused when inactive. If the tab is hidden or the user has been idle for idleMs, the counter does not advance and no duration is sent. This is the "paused when inactive" guarantee.
  • Flushing. While active, the absolute active_duration (total seconds) is sent to POST /duration every flushMs. Because the value is absolute, a dropped request is self-correcting — the next flush carries the true total. A final flush is also sent on visibilitychange → hidden and on pagehide/beforeunload using navigator.sendBeacon (with a fetch(..., { keepalive: true }) fallback) so the last value is never lost.

Clicks

  • Only scoped elements are counted — those carrying data-webeye-scope (added directly in Vanilla, or via <Track> in React/Astro), plus any explicit trackClick(scope) calls.
  • Every scope is passed through slugify(), producing a lowercase, dash-separated, alphanumeric slug matching ^[a-z0-9]+(?:-[a-z0-9]+)*$. Empty or invalid scopes are ignored (a warning is logged in debug mode).
  • Counts are cumulative and absolute per scope. On each recorded click the local count is incremented, persisted to webeye:clicks, and sent to POST /clicks as the new total. Sending the absolute count keeps the API idempotent and self-healing: a dropped request is corrected by the next click.
  • If the session isn't ready yet, clicks are still counted locally and flushed once the session id arrives.

Public API

webeye-libs (core / Vanilla)

  • init(config: WebeyeConfig): Webeye — idempotent singleton; starts tracking and returns the instance.
  • getWebeye(): Webeye | null — the current instance, or null before init.
  • trackClick(scope: string): void — proxies to the singleton (no-op if not initialized).
  • slugify(input: string): string, isValidScope(input: string): boolean.
  • Webeye instance methods: trackClick(scope), getVisitorId(): string | null, getSessionId(): string | null, stop(): void.
  • Types: WebeyeConfig, DeviceInfo, DeviceType.

webeye-libs/react

  • WebeyeProvider({ config, children }) — calls init once (client-only, SSR-safe) and provides context.
  • useWebeye(): Webeye | null.
  • useTrackClick(scope: string): () => void — memoized handler for imperative counting.
  • Track{ scope: string; as?: keyof JSX.IntrinsicElements; children; ...rest }; renders <As data-webeye-scope={slug} {...rest}> (default as="span").

webeye-libs/astro/*

  • Webeye.astro — props { endpoint, website, publicKey, idleMs?, flushMs?, autoTrack?, debug? }; injects the client init script.
  • Track.astro — props { scope: string; as?: string } (default as="span"); renders the slugified data-webeye-scope wrapper around its slot.

SSR & safety

  • Importing webeye-libs in Node/SSR is safe: init() is a no-op returning a stub when there is no window/document.
  • All storage and navigator access is guarded, so nothing here can throw into the host page.

License

MIT