Node.js SDK for Micrologs - self-hosted analytics and error tracking.
Requires Node.js 18+
Micrologs is an engine you install on your own server. You own the database, you own the data. This SDK is a thin wrapper around its REST API - it makes HTTP calls to your server, not to any third-party service.
Your Node app → SDK → your Micrologs server → your database
Nothing goes anywhere you don't control.
npm install @micrologs/node// CommonJS
const Micrologs = require("@micrologs/node")
// ESM
import Micrologs from "@micrologs/node"
const client = new Micrologs({
host: "https://analytics.yourdomain.com", // your server - where Micrologs is installed
key: "your_secret_key" // your project secret key
})Both host and key are required. The constructor will throw immediately if either is missing.
host is the URL of the server where you installed Micrologs. key is the secret key for your project - find it in your env.php. Never use the public key here - that's for the JS snippet only.
Use this to send errors from your Node backend to Micrologs. Works alongside the JS snippet - the snippet catches frontend errors, this catches backend errors.
try {
await processPayment(order)
} catch (err) {
await client.error(err.message, {
type: "CheckoutError", // groups errors of the same type together
severity: "critical", // info | warning | error | critical
file: "checkout.js",
line: 42,
stack: err.stack,
url: "/api/checkout",
environment: "production", // default: "production"
context: { order_id: 123, amount: 2999 } // any extra data, capped at 8KB
})
}All fields except message are optional.
How grouping works: Micrologs hashes error_type + message + file + line into a fingerprint. The same error firing 1000 times creates 1 group with 1000 occurrences - not 1000 separate records. If you mark a group as resolved and it fires again, it automatically reopens.
Response:
{
"success": true,
"message": "OK",
"data": {
"queued": true
}
}The error is queued for async processing — the worker handles the DB write in the background.
Use this to record any action that matters in your application - logins, payments, config changes, anything you want a trail for.
// action (required), actor (optional), context (optional)
await client.audit("user.login", "user@email.com", { ip: "1.2.3.4", role: "admin" })
await client.audit("order.placed", "user@email.com", { order_id: 123, amount: 2999 })
await client.audit("settings.updated", "admin@email.com")
await client.audit("api_key.rotated", "admin@email.com")action is a free-form string - use dot notation by convention (resource.action) for easy filtering. actor is whoever triggered the action. context is any extra data you want attached.
const link = await client.createLink(
"https://yourdomain.com/pricing", // destination URL
"Pricing CTA" // optional label
)
console.log(link.data)
// {
// code: "aB3xYz12",
// short_url: "https://analytics.yourdomain.com/api/redirect.php?c=aB3xYz12",
// destination_url: "https://yourdomain.com/pricing",
// label: "Pricing CTA"
// }Every click on the short link is tracked with timestamp, referrer, and location. Query click analytics via client.analytics.linkDetail({ code: "aB3xYz12" }).
await client.deleteLink("aB3xYz12")const link = await client.getLink("aB3xYz12")
// Returns link details including total_clicks// Any combination of fields - all optional except code
await client.editLink("aB3xYz12", {
destinationUrl: "https://yourdomain.com/new-page",
label: "Updated CTA",
isActive: false
})All analytics methods return data from your Micrologs server as plain JSON. Every response follows this shape:
{
"success": true,
"message": "...",
"data": { ... }
}Access your data via result.data.
All analytics methods accept an optional params object:
| Param | Type | Default | Description |
|---|---|---|---|
range |
string | "30d" |
"7d" / "30d" / "90d" / "custom" |
from |
string | - | "YYYY-MM-DD" - required when range="custom" |
to |
string | - | "YYYY-MM-DD" - required when range="custom" |
Custom ranges are capped at 365 days and from must be before to.
const result = await client.analytics.visitors({ range: "30d" })
console.log(result.data)
// {
// range: { from: "2026-01-31", to: "2026-03-01" },
// unique_visitors: 1842,
// total_pageviews: 5631,
// total_sessions: 2109,
// bounce_rate: 43.2,
// over_time: [
// { date: "2026-01-31", pageviews: 178, unique_visitors: 91 },
// { date: "2026-02-01", pageviews: 204, unique_visitors: 113 },
// ...
// ]
// }const result = await client.analytics.returning({ range: "30d" })
console.log(result.data)
// {
// total_visitors: 500,
// new_visitors: 340,
// returning_visitors: 160,
// new_pct: 68.0,
// returning_pct: 32.0,
// over_time: [
// { date: "2026-02-01", new_visitors: 12, returning_visitors: 5 },
// ...
// ]
// }New = first visit falls within the selected range. Returning = visited before the range and came back within it.
const result = await client.analytics.sessions({ range: "7d" })
console.log(result.data)
// {
// total_sessions: 2109,
// bounce_rate: 43.2,
// avg_duration_seconds: 91, // all sessions including bounces
// avg_duration_engaged: 180, // bounced sessions excluded - more honest number
// avg_pages_per_session: 2.67,
// over_time: [
// { date: "2026-02-01", sessions: 45, avg_duration_seconds: 120 },
// ...
// ]
// }avg_duration_engaged excludes bounced sessions (visitors who left immediately). It's a more accurate measure of how long engaged users actually stay.
const result = await client.analytics.pages({ range: "30d", limit: 10 })
console.log(result.data)
// [
// { url: "/", page_title: "Home", pageviews: 1200, unique_visitors: 800 },
// { url: "/pricing", page_title: "Pricing", pageviews: 430, unique_visitors: 310 },
// ...
// ]const result = await client.analytics.locations({ range: "30d" })
// Breakdown by country, region, and cityconst result = await client.analytics.devices({ range: "30d" })
// Breakdown by device type (desktop/mobile/tablet), OS, browserconst result = await client.analytics.referrers({ range: "30d" })
// Traffic sources categorized: organic, social, email, referral, directconst result = await client.analytics.utm({ range: "30d" })
// Breakdown by utm_source, utm_medium, utm_campaign// All error groups
const result = await client.analytics.errors({ range: "30d" })
// Filtered
const filtered = await client.analytics.errors({
range: "30d",
status: "open",
severity: "critical",
environment: "production",
})
// Daily error trend across all groups
const trend = await client.analytics.errorsTrend({ range: "30d" })
// Daily trend for one specific error group
const groupTrend = await client.analytics.errorsTrend({ range: "30d", group_id: 12 })
// Full detail for one error group - all occurrences, stack traces, context
const detail = await client.analytics.errorDetail({ id: 12 })Mark error groups as investigating, resolved, ignored, or reopen as open. Accepts a single ID or an array of up to 100.
// Start investigating
await client.updateErrorStatus(42, "investigating")
// Resolve a single group
await client.updateErrorStatus(42, "resolved")
// Bulk ignore
await client.updateErrorStatus([12, 15, 22], "ignored")const result = await client.analytics.audits({ range: "7d" })
// Filtered
const filtered = await client.analytics.audits({
range: "30d",
action: "user.login",
actor: "user@email.com",
})// All links with click counts
const result = await client.analytics.links({ range: "30d" })
// Detail for one link - clicks over time
const detail = await client.analytics.linkDetail({ code: "aB3xYz12", range: "30d" })const result = await client.analytics.visitors({
range: "custom",
from: "2026-01-01",
to: "2026-01-31"
})const result = await client.verify("some_key")
// Returns whether the key is valid and which project it belongs toThe SDK never throws or crashes your application. If a network error occurs, the server is unreachable, or the server returns an error - the method returns null and logs a warning via console.warn. This is intentional.
const result = await client.error("Payment failed")
if (result === null) {
// SDK call failed - your server may be unreachable
// Your application continues normally regardless
}Analytics failures should never affect your application's users.
| Method | Description |
|---|---|
client.error(message, options?) |
Track a backend error |
client.audit(action, actor?, context?) |
Track an audit event |
client.createLink(destinationUrl, label?) |
Create a tracked short link |
client.getLink(code) |
Fetch a single tracked link by code |
client.editLink(code, options?) |
Edit a link's destination, label, or active state |
client.deleteLink(code) |
Delete a tracked link by code |
client.updateErrorStatus(ids, status) |
Update error group status - single ID or array |
client.verify(key) |
Verify a public or secret key |
client.analytics.visitors(params?) |
Unique visitors, pageviews, sessions, bounce rate |
client.analytics.returning(params?) |
New vs returning visitors |
client.analytics.sessions(params?) |
Session duration, pages per session |
client.analytics.pages(params?) |
Top pages by pageviews |
client.analytics.devices(params?) |
Device, OS, browser breakdown |
client.analytics.locations(params?) |
Country, region, city breakdown |
client.analytics.referrers(params?) |
Traffic sources |
client.analytics.utm(params?) |
UTM campaign data |
client.analytics.errors(params?) |
Error groups with occurrence counts |
client.analytics.errorsTrend(params?) |
Daily error trend, top groups |
client.analytics.errorDetail(params?) |
Single error group - all occurrences and detail |
client.analytics.audits(params?) |
Audit log events |
client.analytics.links(params?) |
Tracked links with click counts |
client.analytics.linkDetail(params?) |
Single link - clicks over time |
- Node.js 18+ - uses native
fetchand private class fields (#field). No polyfills, no build step. - A running Micrologs server (v1.3.0+)
MIT - Om Dongaonkar