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