express-sequelize-traffic 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,9 +30,9 @@ npm install
30
30
  npm run build
31
31
  ```
32
32
 
33
- ## Sequelize Setup
33
+ ## Quick Setup
34
34
 
35
- 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.
35
+ 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.
36
36
 
37
37
  ```js
38
38
  import { Sequelize } from "sequelize";
@@ -46,7 +46,9 @@ await sequelize.authenticate();
46
46
  await traffic.sync();
47
47
  ```
48
48
 
49
- ## Express Usage
49
+ ## ES Module Setup
50
+
51
+ Use this form when the target app uses `"type": "module"` or native ESM imports.
50
52
 
51
53
  ```js
52
54
  import express from "express";
@@ -63,6 +65,7 @@ const traffic = createTrafficTracker({
63
65
  getUserId: (req) => req.user?.id || req.headers["x-user-id"],
64
66
  getSessionId: (req) => req.sessionID || req.headers["x-session-id"],
65
67
  slowRouteThresholdMs: 1000,
68
+ includeRoutes: ["/api/*"],
66
69
  trackIp: false,
67
70
  trackUserAgent: true,
68
71
  ignoredRoutes: ["/health", "/favicon.ico"],
@@ -83,15 +86,186 @@ app.use("/traffic-dashboard", traffic.dashboard);
83
86
  server.listen(3000);
84
87
  ```
85
88
 
86
- CommonJS applications can use the published `require(...)` entry:
89
+ ## CommonJS Setup
90
+
91
+ The package also publishes a CommonJS entry, so `require(...)` works directly.
87
92
 
88
93
  ```js
89
94
  const express = require("express");
90
95
  const http = require("node:http");
91
96
  const { Sequelize } = require("sequelize");
92
97
  const { createTrafficTracker } = require("express-sequelize-traffic");
98
+
99
+ const app = express();
100
+ const server = http.createServer(app);
101
+
102
+ const sequelize = new Sequelize(process.env.DB_URL, {
103
+ logging: false,
104
+ });
105
+
106
+ async function start() {
107
+ const traffic = createTrafficTracker({
108
+ sequelize,
109
+ getUserId: (req) => req.user?.id || req.headers["x-user-id"] || null,
110
+ getSessionId: (req) => req.sessionID || req.headers["x-session-id"] || null,
111
+ slowRouteThresholdMs: 1000,
112
+ includeRoutes: ["/api/*"],
113
+ ignoredRoutes: ["/health", "/favicon.ico"],
114
+ trackIp: false,
115
+ trackUserAgent: true,
116
+ dashboard: {
117
+ enabled: true,
118
+ mountPath: "/traffic-dashboard",
119
+ username: process.env.TRAFFIC_ADMIN_USER,
120
+ password: process.env.TRAFFIC_ADMIN_PASS,
121
+ },
122
+ });
123
+
124
+ await sequelize.authenticate();
125
+ await traffic.sync();
126
+
127
+ app.use(traffic.middleware);
128
+ traffic.attachRealtime(server);
129
+ app.use("/traffic-dashboard", traffic.dashboard);
130
+
131
+ app.get("/health", (_req, res) => {
132
+ res.json({ ok: true });
133
+ });
134
+
135
+ server.listen(3000);
136
+ }
137
+
138
+ start().catch((error) => {
139
+ console.error("Startup failed:", error);
140
+ process.exit(1);
141
+ });
142
+ ```
143
+
144
+ ## Passing User Details
145
+
146
+ The package does not assume how authentication works in your app. You pass user and session details through resolver functions.
147
+
148
+ ```js
149
+ const traffic = createTrafficTracker({
150
+ sequelize,
151
+ getUserId: (req) =>
152
+ req.user?.id || req.auth?.sub || req.headers["x-user-id"] || null,
153
+ getSessionId: (req) =>
154
+ req.sessionID || req.cookies?.sessionId || req.headers["x-session-id"] || null,
155
+ });
156
+ ```
157
+
158
+ Common sources are:
159
+
160
+ - `req.user.id` from Passport or your own auth middleware
161
+ - `req.auth.sub` from JWT middleware
162
+ - `req.sessionID` from `express-session`
163
+ - custom request headers in internal systems
164
+
165
+ Rules to follow:
166
+
167
+ - Register auth and session middleware before `traffic.middleware`
168
+ - Return `null` when user or session data is unavailable
169
+ - If you omit `getUserId` or `getSessionId`, those fields are stored as `NULL`
170
+
171
+ ## Why It Tracks More Than Application Routes
172
+
173
+ This package is request middleware. If you mount it globally with:
174
+
175
+ ```js
176
+ app.use(traffic.middleware);
177
+ ```
178
+
179
+ it will see every request that reaches that point in the Express stack, not just your business endpoints.
180
+
181
+ That includes:
182
+
183
+ - API routes
184
+ - page routes
185
+ - health checks
186
+ - static files
187
+ - 404 requests
188
+ - dashboard API requests
189
+ - dashboard frontend asset requests
190
+
191
+ Internally, the tracker records 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.
192
+
193
+ ## How To Track Only Application Routes
194
+
195
+ If you want to limit tracking to business endpoints, start with `includeRoutes`. It is usually easier than maintaining a long `ignoredRoutes` list.
196
+
197
+ Track only route families you want:
198
+
199
+ ```js
200
+ const traffic = createTrafficTracker({
201
+ sequelize,
202
+ includeRoutes: ["/api/*", "/admin/*"],
203
+ });
204
+ ```
205
+
206
+ String matchers support:
207
+
208
+ - exact matches like `"/health"`
209
+ - prefix-style wildcards like `"/api/*"`
210
+ - regex matchers like `/^\/v[0-9]+\//`
211
+ - custom functions
212
+
213
+ If `includeRoutes` is provided, only matching requests are tracked. You can still use `ignoredRoutes` to exclude specific subsets from a broader include rule.
214
+
215
+ Example:
216
+
217
+ ```js
218
+ const traffic = createTrafficTracker({
219
+ sequelize,
220
+ includeRoutes: ["/api/*"],
221
+ ignoredRoutes: ["/api/internal/*"],
222
+ });
223
+ ```
224
+
225
+ You can also use one or more of these mounting patterns.
226
+
227
+ Mount the tracker only on the route prefix you care about:
228
+
229
+ ```js
230
+ app.use("/api", traffic.middleware);
93
231
  ```
94
232
 
233
+ Register static assets or health checks before the tracker:
234
+
235
+ ```js
236
+ app.use("/public", express.static("public"));
237
+ app.get("/health", (_req, res) => res.json({ ok: true }));
238
+
239
+ app.use(traffic.middleware);
240
+ ```
241
+
242
+ Ignore route groups with regex:
243
+
244
+ ```js
245
+ const traffic = createTrafficTracker({
246
+ sequelize,
247
+ ignoredRoutes: [
248
+ "/health",
249
+ "/favicon.ico",
250
+ /^\/traffic-dashboard(\/|$)/,
251
+ /^\/public(\/|$)/,
252
+ ],
253
+ });
254
+ ```
255
+
256
+ Use a function when the skip logic depends on route naming conventions:
257
+
258
+ ```js
259
+ const traffic = createTrafficTracker({
260
+ sequelize,
261
+ ignoredRoutes: [
262
+ (route) => route.startsWith("/internal/"),
263
+ ],
264
+ });
265
+ ```
266
+
267
+ If you mount the tracker globally, global tracking is expected behavior.
268
+
95
269
  ## Dashboard Usage
96
270
 
97
271
  The dashboard is optional. Set `dashboard.enabled` to `true`, build the frontend assets, attach realtime support, and mount the provided router.
@@ -110,6 +284,7 @@ npm run build:dashboard
110
284
  | `getUserId` | `(req) => string \| null` | `undefined` | Custom resolver for `userId` |
111
285
  | `getSessionId` | `(req) => string \| null` | `undefined` | Custom resolver for `sessionId` |
112
286
  | `slowRouteThresholdMs` | `number` | `1000` | Marks requests as slow when duration meets or exceeds this value |
287
+ | `includeRoutes` | `Array<string \| RegExp \| Function>` | `[]` | If set, only matching routes or URLs are tracked |
113
288
  | `ignoredRoutes` | `Array<string \| RegExp \| Function>` | `[]` | Routes or URLs to skip |
114
289
  | `trackIp` | `boolean` | `false` | Persist `req.ip` when enabled |
115
290
  | `trackUserAgent` | `boolean` | `true` | Persist the request `user-agent` header when enabled |
package/dist/index.cjs CHANGED
@@ -147,7 +147,7 @@ function resolveTrackedRoute(req) {
147
147
  }
148
148
  return routePath || req.path || req.originalUrl || "/";
149
149
  }
150
- function matchesIgnoredRoute(matcher, candidate) {
150
+ function matchesRouteMatcher(matcher, candidate) {
151
151
  if (!candidate) {
152
152
  return false;
153
153
  }
@@ -158,18 +158,32 @@ function matchesIgnoredRoute(matcher, candidate) {
158
158
  return Boolean(matcher(candidate));
159
159
  }
160
160
  if (typeof matcher === "string") {
161
+ if (matcher.endsWith("/*")) {
162
+ const prefix = matcher.slice(0, -2);
163
+ if (!prefix || prefix === "/") {
164
+ return true;
165
+ }
166
+ return candidate === prefix || candidate.startsWith(`${prefix}/`) || candidate.startsWith(`${prefix}?`);
167
+ }
161
168
  return candidate === matcher || candidate.startsWith(`${matcher}?`);
162
169
  }
163
170
  return false;
164
171
  }
165
- function shouldIgnoreRoute({
172
+ function matchesRouteList(matchers, route, originalUrl) {
173
+ return matchers.some(
174
+ (matcher) => matchesRouteMatcher(matcher, route) || matchesRouteMatcher(matcher, originalUrl)
175
+ );
176
+ }
177
+ function shouldTrackRoute({
166
178
  route,
167
179
  originalUrl,
180
+ includeRoutes = [],
168
181
  ignoredRoutes = []
169
182
  }) {
170
- return ignoredRoutes.some(
171
- (matcher) => matchesIgnoredRoute(matcher, route) || matchesIgnoredRoute(matcher, originalUrl)
172
- );
183
+ if (includeRoutes.length > 0 && !matchesRouteList(includeRoutes, route, originalUrl)) {
184
+ return false;
185
+ }
186
+ return !matchesRouteList(ignoredRoutes, route, originalUrl);
173
187
  }
174
188
 
175
189
  // src/middleware.js
@@ -223,6 +237,7 @@ function createTrackingMiddleware({
223
237
  getUserId,
224
238
  getSessionId,
225
239
  slowRouteThresholdMs = 1e3,
240
+ includeRoutes = [],
226
241
  ignoredRoutes = [],
227
242
  trackIp = false,
228
243
  trackUserAgent = true,
@@ -235,7 +250,12 @@ function createTrackingMiddleware({
235
250
  const endedAt = /* @__PURE__ */ new Date();
236
251
  const route = resolveTrackedRoute(req);
237
252
  const originalUrl = req.originalUrl || route;
238
- if (shouldIgnoreRoute({ route, originalUrl, ignoredRoutes })) {
253
+ if (!shouldTrackRoute({
254
+ route,
255
+ originalUrl,
256
+ includeRoutes,
257
+ ignoredRoutes
258
+ })) {
239
259
  return;
240
260
  }
241
261
  void safeAsync(
@@ -828,6 +848,7 @@ function createTrafficTracker(options = {}) {
828
848
  getUserId,
829
849
  getSessionId,
830
850
  slowRouteThresholdMs = 1e3,
851
+ includeRoutes = [],
831
852
  ignoredRoutes = [],
832
853
  trackIp = false,
833
854
  trackUserAgent = true,
@@ -848,6 +869,7 @@ function createTrafficTracker(options = {}) {
848
869
  getUserId,
849
870
  getSessionId,
850
871
  slowRouteThresholdMs,
872
+ includeRoutes,
851
873
  ignoredRoutes,
852
874
  trackIp,
853
875
  trackUserAgent,
package/dist/index.js CHANGED
@@ -112,7 +112,7 @@ function resolveTrackedRoute(req) {
112
112
  }
113
113
  return routePath || req.path || req.originalUrl || "/";
114
114
  }
115
- function matchesIgnoredRoute(matcher, candidate) {
115
+ function matchesRouteMatcher(matcher, candidate) {
116
116
  if (!candidate) {
117
117
  return false;
118
118
  }
@@ -123,18 +123,32 @@ function matchesIgnoredRoute(matcher, candidate) {
123
123
  return Boolean(matcher(candidate));
124
124
  }
125
125
  if (typeof matcher === "string") {
126
+ if (matcher.endsWith("/*")) {
127
+ const prefix = matcher.slice(0, -2);
128
+ if (!prefix || prefix === "/") {
129
+ return true;
130
+ }
131
+ return candidate === prefix || candidate.startsWith(`${prefix}/`) || candidate.startsWith(`${prefix}?`);
132
+ }
126
133
  return candidate === matcher || candidate.startsWith(`${matcher}?`);
127
134
  }
128
135
  return false;
129
136
  }
130
- function shouldIgnoreRoute({
137
+ function matchesRouteList(matchers, route, originalUrl) {
138
+ return matchers.some(
139
+ (matcher) => matchesRouteMatcher(matcher, route) || matchesRouteMatcher(matcher, originalUrl)
140
+ );
141
+ }
142
+ function shouldTrackRoute({
131
143
  route,
132
144
  originalUrl,
145
+ includeRoutes = [],
133
146
  ignoredRoutes = []
134
147
  }) {
135
- return ignoredRoutes.some(
136
- (matcher) => matchesIgnoredRoute(matcher, route) || matchesIgnoredRoute(matcher, originalUrl)
137
- );
148
+ if (includeRoutes.length > 0 && !matchesRouteList(includeRoutes, route, originalUrl)) {
149
+ return false;
150
+ }
151
+ return !matchesRouteList(ignoredRoutes, route, originalUrl);
138
152
  }
139
153
 
140
154
  // src/middleware.js
@@ -188,6 +202,7 @@ function createTrackingMiddleware({
188
202
  getUserId,
189
203
  getSessionId,
190
204
  slowRouteThresholdMs = 1e3,
205
+ includeRoutes = [],
191
206
  ignoredRoutes = [],
192
207
  trackIp = false,
193
208
  trackUserAgent = true,
@@ -200,7 +215,12 @@ function createTrackingMiddleware({
200
215
  const endedAt = /* @__PURE__ */ new Date();
201
216
  const route = resolveTrackedRoute(req);
202
217
  const originalUrl = req.originalUrl || route;
203
- if (shouldIgnoreRoute({ route, originalUrl, ignoredRoutes })) {
218
+ if (!shouldTrackRoute({
219
+ route,
220
+ originalUrl,
221
+ includeRoutes,
222
+ ignoredRoutes
223
+ })) {
204
224
  return;
205
225
  }
206
226
  void safeAsync(
@@ -793,6 +813,7 @@ function createTrafficTracker(options = {}) {
793
813
  getUserId,
794
814
  getSessionId,
795
815
  slowRouteThresholdMs = 1e3,
816
+ includeRoutes = [],
796
817
  ignoredRoutes = [],
797
818
  trackIp = false,
798
819
  trackUserAgent = true,
@@ -813,6 +834,7 @@ function createTrafficTracker(options = {}) {
813
834
  getUserId,
814
835
  getSessionId,
815
836
  slowRouteThresholdMs,
837
+ includeRoutes,
816
838
  ignoredRoutes,
817
839
  trackIp,
818
840
  trackUserAgent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-sequelize-traffic",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Privacy-first, self-hosted Express request analytics with Sequelize storage and an optional realtime dashboard.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",