express-sequelize-traffic
v0.7.1
Published
Privacy-first, self-hosted Express request analytics with Sequelize storage and an optional realtime dashboard.
Downloads
761
Maintainers
Readme
express-sequelize-traffic
express-sequelize-traffic is a privacy-first, self-hosted npm package for Express.js applications. It records request analytics directly into your own Sequelize database and provides an optional realtime dashboard with REST APIs and Socket.IO updates.
No traffic data is sent to Google Analytics, Prometheus, Segment, Sentry, or any external service. The package only writes to the Sequelize connection you provide.
Features
- Express middleware that tracks every request without interrupting the main app on failure
- Sequelize
TrafficLogmodel stored in thetraffic_logstable - Request tracking for user, session, route, status code, duration, slow routes, IP, user agent, start time, and end time
- Analytics REST API under the dashboard router
- Live dashboard updates through database polling or Socket.IO
- Optional built-in dashboard served from Express
- Simple HTTP basic authentication for the dashboard
- SQLite-backed example app for local testing
Installation
Install the package dependencies:
npm install express-sequelize-trafficIf you are working on this repository directly:
npm install
npm run buildQuick Setup
The package defines a TrafficLog model for you. You can either manage the table through your own migrations or call traffic.sync() for quick setup. traffic.sync() wraps Sequelize model sync and creates the traffic_logs table if it does not already exist.
import { Sequelize } from "sequelize";
import { createTrafficTracker } from "express-sequelize-traffic";
const sequelize = new Sequelize(process.env.DB_URL);
const traffic = createTrafficTracker({ sequelize });
await sequelize.authenticate();
await traffic.sync();ES Module Setup
Use this form when the target app uses "type": "module" or native ESM imports.
import express from "express";
import http from "node:http";
import { Sequelize } from "sequelize";
import { createTrafficTracker } from "express-sequelize-traffic";
const app = express();
const server = http.createServer(app);
const sequelize = new Sequelize(process.env.DB_URL);
const traffic = createTrafficTracker({
sequelize,
getUserDetails: (req) =>
req.user
? {
id: req.user.id,
name: req.user.name,
email: req.user.email,
}
: null,
getSessionId: (req) => req.sessionID || req.headers["x-session-id"],
slowRouteThresholdMs: 1000,
includeRoutes: ["/api/*"],
routeValue: "pattern",
trackIp: true,
trackUserAgent: true,
ignoredRoutes: ["/health", "/favicon.ico"],
dashboard: {
enabled: true,
mountPath: "/traffic-dashboard",
liveMode: "socket",
username: process.env.TRAFFIC_ADMIN_USER,
password: process.env.TRAFFIC_ADMIN_PASS,
},
});
await traffic.sync();
app.use(traffic.middleware);
traffic.attachRealtime(server);
app.use("/traffic-dashboard", traffic.dashboard);
server.listen(3000);CommonJS Setup
The package also publishes a CommonJS entry, so require(...) works directly.
const express = require("express");
const http = require("node:http");
const { Sequelize } = require("sequelize");
const { createTrafficTracker } = require("express-sequelize-traffic");
const app = express();
const server = http.createServer(app);
const sequelize = new Sequelize(process.env.DB_URL, {
logging: false,
});
async function start() {
const traffic = createTrafficTracker({
sequelize,
getUserDetails: (req) =>
req.user
? {
id: req.user.id,
name: req.user.name,
email: req.user.email,
}
: null,
getSessionId: (req) => req.sessionID || req.headers["x-session-id"] || null,
slowRouteThresholdMs: 1000,
includeRoutes: ["/api/*"],
ignoredRoutes: ["/health", "/favicon.ico"],
routeValue: "pattern",
trackIp: true,
trackUserAgent: true,
dashboard: {
enabled: true,
mountPath: "/traffic-dashboard",
liveMode: "socket",
username: process.env.TRAFFIC_ADMIN_USER,
password: process.env.TRAFFIC_ADMIN_PASS,
},
});
await sequelize.authenticate();
await traffic.sync();
app.use(traffic.middleware);
traffic.attachRealtime(server);
app.use("/traffic-dashboard", traffic.dashboard);
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
server.listen(3000);
}
start().catch((error) => {
console.error("Startup failed:", error);
process.exit(1);
});Common Tracking Presets
Use the default grouped route format for normal API analytics:
const traffic = createTrafficTracker({
sequelize,
includeRoutes: ["/api/*"],
routeValue: "pattern",
});Use literal file or media paths when every concrete URL matters:
const traffic = createTrafficTracker({
sequelize,
includeRoutes: ["/media/file/*"],
routeValue: "path",
});Use the full URL when query string differences also matter:
const traffic = createTrafficTracker({
sequelize,
includeRoutes: ["/search/*"],
routeValue: "originalUrl",
});Dashboard Live Modes
The dashboard supports three live modes:
poll- default
- production-safe for single-process, cluster, PM2, containers, and load-balanced apps
- the dashboard refreshes from the analytics APIs on an interval
socket- best for single-process apps
- requires
traffic.attachRealtime(server)
socket-cluster- advanced mode for clustered deployments
- requires
traffic.attachRealtime(server)anddashboard.socketCluster.configure(io, context) - the frontend uses websocket-only transport in this mode to avoid polling session mismatch
Single-process example:
const traffic = createTrafficTracker({
sequelize,
dashboard: {
enabled: true,
mountPath: "/traffic-dashboard",
liveMode: "socket",
},
});
app.use(traffic.middleware);
app.use("/traffic-dashboard", traffic.dashboard);
const server = app.listen(3000);
traffic.attachRealtime(server);Multi-core / clustered example:
const traffic = createTrafficTracker({
sequelize,
dashboard: {
enabled: true,
mountPath: "/traffic-dashboard",
liveMode: "poll",
pollIntervalMs: 2000,
},
});
app.use(traffic.middleware);
app.use("/traffic-dashboard", traffic.dashboard);
app.listen(3000);Clustered Socket.IO example with an explicit hook:
const traffic = createTrafficTracker({
sequelize,
dashboard: {
enabled: true,
mountPath: "/traffic-dashboard",
liveMode: "socket-cluster",
socketCluster: {
configure(io, context) {
// apply your Socket.IO adapter here
// io.adapter(...)
// use context.server / context.socketPath if needed
},
},
},
});Passing User Details
The package does not assume how authentication works in your app. You pass user and session details through resolver functions.
If you only need a user id, getUserId is enough:
const traffic = createTrafficTracker({
sequelize,
getUserId: (req) => req.user?.id || req.auth?.sub || null,
});If you want the dashboard user drill-down to show profile data, use getUserDetails:
const traffic = createTrafficTracker({
sequelize,
getUserDetails: (req) =>
req.user
? {
id: req.user.id,
name: req.user.name,
email: req.user.email,
}
: null,
getSessionId: (req) =>
req.sessionID || req.cookies?.sessionId || req.headers["x-session-id"] || null,
trackIp: true,
trackUserAgent: true,
});getUserDetails may return any of these fields:
idoruserIdnameoruserNameemailoruserEmail
The user drill-down page uses those fields together with trackIp and trackUserAgent to show:
- resolved user name and email
- user id
- remote IP details
- device and user-agent details
- route, slow-route, and error-route tables for that user
Common sources are:
req.user.idfrom Passport or your own auth middlewarereq.user.nameandreq.user.emailfrom your session or auth layerreq.auth.subfrom JWT middlewarereq.sessionIDfromexpress-session- custom request headers in internal systems
Rules to follow:
- Register auth and session middleware before
traffic.middleware - Return
nullwhen user or session data is unavailable - If you omit
getUserId,getUserDetails, orgetSessionId, those fields are stored asNULL - If you already have an older
traffic_logstable, add theuserNameanduserEmailcolumns through a migration ortraffic.sync({ alter: true })before enablinggetUserDetails
Choosing What Gets Stored In route
By default, the package stores the Express route pattern in the route column. For a route like:
app.get("/media/file/:storedName", handler);the saved values are typically:
route:"/media/file/:storedName"originalUrl:"/media/file/1772884983691_497047272_image11_low.jpeg"
That default is intentional because it groups analytics by route definition instead of creating a different bucket for every file name, ID, slug, or UUID.
If you want the route column to store something else, use routeValue.
Behavior Summary
| routeValue | Saved route example | Includes query string | Best for |
| --- | --- | --- | --- |
| "pattern" | /media/file/:storedName | no | grouped route analytics |
| "path" | /media/file/1772884983691_497047272_image11_low.jpeg | no | literal paths such as files, slugs, or IDs |
| "originalUrl" | /media/file/1772884983691_497047272_image11_low.jpeg?download=1 | yes | cases where query strings are part of the analysis |
Configuration Examples
Store grouped Express route patterns:
const traffic = createTrafficTracker({
sequelize,
routeValue: "pattern",
});Store the concrete request path without the query string:
const traffic = createTrafficTracker({
sequelize,
routeValue: "path",
});Store the full original URL, including query string when present:
const traffic = createTrafficTracker({
sequelize,
routeValue: "originalUrl",
});Important Matching Rule
includeRoutes and ignoredRoutes are matched against all of these request forms:
- the normalized Express route pattern
- the concrete request path
- the full original URL
That means this works even when routeValue is still "pattern":
const traffic = createTrafficTracker({
sequelize,
includeRoutes: ["/media/file/*"],
});For a request to:
/media/file/1772884983691_497047272_image11_low.jpegthe tracker will:
- match
includeRoutes: ["/media/file/*"] - save
routeas/media/file/:storedNamewhenrouteValueis"pattern" - save
originalUrlas the real requested URL
Switch to routeValue: "path" only if you want the route column itself to store the concrete path.
Value Meanings
Available values:
pattern- default
- best for grouped route analytics
path- stores literal paths like
/media/file/1772884983691_497047272_image11_low.jpeg - excludes query strings
- stores literal paths like
originalUrl- stores the full incoming URL path and query string
Changing routeValue only changes what is persisted in the route column. It does not narrow route matching to one format.
Why It Tracks More Than Application Routes
This package is request middleware. If you mount it globally with:
app.use(traffic.middleware);it will see every request that reaches that point in the Express stack, not just your business endpoints.
That includes:
- API routes
- page routes
- health checks
- static files
- 404 requests
- dashboard API requests
- dashboard frontend asset requests
By default, the tracker stores the route from req.route?.path, then falls back to req.path, then req.originalUrl. That is why it can still log requests even when Express does not resolve them to a named application route. If you need literal request paths instead of normalized Express patterns, set routeValue: "path" or routeValue: "originalUrl".
How To Track Only Application Routes
If you want to limit tracking to business endpoints, start with includeRoutes. It is usually easier than maintaining a long ignoredRoutes list.
Track only route families you want:
const traffic = createTrafficTracker({
sequelize,
includeRoutes: ["/api/*", "/admin/*"],
});Track media downloads by concrete URL path while still grouping other routes normally:
const traffic = createTrafficTracker({
sequelize,
includeRoutes: ["/media/file/*"],
routeValue: "path",
});String matchers support:
- exact matches like
"/health" - prefix-style wildcards like
"/api/*" - regex matchers like
/^\/v[0-9]+\// - custom functions
If includeRoutes is provided, only matching requests are tracked. You can still use ignoredRoutes to exclude specific subsets from a broader include rule.
Example:
const traffic = createTrafficTracker({
sequelize,
includeRoutes: ["/api/*"],
ignoredRoutes: ["/api/internal/*"],
});You can also use one or more of these mounting patterns.
Mount the tracker only on the route prefix you care about:
app.use("/api", traffic.middleware);Register static assets or health checks before the tracker:
app.use("/public", express.static("public"));
app.get("/health", (_req, res) => res.json({ ok: true }));
app.use(traffic.middleware);Ignore route groups with regex:
const traffic = createTrafficTracker({
sequelize,
ignoredRoutes: [
"/health",
"/favicon.ico",
/^\/traffic-dashboard(\/|$)/,
/^\/public(\/|$)/,
],
});Use a function when the skip logic depends on route naming conventions:
const traffic = createTrafficTracker({
sequelize,
ignoredRoutes: [
(route) => route.startsWith("/internal/"),
],
});If you mount the tracker globally, global tracking is expected behavior.
Dashboard Usage
The dashboard is optional. Set dashboard.enabled to true, build the frontend assets, and mount the provided router.
If you use dashboard.liveMode: "socket" or dashboard.liveMode: "socket-cluster", call traffic.attachRealtime(server) after the HTTP server is created. If you use dashboard.liveMode: "poll", do not call attachRealtime.
The dashboard expects the router mount path to match dashboard.mountPath. If you keep the default /traffic-dashboard, the client-side API and Socket.IO paths line up automatically.
npm run build:dashboardOptions
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| sequelize | Sequelize | required | Sequelize instance used for the TrafficLog model |
| getUserId | (req) => string \| null | undefined | Custom resolver for userId |
| getUserDetails | (req) => { id?, name?, email? } \| null | undefined | Optional resolver for userId, userName, and userEmail |
| getSessionId | (req) => string \| null | undefined | Custom resolver for sessionId |
| slowRouteThresholdMs | number | 1000 | Marks requests as slow when duration meets or exceeds this value |
| includeRoutes | Array<string \| RegExp \| Function> | [] | If set, only matching routes or URLs are tracked |
| ignoredRoutes | Array<string \| RegExp \| Function> | [] | Routes or URLs to skip |
| routeValue | "pattern" \| "path" \| "originalUrl" | "pattern" | Controls what is persisted in the route column |
| trackIp | boolean | false | Persist req.ip when enabled |
| trackUserAgent | boolean | true | Persist the request user-agent header when enabled |
| dashboard.enabled | boolean | false | Enables dashboard APIs and static UI |
| dashboard.mountPath | string | "/traffic-dashboard" | Mount path used by the dashboard UI and realtime socket |
| dashboard.liveMode | "poll" \| "socket" \| "socket-cluster" | "poll" | Controls whether the dashboard uses API polling or Socket.IO |
| dashboard.pollIntervalMs | number | 2000 | Refresh interval used by dashboard polling mode |
| dashboard.username | string | undefined | Basic auth username for the dashboard |
| dashboard.password | string | undefined | Basic auth password for the dashboard |
| dashboard.socketCluster.configure | (io, context) => void | undefined | Required hook for dashboard.liveMode: "socket-cluster" |
| debug | boolean | false | Logs tracking or analytics errors without throwing them |
API Routes
When the dashboard router is mounted, these endpoints are available under /<mountPath>/api:
GET /api/configGET /api/overviewGET /api/liveGET /api/routesGET /api/usersGET /api/users/:userIdGET /api/errors
GET /api/config returns the dashboard runtime mode used by the built frontend, including:
liveModepollIntervalMssocket.pathsocket.transportswhen applicable
GET /api/users/:userId returns a user drill-down payload with:
userroutesslowRouteserrorRoutesdevicesipAddressessessions
GET /api/overview returns:
totalRequestsaverageDurationMsslowRequestCounterrorRequestCountuniqueUsersactiveUsersLastFiveMinutestopRoutesslowestRoutesstatusCodeSummary
activeUsersLastFiveMinutes is the distinct count of resolved userId values seen during the most recent rolling five-minute window.
The implementation also includes latestRequests and requestsTimeline to support the dashboard overview page.
Realtime Events
Call traffic.attachRealtime(server) with your Node HTTP server when dashboard.liveMode is "socket" or "socket-cluster". Each stored request emits:
- Event:
traffic:new-request
Payload:
{
"userId": "123",
"sessionId": "session-1",
"method": "GET",
"route": "/api/products",
"originalUrl": "/api/products?page=1",
"statusCode": 200,
"durationMs": 187,
"isSlow": false,
"createdAt": "2026-05-07T12:00:00.000Z"
}Example App
Run the SQLite example:
npm run exampleExample endpoints:
GET /api/productsGET /api/slowGET /api/errorGET /api/users/:idGET /traffic-dashboard/
The example dashboard uses HTTP basic auth with:
- username:
admin - password:
password
Override them with TRAFFIC_ADMIN_USER and TRAFFIC_ADMIN_PASS.
Screenshot Placeholders
- Overview dashboard screenshot placeholder
- Live traffic dashboard screenshot placeholder
Capture real screenshots after building and running the example app.
License
MIT
