express-sequelize-traffic 0.1.0 → 0.3.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/dist/index.cjs ADDED
@@ -0,0 +1,880 @@
1
+ const __PACKAGE_DIR__ = __dirname;
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.js
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ createTrafficTracker: () => createTrafficTracker,
34
+ defineTrafficLogModel: () => defineTrafficLogModel
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/models/TrafficLog.js
39
+ var import_sequelize = require("sequelize");
40
+ function defineTrafficLogModel(sequelize) {
41
+ if (!sequelize) {
42
+ throw new Error("A Sequelize instance is required to define TrafficLog.");
43
+ }
44
+ if (sequelize.models.TrafficLog) {
45
+ return sequelize.models.TrafficLog;
46
+ }
47
+ return sequelize.define(
48
+ "TrafficLog",
49
+ {
50
+ id: {
51
+ type: import_sequelize.DataTypes.BIGINT,
52
+ autoIncrement: true,
53
+ primaryKey: true
54
+ },
55
+ userId: {
56
+ type: import_sequelize.DataTypes.STRING,
57
+ allowNull: true
58
+ },
59
+ sessionId: {
60
+ type: import_sequelize.DataTypes.STRING,
61
+ allowNull: true
62
+ },
63
+ method: {
64
+ type: import_sequelize.DataTypes.STRING(16),
65
+ allowNull: false
66
+ },
67
+ route: {
68
+ type: import_sequelize.DataTypes.STRING,
69
+ allowNull: false
70
+ },
71
+ originalUrl: {
72
+ type: import_sequelize.DataTypes.STRING,
73
+ allowNull: false
74
+ },
75
+ statusCode: {
76
+ type: import_sequelize.DataTypes.INTEGER,
77
+ allowNull: false
78
+ },
79
+ durationMs: {
80
+ type: import_sequelize.DataTypes.INTEGER,
81
+ allowNull: false
82
+ },
83
+ isSlow: {
84
+ type: import_sequelize.DataTypes.BOOLEAN,
85
+ allowNull: false,
86
+ defaultValue: false
87
+ },
88
+ ip: {
89
+ type: import_sequelize.DataTypes.STRING,
90
+ allowNull: true
91
+ },
92
+ userAgent: {
93
+ type: import_sequelize.DataTypes.TEXT,
94
+ allowNull: true
95
+ },
96
+ startedAt: {
97
+ type: import_sequelize.DataTypes.DATE,
98
+ allowNull: false
99
+ },
100
+ endedAt: {
101
+ type: import_sequelize.DataTypes.DATE,
102
+ allowNull: false
103
+ }
104
+ },
105
+ {
106
+ tableName: "traffic_logs",
107
+ indexes: [
108
+ { fields: ["userId"] },
109
+ { fields: ["sessionId"] },
110
+ { fields: ["route"] },
111
+ { fields: ["method"] },
112
+ { fields: ["statusCode"] },
113
+ { fields: ["durationMs"] },
114
+ { fields: ["createdAt"] }
115
+ ]
116
+ }
117
+ );
118
+ }
119
+
120
+ // src/utils/safeAsync.js
121
+ async function safeAsync(task, options = {}) {
122
+ const { fallback = null, onError } = options;
123
+ try {
124
+ return await task();
125
+ } catch (error) {
126
+ if (typeof onError === "function") {
127
+ onError(error);
128
+ }
129
+ return fallback;
130
+ }
131
+ }
132
+
133
+ // src/utils/routeMatcher.js
134
+ function normalizeRouteCandidate(route) {
135
+ if (Array.isArray(route)) {
136
+ return route[0] || "/";
137
+ }
138
+ if (route instanceof RegExp) {
139
+ return route.toString();
140
+ }
141
+ return route || null;
142
+ }
143
+ function resolveTrackedRoute(req) {
144
+ const routePath = normalizeRouteCandidate(req.route?.path);
145
+ if (req.baseUrl && routePath) {
146
+ return routePath === "/" ? req.baseUrl || "/" : `${req.baseUrl}${routePath}`;
147
+ }
148
+ return routePath || req.path || req.originalUrl || "/";
149
+ }
150
+ function matchesIgnoredRoute(matcher, candidate) {
151
+ if (!candidate) {
152
+ return false;
153
+ }
154
+ if (matcher instanceof RegExp) {
155
+ return matcher.test(candidate);
156
+ }
157
+ if (typeof matcher === "function") {
158
+ return Boolean(matcher(candidate));
159
+ }
160
+ if (typeof matcher === "string") {
161
+ return candidate === matcher || candidate.startsWith(`${matcher}?`);
162
+ }
163
+ return false;
164
+ }
165
+ function shouldIgnoreRoute({
166
+ route,
167
+ originalUrl,
168
+ ignoredRoutes = []
169
+ }) {
170
+ return ignoredRoutes.some(
171
+ (matcher) => matchesIgnoredRoute(matcher, route) || matchesIgnoredRoute(matcher, originalUrl)
172
+ );
173
+ }
174
+
175
+ // src/middleware.js
176
+ function resolveOptionalValue(getter, req, debug, label) {
177
+ if (typeof getter !== "function") {
178
+ return null;
179
+ }
180
+ try {
181
+ return getter(req) ?? null;
182
+ } catch (error) {
183
+ if (debug) {
184
+ console.error(
185
+ `[express-sequelize-traffic] Failed to resolve ${label}.`,
186
+ error
187
+ );
188
+ }
189
+ return null;
190
+ }
191
+ }
192
+ function createLogPayload({
193
+ req,
194
+ res,
195
+ startedAt,
196
+ endedAt,
197
+ slowRouteThresholdMs,
198
+ trackIp,
199
+ trackUserAgent,
200
+ getUserId,
201
+ getSessionId,
202
+ debug
203
+ }) {
204
+ const route = resolveTrackedRoute(req);
205
+ const durationMs = Math.max(0, endedAt.getTime() - startedAt.getTime());
206
+ return {
207
+ userId: resolveOptionalValue(getUserId, req, debug, "userId"),
208
+ sessionId: resolveOptionalValue(getSessionId, req, debug, "sessionId"),
209
+ method: req.method,
210
+ route,
211
+ originalUrl: req.originalUrl || route,
212
+ statusCode: res.statusCode,
213
+ durationMs,
214
+ isSlow: durationMs >= slowRouteThresholdMs,
215
+ ip: trackIp ? req.ip || req.socket?.remoteAddress || null : null,
216
+ userAgent: trackUserAgent ? req.get("user-agent") || null : null,
217
+ startedAt,
218
+ endedAt
219
+ };
220
+ }
221
+ function createTrackingMiddleware({
222
+ TrafficLog,
223
+ getUserId,
224
+ getSessionId,
225
+ slowRouteThresholdMs = 1e3,
226
+ ignoredRoutes = [],
227
+ trackIp = false,
228
+ trackUserAgent = true,
229
+ debug = false,
230
+ realtimeBridge
231
+ }) {
232
+ return (req, res, next) => {
233
+ const startedAt = /* @__PURE__ */ new Date();
234
+ res.once("finish", () => {
235
+ const endedAt = /* @__PURE__ */ new Date();
236
+ const route = resolveTrackedRoute(req);
237
+ const originalUrl = req.originalUrl || route;
238
+ if (shouldIgnoreRoute({ route, originalUrl, ignoredRoutes })) {
239
+ return;
240
+ }
241
+ void safeAsync(
242
+ async () => {
243
+ const payload = createLogPayload({
244
+ req,
245
+ res,
246
+ startedAt,
247
+ endedAt,
248
+ slowRouteThresholdMs,
249
+ trackIp,
250
+ trackUserAgent,
251
+ getUserId,
252
+ getSessionId,
253
+ debug
254
+ });
255
+ const createdLog = await TrafficLog.create(payload);
256
+ realtimeBridge?.emitNewRequest(createdLog.get({ plain: true }));
257
+ },
258
+ {
259
+ onError: (error) => {
260
+ if (debug) {
261
+ console.error(
262
+ "[express-sequelize-traffic] Failed to persist traffic log.",
263
+ error
264
+ );
265
+ }
266
+ }
267
+ }
268
+ );
269
+ });
270
+ next();
271
+ };
272
+ }
273
+
274
+ // src/services/analyticsService.js
275
+ var import_sequelize2 = require("sequelize");
276
+ function toNumber(value, decimals = 2) {
277
+ const parsed = Number(value ?? 0);
278
+ if (!Number.isFinite(parsed)) {
279
+ return 0;
280
+ }
281
+ return Number(parsed.toFixed(decimals));
282
+ }
283
+ function normalizeLog(log) {
284
+ return {
285
+ id: log.id,
286
+ userId: log.userId,
287
+ sessionId: log.sessionId,
288
+ method: log.method,
289
+ route: log.route,
290
+ originalUrl: log.originalUrl,
291
+ statusCode: Number(log.statusCode),
292
+ durationMs: Number(log.durationMs),
293
+ isSlow: Boolean(log.isSlow),
294
+ ip: log.ip,
295
+ userAgent: log.userAgent,
296
+ startedAt: log.startedAt,
297
+ endedAt: log.endedAt,
298
+ createdAt: log.createdAt,
299
+ updatedAt: log.updatedAt
300
+ };
301
+ }
302
+ function buildTimeline(logs = []) {
303
+ const buckets = /* @__PURE__ */ new Map();
304
+ for (const log of logs) {
305
+ const bucketDate = new Date(log.createdAt);
306
+ bucketDate.setMinutes(0, 0, 0);
307
+ const bucketKey = bucketDate.toISOString();
308
+ buckets.set(bucketKey, (buckets.get(bucketKey) || 0) + 1);
309
+ }
310
+ return [...buckets.entries()].sort(([left], [right]) => left.localeCompare(right)).slice(-24).map(([timestamp, totalRequests]) => ({
311
+ timestamp,
312
+ totalRequests
313
+ }));
314
+ }
315
+ function buildGroupKey(row) {
316
+ return `${row.route}::${row.method}`;
317
+ }
318
+ function mapGroupedCounts(rows = []) {
319
+ const counts = /* @__PURE__ */ new Map();
320
+ for (const row of rows) {
321
+ counts.set(buildGroupKey(row), Number(row.totalCount || 0));
322
+ }
323
+ return counts;
324
+ }
325
+ function createAnalyticsService(TrafficLog) {
326
+ return {
327
+ async getOverview() {
328
+ const [
329
+ totalRequests,
330
+ averageDurationMsRaw,
331
+ slowRequestCount,
332
+ errorRequestCount,
333
+ uniqueUsers,
334
+ topRoutesRows,
335
+ slowestRoutesRows,
336
+ statusCodeRows,
337
+ latestRequests,
338
+ timelineLogs
339
+ ] = await Promise.all([
340
+ TrafficLog.count(),
341
+ TrafficLog.aggregate("durationMs", "avg"),
342
+ TrafficLog.count({ where: { isSlow: true } }),
343
+ TrafficLog.count({
344
+ where: {
345
+ statusCode: { [import_sequelize2.Op.gte]: 400 }
346
+ }
347
+ }),
348
+ TrafficLog.count({
349
+ distinct: true,
350
+ col: "userId",
351
+ where: {
352
+ userId: { [import_sequelize2.Op.not]: null }
353
+ }
354
+ }),
355
+ TrafficLog.findAll({
356
+ attributes: [
357
+ "route",
358
+ [(0, import_sequelize2.fn)("COUNT", (0, import_sequelize2.col)("id")), "totalRequests"]
359
+ ],
360
+ where: {
361
+ route: { [import_sequelize2.Op.not]: null }
362
+ },
363
+ group: ["route"],
364
+ order: [[(0, import_sequelize2.literal)("totalRequests"), "DESC"]],
365
+ limit: 5,
366
+ raw: true
367
+ }),
368
+ TrafficLog.findAll({
369
+ attributes: [
370
+ "route",
371
+ "method",
372
+ [(0, import_sequelize2.fn)("AVG", (0, import_sequelize2.col)("durationMs")), "averageDurationMs"],
373
+ [(0, import_sequelize2.fn)("MAX", (0, import_sequelize2.col)("durationMs")), "maxDurationMs"],
374
+ [(0, import_sequelize2.fn)("COUNT", (0, import_sequelize2.col)("id")), "totalRequests"]
375
+ ],
376
+ where: {
377
+ route: { [import_sequelize2.Op.not]: null }
378
+ },
379
+ group: ["route", "method"],
380
+ order: [[(0, import_sequelize2.literal)("averageDurationMs"), "DESC"]],
381
+ limit: 5,
382
+ raw: true
383
+ }),
384
+ TrafficLog.findAll({
385
+ attributes: [
386
+ "statusCode",
387
+ [(0, import_sequelize2.fn)("COUNT", (0, import_sequelize2.col)("id")), "totalRequests"]
388
+ ],
389
+ group: ["statusCode"],
390
+ order: [["statusCode", "ASC"]],
391
+ raw: true
392
+ }),
393
+ TrafficLog.findAll({
394
+ order: [["createdAt", "DESC"]],
395
+ limit: 10,
396
+ raw: true
397
+ }),
398
+ TrafficLog.findAll({
399
+ attributes: ["createdAt"],
400
+ order: [["createdAt", "DESC"]],
401
+ limit: 1e3,
402
+ raw: true
403
+ })
404
+ ]);
405
+ return {
406
+ totalRequests,
407
+ averageDurationMs: toNumber(averageDurationMsRaw),
408
+ slowRequestCount,
409
+ errorRequestCount,
410
+ uniqueUsers,
411
+ topRoutes: topRoutesRows.map((row) => ({
412
+ route: row.route,
413
+ totalRequests: Number(row.totalRequests)
414
+ })),
415
+ slowestRoutes: slowestRoutesRows.map((row) => ({
416
+ route: row.route,
417
+ method: row.method,
418
+ totalRequests: Number(row.totalRequests),
419
+ averageDurationMs: toNumber(row.averageDurationMs),
420
+ maxDurationMs: Number(row.maxDurationMs)
421
+ })),
422
+ statusCodeSummary: statusCodeRows.map((row) => ({
423
+ statusCode: Number(row.statusCode),
424
+ totalRequests: Number(row.totalRequests)
425
+ })),
426
+ latestRequests: latestRequests.map(normalizeLog),
427
+ requestsTimeline: buildTimeline(timelineLogs)
428
+ };
429
+ },
430
+ async getLive() {
431
+ const logs = await TrafficLog.findAll({
432
+ order: [["createdAt", "DESC"]],
433
+ limit: 100,
434
+ raw: true
435
+ });
436
+ return logs.map(normalizeLog);
437
+ },
438
+ async getRoutes() {
439
+ const [baseRows, errorRows, slowRows] = await Promise.all([
440
+ TrafficLog.findAll({
441
+ attributes: [
442
+ "route",
443
+ "method",
444
+ [(0, import_sequelize2.fn)("COUNT", (0, import_sequelize2.col)("id")), "totalRequests"],
445
+ [(0, import_sequelize2.fn)("AVG", (0, import_sequelize2.col)("durationMs")), "averageDurationMs"],
446
+ [(0, import_sequelize2.fn)("MAX", (0, import_sequelize2.col)("durationMs")), "maxDurationMs"]
447
+ ],
448
+ where: {
449
+ route: { [import_sequelize2.Op.not]: null }
450
+ },
451
+ group: ["route", "method"],
452
+ order: [[(0, import_sequelize2.literal)("averageDurationMs"), "DESC"]],
453
+ raw: true
454
+ }),
455
+ TrafficLog.findAll({
456
+ attributes: [
457
+ "route",
458
+ "method",
459
+ [(0, import_sequelize2.fn)("COUNT", (0, import_sequelize2.col)("id")), "totalCount"]
460
+ ],
461
+ where: {
462
+ route: { [import_sequelize2.Op.not]: null },
463
+ statusCode: { [import_sequelize2.Op.gte]: 400 }
464
+ },
465
+ group: ["route", "method"],
466
+ raw: true
467
+ }),
468
+ TrafficLog.findAll({
469
+ attributes: [
470
+ "route",
471
+ "method",
472
+ [(0, import_sequelize2.fn)("COUNT", (0, import_sequelize2.col)("id")), "totalCount"]
473
+ ],
474
+ where: {
475
+ route: { [import_sequelize2.Op.not]: null },
476
+ isSlow: true
477
+ },
478
+ group: ["route", "method"],
479
+ raw: true
480
+ })
481
+ ]);
482
+ const errorCounts = mapGroupedCounts(errorRows);
483
+ const slowCounts = mapGroupedCounts(slowRows);
484
+ return baseRows.map((row) => {
485
+ const key = buildGroupKey(row);
486
+ return {
487
+ route: row.route,
488
+ method: row.method,
489
+ totalRequests: Number(row.totalRequests),
490
+ averageDurationMs: toNumber(row.averageDurationMs),
491
+ maxDurationMs: Number(row.maxDurationMs),
492
+ errorCount: errorCounts.get(key) || 0,
493
+ slowCount: slowCounts.get(key) || 0
494
+ };
495
+ });
496
+ },
497
+ async getUsers() {
498
+ const rows = await TrafficLog.findAll({
499
+ attributes: [
500
+ "userId",
501
+ [(0, import_sequelize2.fn)("COUNT", (0, import_sequelize2.col)("id")), "totalRequests"],
502
+ [(0, import_sequelize2.fn)("MAX", (0, import_sequelize2.col)("createdAt")), "lastSeenAt"],
503
+ [(0, import_sequelize2.fn)("AVG", (0, import_sequelize2.col)("durationMs")), "averageDurationMs"]
504
+ ],
505
+ where: {
506
+ userId: { [import_sequelize2.Op.not]: null }
507
+ },
508
+ group: ["userId"],
509
+ order: [[(0, import_sequelize2.literal)("totalRequests"), "DESC"]],
510
+ raw: true
511
+ });
512
+ return rows.map((row) => ({
513
+ userId: row.userId,
514
+ totalRequests: Number(row.totalRequests),
515
+ lastSeenAt: row.lastSeenAt,
516
+ averageDurationMs: toNumber(row.averageDurationMs)
517
+ }));
518
+ },
519
+ async getErrors() {
520
+ const rows = await TrafficLog.findAll({
521
+ where: {
522
+ statusCode: { [import_sequelize2.Op.gte]: 400 }
523
+ },
524
+ order: [["createdAt", "DESC"]],
525
+ limit: 100,
526
+ raw: true
527
+ });
528
+ return rows.map(normalizeLog);
529
+ }
530
+ };
531
+ }
532
+
533
+ // src/dashboard.js
534
+ var import_node_fs = __toESM(require("node:fs"), 1);
535
+ var import_node_path = __toESM(require("node:path"), 1);
536
+ var import_express2 = __toESM(require("express"), 1);
537
+
538
+ // src/routes/analyticsRoutes.js
539
+ var import_express = __toESM(require("express"), 1);
540
+ function sendApiError(res, message) {
541
+ res.status(500).json({ error: message });
542
+ }
543
+ function createAnalyticsRouter({ analyticsService, debug = false }) {
544
+ const router = import_express.default.Router();
545
+ const logApiError = (scope) => (error) => {
546
+ if (debug) {
547
+ console.error(`[express-sequelize-traffic] ${scope}`, error);
548
+ }
549
+ };
550
+ router.get("/overview", async (_req, res) => {
551
+ const data = await safeAsync(() => analyticsService.getOverview(), {
552
+ onError: logApiError("Failed to load overview analytics.")
553
+ });
554
+ if (!data) {
555
+ sendApiError(res, "Unable to load overview analytics.");
556
+ return;
557
+ }
558
+ res.json(data);
559
+ });
560
+ router.get("/live", async (_req, res) => {
561
+ const data = await safeAsync(() => analyticsService.getLive(), {
562
+ onError: logApiError("Failed to load live traffic logs.")
563
+ });
564
+ if (!data) {
565
+ sendApiError(res, "Unable to load live traffic logs.");
566
+ return;
567
+ }
568
+ res.json(data);
569
+ });
570
+ router.get("/routes", async (_req, res) => {
571
+ const data = await safeAsync(() => analyticsService.getRoutes(), {
572
+ onError: logApiError("Failed to load route analytics.")
573
+ });
574
+ if (!data) {
575
+ sendApiError(res, "Unable to load route analytics.");
576
+ return;
577
+ }
578
+ res.json(data);
579
+ });
580
+ router.get("/users", async (_req, res) => {
581
+ const data = await safeAsync(() => analyticsService.getUsers(), {
582
+ onError: logApiError("Failed to load user analytics.")
583
+ });
584
+ if (!data) {
585
+ sendApiError(res, "Unable to load user analytics.");
586
+ return;
587
+ }
588
+ res.json(data);
589
+ });
590
+ router.get("/errors", async (_req, res) => {
591
+ const data = await safeAsync(() => analyticsService.getErrors(), {
592
+ onError: logApiError("Failed to load error analytics.")
593
+ });
594
+ if (!data) {
595
+ sendApiError(res, "Unable to load error analytics.");
596
+ return;
597
+ }
598
+ res.json(data);
599
+ });
600
+ return router;
601
+ }
602
+
603
+ // src/utils/dashboardAuth.js
604
+ function parseBasicAuthHeader(headerValue) {
605
+ if (!headerValue || !headerValue.startsWith("Basic ")) {
606
+ return null;
607
+ }
608
+ try {
609
+ const decoded = Buffer.from(headerValue.slice(6), "base64").toString(
610
+ "utf8"
611
+ );
612
+ const separatorIndex = decoded.indexOf(":");
613
+ if (separatorIndex === -1) {
614
+ return null;
615
+ }
616
+ return {
617
+ username: decoded.slice(0, separatorIndex),
618
+ password: decoded.slice(separatorIndex + 1)
619
+ };
620
+ } catch (_error) {
621
+ return null;
622
+ }
623
+ }
624
+ function isDashboardAuthEnabled(dashboard = {}) {
625
+ return Boolean(dashboard.enabled && dashboard.username && dashboard.password);
626
+ }
627
+ function createBasicAuthMiddleware(dashboard = {}) {
628
+ if (!isDashboardAuthEnabled(dashboard)) {
629
+ return (_req, _res, next) => next();
630
+ }
631
+ return (req, res, next) => {
632
+ const credentials = parseBasicAuthHeader(req.headers.authorization);
633
+ if (credentials?.username === dashboard.username && credentials?.password === dashboard.password) {
634
+ next();
635
+ return;
636
+ }
637
+ res.setHeader("WWW-Authenticate", 'Basic realm="Traffic Dashboard"');
638
+ res.status(401).json({ error: "Dashboard authentication required." });
639
+ };
640
+ }
641
+ function createSocketAuthMiddleware(dashboard = {}) {
642
+ return (socket, next) => {
643
+ if (!isDashboardAuthEnabled(dashboard)) {
644
+ next();
645
+ return;
646
+ }
647
+ const credentials = parseBasicAuthHeader(
648
+ socket.handshake.headers.authorization
649
+ );
650
+ if (credentials?.username === dashboard.username && credentials?.password === dashboard.password) {
651
+ next();
652
+ return;
653
+ }
654
+ next(new Error("Dashboard authentication required."));
655
+ };
656
+ }
657
+
658
+ // src/dashboard.js
659
+ var dashboardBuildDirectory = import_node_path.default.resolve(
660
+ __PACKAGE_DIR__,
661
+ "dashboard-public"
662
+ );
663
+ var dashboardIndexFile = import_node_path.default.join(dashboardBuildDirectory, "index.html");
664
+ function createMissingBuildPage() {
665
+ return `<!doctype html>
666
+ <html lang="en">
667
+ <head>
668
+ <meta charset="utf-8" />
669
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
670
+ <title>Traffic Dashboard Build Missing</title>
671
+ <style>
672
+ body {
673
+ margin: 0;
674
+ font-family: "Segoe UI", sans-serif;
675
+ background: linear-gradient(135deg, #0f172a, #1e293b);
676
+ color: #e2e8f0;
677
+ min-height: 100vh;
678
+ display: grid;
679
+ place-items: center;
680
+ padding: 24px;
681
+ }
682
+
683
+ .panel {
684
+ max-width: 640px;
685
+ background: rgba(15, 23, 42, 0.9);
686
+ border: 1px solid rgba(148, 163, 184, 0.2);
687
+ border-radius: 24px;
688
+ padding: 32px;
689
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.45);
690
+ }
691
+
692
+ h1 {
693
+ margin-top: 0;
694
+ font-size: 2rem;
695
+ }
696
+
697
+ code {
698
+ background: rgba(148, 163, 184, 0.16);
699
+ padding: 2px 6px;
700
+ border-radius: 6px;
701
+ }
702
+ </style>
703
+ </head>
704
+ <body>
705
+ <div class="panel">
706
+ <h1>Dashboard build not found</h1>
707
+ <p>
708
+ The analytics APIs are available, but the Vite dashboard has not been built
709
+ into <code>dist/dashboard-public</code> yet.
710
+ </p>
711
+ <p>Run <code>npm run build:dashboard</code> in this package to generate the static dashboard assets.</p>
712
+ </div>
713
+ </body>
714
+ </html>`;
715
+ }
716
+ function createDisabledDashboardRouter() {
717
+ const router = import_express2.default.Router();
718
+ router.use("/api", (_req, res) => {
719
+ res.status(404).json({ error: "Dashboard is disabled." });
720
+ });
721
+ router.use((_req, res) => {
722
+ res.status(404).send("The traffic dashboard is disabled for this tracker instance.");
723
+ });
724
+ return router;
725
+ }
726
+ function createDashboardRouter({
727
+ analyticsService,
728
+ dashboard = {},
729
+ debug = false
730
+ }) {
731
+ const router = import_express2.default.Router();
732
+ router.use(createBasicAuthMiddleware(dashboard));
733
+ router.use("/api", createAnalyticsRouter({ analyticsService, debug }));
734
+ const hasBuiltDashboard = import_node_fs.default.existsSync(dashboardIndexFile);
735
+ if (hasBuiltDashboard) {
736
+ router.get("/", (req, res, next) => {
737
+ if (!req.originalUrl.endsWith("/")) {
738
+ res.redirect(302, `${req.baseUrl}/`);
739
+ return;
740
+ }
741
+ next();
742
+ });
743
+ router.use(
744
+ import_express2.default.static(dashboardBuildDirectory, {
745
+ index: false
746
+ })
747
+ );
748
+ router.get("*", (req, res, next) => {
749
+ if (req.path.startsWith("/api")) {
750
+ next();
751
+ return;
752
+ }
753
+ res.sendFile(dashboardIndexFile);
754
+ });
755
+ return router;
756
+ }
757
+ router.get("*", (req, res, next) => {
758
+ if (req.path.startsWith("/api")) {
759
+ next();
760
+ return;
761
+ }
762
+ res.status(503).send(createMissingBuildPage());
763
+ });
764
+ return router;
765
+ }
766
+
767
+ // src/realtime.js
768
+ var import_socket = require("socket.io");
769
+ function normalizeSocketPath(dashboard = {}) {
770
+ const mountPath = dashboard.mountPath || "/traffic-dashboard";
771
+ const sanitizedMountPath = mountPath.startsWith("/") ? mountPath : `/${mountPath}`;
772
+ return `${sanitizedMountPath.replace(/\/$/, "")}/socket.io`;
773
+ }
774
+ function buildRealtimePayload(log) {
775
+ return {
776
+ userId: log.userId,
777
+ sessionId: log.sessionId,
778
+ method: log.method,
779
+ route: log.route,
780
+ originalUrl: log.originalUrl,
781
+ statusCode: Number(log.statusCode),
782
+ durationMs: Number(log.durationMs),
783
+ isSlow: Boolean(log.isSlow),
784
+ createdAt: log.createdAt
785
+ };
786
+ }
787
+ function createRealtimeBridge({ dashboard = {}, debug = false } = {}) {
788
+ let io = null;
789
+ const socketPath = normalizeSocketPath(dashboard);
790
+ return {
791
+ attachRealtime(server) {
792
+ if (!server) {
793
+ throw new Error("An HTTP server instance is required for realtime.");
794
+ }
795
+ if (io) {
796
+ return io;
797
+ }
798
+ io = new import_socket.Server(server, {
799
+ path: socketPath,
800
+ serveClient: false
801
+ });
802
+ io.use(createSocketAuthMiddleware(dashboard));
803
+ if (debug) {
804
+ io.on("connection", (socket) => {
805
+ console.info(
806
+ `[express-sequelize-traffic] Dashboard realtime connected: ${socket.id}`
807
+ );
808
+ });
809
+ }
810
+ return io;
811
+ },
812
+ emitNewRequest(log) {
813
+ if (!io) {
814
+ return;
815
+ }
816
+ io.emit("traffic:new-request", buildRealtimePayload(log));
817
+ },
818
+ getSocketPath() {
819
+ return socketPath;
820
+ }
821
+ };
822
+ }
823
+
824
+ // src/index.js
825
+ function createTrafficTracker(options = {}) {
826
+ const {
827
+ sequelize,
828
+ getUserId,
829
+ getSessionId,
830
+ slowRouteThresholdMs = 1e3,
831
+ ignoredRoutes = [],
832
+ trackIp = false,
833
+ trackUserAgent = true,
834
+ dashboard = {},
835
+ debug = false
836
+ } = options;
837
+ if (!sequelize) {
838
+ throw new Error(
839
+ "createTrafficTracker requires a Sequelize instance via options.sequelize."
840
+ );
841
+ }
842
+ const TrafficLog = defineTrafficLogModel(sequelize);
843
+ const analyticsService = createAnalyticsService(TrafficLog);
844
+ const realtimeBridge = createRealtimeBridge({ dashboard, debug });
845
+ return {
846
+ middleware: createTrackingMiddleware({
847
+ TrafficLog,
848
+ getUserId,
849
+ getSessionId,
850
+ slowRouteThresholdMs,
851
+ ignoredRoutes,
852
+ trackIp,
853
+ trackUserAgent,
854
+ debug,
855
+ realtimeBridge
856
+ }),
857
+ dashboard: dashboard.enabled ? createDashboardRouter({
858
+ analyticsService,
859
+ dashboard,
860
+ debug
861
+ }) : createDisabledDashboardRouter(),
862
+ attachRealtime(server) {
863
+ return realtimeBridge.attachRealtime(server);
864
+ },
865
+ async sync(syncOptions = {}) {
866
+ return TrafficLog.sync(syncOptions);
867
+ },
868
+ getModel() {
869
+ return TrafficLog;
870
+ },
871
+ getSocketPath() {
872
+ return realtimeBridge.getSocketPath();
873
+ }
874
+ };
875
+ }
876
+ // Annotate the CommonJS export names for ESM import in node:
877
+ 0 && (module.exports = {
878
+ createTrafficTracker,
879
+ defineTrafficLogModel
880
+ });