@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-traceQuick 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
