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

@eshal-tracker/sdk

v1.1.0

Published

Track user IP, browser fingerprint, geolocation, and session identity — lightweight, zero-dependency client SDK.

Readme

user-trace

Lightweight browser-side user identity & session tracking SDK.
Captures IP address, geolocation, browser fingerprint, and session/user IDs — zero dependencies.


Features

| Signal | Details | |---|---| | 🆔 User ID | Persistent across sessions (localStorage) | | 🔁 Session ID | Per-tab, resets on close (sessionStorage) | | 🖥️ Browser Fingerprint | Canvas, WebGL, fonts, plugins, screen, audio | | 🌐 Public IP | Detected via cascaded free APIs | | 📍 Geolocation | Country, city, ISP, proxy detection via IP | | 📡 Browser GPS | Optional — precise device location via the native permission prompt, reverse-geocoded and merged into geo | | 🪝 Webhook | POST full profile to your own backend |


Install

npm install user-trace

Quick Start

import UserTrace from "user-trace";

const tracer = new UserTrace({
  onReady: (profile) => {
    console.log("User ID:",     profile.uid);
    console.log("IP:",          profile.ip);
    console.log("City:",        profile.geo?.city);
    console.log("Country:",     profile.geo?.country);
    console.log("Fingerprint:", profile.fingerprintId);
  }
});

await tracer.init();

Geolocation Providers

By default, user-trace uses fully free providers — no account or API key required.
If you want higher limits or better accuracy, you can plug in your own API key and it will be used first with free ones as fallback.

Free providers (zero config, works out of the box)

| Provider | Free Limit | Proxy Detection | |---|---|---| | ip-api.com | 45 req/min | ✅ Yes | | ipwho.is | Unlimited (fair use) | ❌ | | ipapi.co | 1,000 req/day | ❌ | | freeipapi.com | Unlimited (fair use) | ❌ |

Free-with-key providers (plug in your key for higher limits)

| Provider | Free Limit | Signup | |---|---|---| | ipinfo.io | 50,000 req/month | ipinfo.io/signup | | ip2location.io | 30,000 req/month | ip2location.io/sign-up | | abstractapi.com | 20,000 req/month | app.abstractapi.com | | ipgeolocation.io | 1,000 req/day | ipgeolocation.io/signup | | ipstack.com | 100 req/month | ipstack.com/product |

How the cascade works: If you configure an API key, that provider moves to the front of the list. If it fails (rate limit, downtime), the next provider is tried automatically — all the way down to the free ones.

⚠️ IP geolocation is coarse by nature — it resolves the visitor's ISP gateway, not their device, so the city is often wrong (a user in one city resolves to another). For an accurate end-user position, use Browser GPS below.


Precise Location (Browser GPS)

On by default. user-trace captures the browser's GPS fix on init() (behind the browser's native permission prompt — that prompt is the consent), reverse-geocodes it via BigDataCloud (keyless, CORS-enabled), and merges the result into profile.geo — overriding the coarse IP city / region / country and tagging geo.source = "gps". Consumers that already read profile.geo.city get the accurate value with no other changes. Raw coordinates are also exposed on profile.browserGeo ({ latitude, longitude, accuracy }).

const tracer = new UserTrace();   // geoFromBrowser defaults to true
await tracer.init();              // shows the native location prompt on first visit
// tracer.getProfile().geo → { city, region, country, countryCode, latitude, longitude, source: "gps", … }
// Granted → accurate GPS;  denied / dismissed / plain-HTTP → graceful IP fallback.

Consent-aware modes (geoFromBrowser)

| Value | Behavior | |---|---| | true (default) | Requests GPS during init() — shows the native prompt on first visit (silent if already granted, IP fallback if denied). | | false | Opt out — never touches GPS, IP geo only. | | "auto" | Uses GPS only if permission was already granted (silent, never prompts). Your own UI drives first-time consent, then calls captureBrowserLocation(). |

The native browser prompt always appears the first time an origin requests location — there is no silent/bypass path. Requires a secure context (HTTPS or localhost); on plain HTTP it fails gracefully back to IP geo.

Alternative: polite pre-ask (opt out of prompt-on-load)

The default prompts on the first init(). If you'd rather not prompt on load (better grant rates, no permanent-"Block" risk), set geoFromBrowser: "auto" so the SDK never prompts on its own, show your own lightweight pre-ask, and fire the native prompt only on an explicit Allow:

import UserTrace, { getGeolocationPermission } from "@eshal-tracker/sdk";

const tracer = new UserTrace({ geoFromBrowser: "auto" }); // silently uses GPS only if already granted
await tracer.init();

if ((await getGeolocationPermission()) === "prompt") {
  // Render your own pre-ask: "Share your location for accurate local answers?"
  // Only when the user clicks **Allow**:
  await tracer.captureBrowserLocation(); // fires the native prompt, merges GPS into geo, re-persists
}
// "granted" → already captured during init(); "denied"/"unsupported" → keep the IP fallback.

Standalone helpers

import {
  getGeolocationPermission, // → "granted" | "denied" | "prompt" | "unsupported"  (no prompt)
  getBrowserLocation,       // GPS fix + reverse-geocode → { city, region, country, countryCode, latitude, longitude, accuracy, source: "gps" }
  getBrowserGeolocation,    // raw GPS coords only → { latitude, longitude, accuracy }
  reverseGeocode,           // (lat, lng) → { city, region, regionCode, country, countryCode } via BigDataCloud
} from "@eshal-tracker/sdk";

Configuring API Keys

Option 1 — Global config (recommended, set once at startup)

import UserTrace, { configureGeo } from "user-trace";

// Call this before init()
configureGeo({
  apiKeys: {
    ipinfo: "your_ipinfo_token",
    // ipgeolocation: "...",
    // abstractapi: "...",
    // ip2location: "...",
    // ipstack: "...",
  },
  preferredProvider: "ipinfo",  // always try this first
  timeoutMs: 4000,              // per-request timeout (default: 5000)
});

const tracer = new UserTrace({ autoInit: true, onReady: console.log });

Option 2 — Per-call override

import { resolveLocation } from "user-trace";

const geo = await resolveLocation("203.0.113.42", {
  apiKeys: { ipinfo: "your_token" },
});

Option 3 — Pass keys through UserTrace constructor

const tracer = new UserTrace({
  geoApiKeys: { ipinfo: "your_token" },
  onReady: (profile) => console.log(profile.geo),
});

await tracer.init();

List Available Providers

import { listProviders } from "user-trace";

// See all providers and which are active
console.table(listProviders());

// Pass your keys to see which paid providers would activate
console.table(listProviders({ ipinfo: "tok_xyz" }));

Output:

┌─────────────────────┬────────────────┬───────────────────────────┐
│ name                │ tier           │ freeLimit                 │
├─────────────────────┼────────────────┼───────────────────────────┤
│ ip-api.com          │ free           │ 45 req/min (no key)       │
│ ipwho.is            │ free           │ unlimited (fair use)      │
│ ipapi.co            │ free           │ 1,000 req/day (no key)    │
│ freeipapi.com       │ free           │ unlimited (fair use)      │
│ ipinfo.io           │ free-with-key  │ 50,000 req/month          │
│ ip2location.io      │ free-with-key  │ 30,000 req/month          │
│ abstractapi.com     │ free-with-key  │ 20,000 req/month          │
│ ipgeolocation.io    │ free-with-key  │ 1,000 req/day             │
│ ipstack.com         │ free-with-key  │ 100 req/month             │
└─────────────────────┴────────────────┴───────────────────────────┘

Full API

new UserTrace(options?)

| Option | Type | Default | Description | |---|---|---|---| | onReady | Function | null | Called with the full profile when ready | | onError | Function | null | Called on any error | | geoApiKeys | Object | {} | API keys for geo providers (see above) | | geoFromBrowser | boolean \| "auto" | true | Capture precise GPS and reverse-geocode it into geo. Default true = request on init() (shows the native prompt unless already granted/denied); false = IP only; "auto" = silent only if already granted. See Precise Location. | | geoOptions | PositionOptions | — | Override getCurrentPosition options (enableHighAccuracy, timeout, maximumAge). | | cache | boolean | true | Use cached profile if younger than maxAgeMs | | maxAgeMs | number | 300000 | Cache lifetime in ms (default: 5 min) | | autoInit | boolean | false | Call init() automatically on construction | | webhookUrl | string | null | POST profile JSON to this URL |


tracer.init()Promise<UserProfile>

Collects all signals and resolves with the full profile. Safe to call multiple times (returns cache on repeat calls).

tracer.captureBrowserLocation(options?)Promise<UserProfile | null>

Call after your UI has obtained user consent (e.g. an in-app "Share your location?" pre-ask). Triggers the native permission prompt, reverse-geocodes the GPS fix, merges it into profile.geo (source: "gps"), re-persists, and fires onReady. No-ops gracefully when denied/unavailable (keeps the IP fallback). See Precise Location.

tracer.getProfile()UserProfile | null

Synchronously returns the current profile, or null before init() completes.

tracer.getSummary()UserTraceSummary

Returns a compact, loggable summary:

{
  uid, sid, fingerprintId,
  ip, country, city, isp, proxy,
  timezone, userAgent, language, createdAt
}

tracer.refresh()Promise<UserProfile>

Force a full re-collect — clears cache and re-fetches everything.

tracer.clear()

Removes all stored IDs from localStorage / sessionStorage.


Named / Modular Exports

Tree-shakeable — import only what you need:

import {
  configureGeo,             // configure geo providers globally
  resolveLocation,          // resolve IP → geo data
  listProviders,            // list all available providers
  getGeolocationPermission, // geolocation permission state (no prompt)
  getBrowserLocation,       // GPS fix + reverse-geocode → accurate geo
  getBrowserGeolocation,    // raw GPS coords only
  reverseGeocode,           // (lat, lng) → city/region/country
  generateFingerprint,      // browser fingerprint
  detectIP,                 // detect public IP
  getBrowserContext,        // browser environment info
  getOrCreateUserID,        // persistent user ID
  getOrCreateSessionID,     // per-tab session ID
  clearSession,             // erase stored IDs
} from "@eshal-tracker/sdk";

UserProfile Shape

{
  uid:                   string,   // Long-lived user ID (localStorage)
  sid:                   string,   // Per-tab session ID (sessionStorage)
  isNewUser:             boolean,
  isNewSession:          boolean,
  fingerprintId:         string,   // Stable browser fingerprint hash
  fingerprintConfidence: number,   // 0–100 confidence score
  ip:                    string,   // e.g. "203.0.113.42"
  ipVersion:             "v4" | "v6" | "unknown",
  geo: {
    country, countryCode,
    region,  regionCode,
    city,    zip,
    latitude, longitude,
    timezone, isp, org,
    proxy,    // boolean — VPN / proxy detected?
    hosting,  // boolean — datacenter / cloud IP?
    source,   // which provider resolved this (e.g. "freeipapi.com") — or "gps" when a device fix overrode it
    tier,     // "free" | "free-with-key"
  },
  browserGeo: { latitude, longitude, accuracy } | null,
  browser: {
    userAgent, platform, language, timezone,
    screenRes, viewportRes, colorDepth,
    touchSupport, cookieEnabled,
    referrer, currentUrl, loadedAt,
  },
  raw:       { ...all fingerprint signals },
  createdAt: string,   // ISO timestamp
  mapsUrl:   string,   // Google Maps link for the IP location
}

Webhook

Pass webhookUrl and user-trace will POST the full profile as JSON on every init():

const tracer = new UserTrace({
  webhookUrl: "https://your-api.com/analytics/track",
});

Your server receives the full UserProfile object as JSON.


Privacy & Compliance

  • No cookies are used. IDs live in localStorage / sessionStorage.
  • IP geolocation calls go directly from the browser to third-party free APIs — no data passes through user-trace servers.
  • Browser fingerprinting produces a short non-unique hash — not a globally unique identifier by design.
  • You are responsible for disclosing tracking in your Privacy Policy and complying with GDPR / CCPA and other applicable laws.
  • Use tracer.clear() to erase all stored user data on logout or user request.

License

MIT