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

express-sequelize-traffic

v0.7.1

Published

Privacy-first, self-hosted Express request analytics with Sequelize storage and an optional realtime dashboard.

Downloads

761

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 TrafficLog model stored in the traffic_logs table
  • 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-traffic

If you are working on this repository directly:

npm install
npm run build

Quick 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) and dashboard.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:

  • id or userId
  • name or userName
  • email or userEmail

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.id from Passport or your own auth middleware
  • req.user.name and req.user.email from your session or auth layer
  • req.auth.sub from JWT middleware
  • req.sessionID from express-session
  • custom request headers in internal systems

Rules to follow:

  • Register auth and session middleware before traffic.middleware
  • Return null when user or session data is unavailable
  • If you omit getUserId, getUserDetails, or getSessionId, those fields are stored as NULL
  • If you already have an older traffic_logs table, add the userName and userEmail columns through a migration or traffic.sync({ alter: true }) before enabling getUserDetails

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.jpeg

the tracker will:

  • match includeRoutes: ["/media/file/*"]
  • save route as /media/file/:storedName when routeValue is "pattern"
  • save originalUrl as 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
  • 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:dashboard

Options

| 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/config
  • GET /api/overview
  • GET /api/live
  • GET /api/routes
  • GET /api/users
  • GET /api/users/:userId
  • GET /api/errors

GET /api/config returns the dashboard runtime mode used by the built frontend, including:

  • liveMode
  • pollIntervalMs
  • socket.path
  • socket.transports when applicable

GET /api/users/:userId returns a user drill-down payload with:

  • user
  • routes
  • slowRoutes
  • errorRoutes
  • devices
  • ipAddresses
  • sessions

GET /api/overview returns:

  • totalRequests
  • averageDurationMs
  • slowRequestCount
  • errorRequestCount
  • uniqueUsers
  • activeUsersLastFiveMinutes
  • topRoutes
  • slowestRoutes
  • statusCodeSummary

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 example

Example endpoints:

  • GET /api/products
  • GET /api/slow
  • GET /api/error
  • GET /api/users/:id
  • GET /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