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/README.md +10 -1
- package/dist/index.cjs +880 -0
- package/dist/index.js +844 -0
- package/package.json +13 -6
- package/examples/express-app.js +0 -106
- package/src/dashboard.js +0 -132
- package/src/index.js +0 -67
- package/src/middleware.js +0 -111
- package/src/models/TrafficLog.js +0 -83
- package/src/realtime.js +0 -71
- package/src/routes/analyticsRoutes.js +0 -83
- package/src/services/analyticsService.js +0 -286
- package/src/utils/dashboardAuth.js +0 -71
- package/src/utils/routeMatcher.js +0 -53
- package/src/utils/safeAsync.js +0 -13
- /package/{src → dist}/dashboard-public/assets/index-CaWHQ-tp.js +0 -0
- /package/{src → dist}/dashboard-public/assets/index-iT93XJlh.css +0 -0
- /package/{src → dist}/dashboard-public/index.html +0 -0
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { Op, fn, col, literal } from "sequelize";
|
|
2
|
-
|
|
3
|
-
function toNumber(value, decimals = 2) {
|
|
4
|
-
const parsed = Number(value ?? 0);
|
|
5
|
-
|
|
6
|
-
if (!Number.isFinite(parsed)) {
|
|
7
|
-
return 0;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
return Number(parsed.toFixed(decimals));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function normalizeLog(log) {
|
|
14
|
-
return {
|
|
15
|
-
id: log.id,
|
|
16
|
-
userId: log.userId,
|
|
17
|
-
sessionId: log.sessionId,
|
|
18
|
-
method: log.method,
|
|
19
|
-
route: log.route,
|
|
20
|
-
originalUrl: log.originalUrl,
|
|
21
|
-
statusCode: Number(log.statusCode),
|
|
22
|
-
durationMs: Number(log.durationMs),
|
|
23
|
-
isSlow: Boolean(log.isSlow),
|
|
24
|
-
ip: log.ip,
|
|
25
|
-
userAgent: log.userAgent,
|
|
26
|
-
startedAt: log.startedAt,
|
|
27
|
-
endedAt: log.endedAt,
|
|
28
|
-
createdAt: log.createdAt,
|
|
29
|
-
updatedAt: log.updatedAt,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function buildTimeline(logs = []) {
|
|
34
|
-
const buckets = new Map();
|
|
35
|
-
|
|
36
|
-
for (const log of logs) {
|
|
37
|
-
const bucketDate = new Date(log.createdAt);
|
|
38
|
-
|
|
39
|
-
bucketDate.setMinutes(0, 0, 0);
|
|
40
|
-
|
|
41
|
-
const bucketKey = bucketDate.toISOString();
|
|
42
|
-
|
|
43
|
-
buckets.set(bucketKey, (buckets.get(bucketKey) || 0) + 1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return [...buckets.entries()]
|
|
47
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
48
|
-
.slice(-24)
|
|
49
|
-
.map(([timestamp, totalRequests]) => ({
|
|
50
|
-
timestamp,
|
|
51
|
-
totalRequests,
|
|
52
|
-
}));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function buildGroupKey(row) {
|
|
56
|
-
return `${row.route}::${row.method}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function mapGroupedCounts(rows = []) {
|
|
60
|
-
const counts = new Map();
|
|
61
|
-
|
|
62
|
-
for (const row of rows) {
|
|
63
|
-
counts.set(buildGroupKey(row), Number(row.totalCount || 0));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return counts;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function createAnalyticsService(TrafficLog) {
|
|
70
|
-
return {
|
|
71
|
-
async getOverview() {
|
|
72
|
-
const [
|
|
73
|
-
totalRequests,
|
|
74
|
-
averageDurationMsRaw,
|
|
75
|
-
slowRequestCount,
|
|
76
|
-
errorRequestCount,
|
|
77
|
-
uniqueUsers,
|
|
78
|
-
topRoutesRows,
|
|
79
|
-
slowestRoutesRows,
|
|
80
|
-
statusCodeRows,
|
|
81
|
-
latestRequests,
|
|
82
|
-
timelineLogs,
|
|
83
|
-
] = await Promise.all([
|
|
84
|
-
TrafficLog.count(),
|
|
85
|
-
TrafficLog.aggregate("durationMs", "avg"),
|
|
86
|
-
TrafficLog.count({ where: { isSlow: true } }),
|
|
87
|
-
TrafficLog.count({
|
|
88
|
-
where: {
|
|
89
|
-
statusCode: { [Op.gte]: 400 },
|
|
90
|
-
},
|
|
91
|
-
}),
|
|
92
|
-
TrafficLog.count({
|
|
93
|
-
distinct: true,
|
|
94
|
-
col: "userId",
|
|
95
|
-
where: {
|
|
96
|
-
userId: { [Op.not]: null },
|
|
97
|
-
},
|
|
98
|
-
}),
|
|
99
|
-
TrafficLog.findAll({
|
|
100
|
-
attributes: [
|
|
101
|
-
"route",
|
|
102
|
-
[fn("COUNT", col("id")), "totalRequests"],
|
|
103
|
-
],
|
|
104
|
-
where: {
|
|
105
|
-
route: { [Op.not]: null },
|
|
106
|
-
},
|
|
107
|
-
group: ["route"],
|
|
108
|
-
order: [[literal("totalRequests"), "DESC"]],
|
|
109
|
-
limit: 5,
|
|
110
|
-
raw: true,
|
|
111
|
-
}),
|
|
112
|
-
TrafficLog.findAll({
|
|
113
|
-
attributes: [
|
|
114
|
-
"route",
|
|
115
|
-
"method",
|
|
116
|
-
[fn("AVG", col("durationMs")), "averageDurationMs"],
|
|
117
|
-
[fn("MAX", col("durationMs")), "maxDurationMs"],
|
|
118
|
-
[fn("COUNT", col("id")), "totalRequests"],
|
|
119
|
-
],
|
|
120
|
-
where: {
|
|
121
|
-
route: { [Op.not]: null },
|
|
122
|
-
},
|
|
123
|
-
group: ["route", "method"],
|
|
124
|
-
order: [[literal("averageDurationMs"), "DESC"]],
|
|
125
|
-
limit: 5,
|
|
126
|
-
raw: true,
|
|
127
|
-
}),
|
|
128
|
-
TrafficLog.findAll({
|
|
129
|
-
attributes: [
|
|
130
|
-
"statusCode",
|
|
131
|
-
[fn("COUNT", col("id")), "totalRequests"],
|
|
132
|
-
],
|
|
133
|
-
group: ["statusCode"],
|
|
134
|
-
order: [["statusCode", "ASC"]],
|
|
135
|
-
raw: true,
|
|
136
|
-
}),
|
|
137
|
-
TrafficLog.findAll({
|
|
138
|
-
order: [["createdAt", "DESC"]],
|
|
139
|
-
limit: 10,
|
|
140
|
-
raw: true,
|
|
141
|
-
}),
|
|
142
|
-
TrafficLog.findAll({
|
|
143
|
-
attributes: ["createdAt"],
|
|
144
|
-
order: [["createdAt", "DESC"]],
|
|
145
|
-
limit: 1000,
|
|
146
|
-
raw: true,
|
|
147
|
-
}),
|
|
148
|
-
]);
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
totalRequests,
|
|
152
|
-
averageDurationMs: toNumber(averageDurationMsRaw),
|
|
153
|
-
slowRequestCount,
|
|
154
|
-
errorRequestCount,
|
|
155
|
-
uniqueUsers,
|
|
156
|
-
topRoutes: topRoutesRows.map((row) => ({
|
|
157
|
-
route: row.route,
|
|
158
|
-
totalRequests: Number(row.totalRequests),
|
|
159
|
-
})),
|
|
160
|
-
slowestRoutes: slowestRoutesRows.map((row) => ({
|
|
161
|
-
route: row.route,
|
|
162
|
-
method: row.method,
|
|
163
|
-
totalRequests: Number(row.totalRequests),
|
|
164
|
-
averageDurationMs: toNumber(row.averageDurationMs),
|
|
165
|
-
maxDurationMs: Number(row.maxDurationMs),
|
|
166
|
-
})),
|
|
167
|
-
statusCodeSummary: statusCodeRows.map((row) => ({
|
|
168
|
-
statusCode: Number(row.statusCode),
|
|
169
|
-
totalRequests: Number(row.totalRequests),
|
|
170
|
-
})),
|
|
171
|
-
latestRequests: latestRequests.map(normalizeLog),
|
|
172
|
-
requestsTimeline: buildTimeline(timelineLogs),
|
|
173
|
-
};
|
|
174
|
-
},
|
|
175
|
-
|
|
176
|
-
async getLive() {
|
|
177
|
-
const logs = await TrafficLog.findAll({
|
|
178
|
-
order: [["createdAt", "DESC"]],
|
|
179
|
-
limit: 100,
|
|
180
|
-
raw: true,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
return logs.map(normalizeLog);
|
|
184
|
-
},
|
|
185
|
-
|
|
186
|
-
async getRoutes() {
|
|
187
|
-
const [baseRows, errorRows, slowRows] = await Promise.all([
|
|
188
|
-
TrafficLog.findAll({
|
|
189
|
-
attributes: [
|
|
190
|
-
"route",
|
|
191
|
-
"method",
|
|
192
|
-
[fn("COUNT", col("id")), "totalRequests"],
|
|
193
|
-
[fn("AVG", col("durationMs")), "averageDurationMs"],
|
|
194
|
-
[fn("MAX", col("durationMs")), "maxDurationMs"],
|
|
195
|
-
],
|
|
196
|
-
where: {
|
|
197
|
-
route: { [Op.not]: null },
|
|
198
|
-
},
|
|
199
|
-
group: ["route", "method"],
|
|
200
|
-
order: [[literal("averageDurationMs"), "DESC"]],
|
|
201
|
-
raw: true,
|
|
202
|
-
}),
|
|
203
|
-
TrafficLog.findAll({
|
|
204
|
-
attributes: [
|
|
205
|
-
"route",
|
|
206
|
-
"method",
|
|
207
|
-
[fn("COUNT", col("id")), "totalCount"],
|
|
208
|
-
],
|
|
209
|
-
where: {
|
|
210
|
-
route: { [Op.not]: null },
|
|
211
|
-
statusCode: { [Op.gte]: 400 },
|
|
212
|
-
},
|
|
213
|
-
group: ["route", "method"],
|
|
214
|
-
raw: true,
|
|
215
|
-
}),
|
|
216
|
-
TrafficLog.findAll({
|
|
217
|
-
attributes: [
|
|
218
|
-
"route",
|
|
219
|
-
"method",
|
|
220
|
-
[fn("COUNT", col("id")), "totalCount"],
|
|
221
|
-
],
|
|
222
|
-
where: {
|
|
223
|
-
route: { [Op.not]: null },
|
|
224
|
-
isSlow: true,
|
|
225
|
-
},
|
|
226
|
-
group: ["route", "method"],
|
|
227
|
-
raw: true,
|
|
228
|
-
}),
|
|
229
|
-
]);
|
|
230
|
-
|
|
231
|
-
const errorCounts = mapGroupedCounts(errorRows);
|
|
232
|
-
const slowCounts = mapGroupedCounts(slowRows);
|
|
233
|
-
|
|
234
|
-
return baseRows.map((row) => {
|
|
235
|
-
const key = buildGroupKey(row);
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
route: row.route,
|
|
239
|
-
method: row.method,
|
|
240
|
-
totalRequests: Number(row.totalRequests),
|
|
241
|
-
averageDurationMs: toNumber(row.averageDurationMs),
|
|
242
|
-
maxDurationMs: Number(row.maxDurationMs),
|
|
243
|
-
errorCount: errorCounts.get(key) || 0,
|
|
244
|
-
slowCount: slowCounts.get(key) || 0,
|
|
245
|
-
};
|
|
246
|
-
});
|
|
247
|
-
},
|
|
248
|
-
|
|
249
|
-
async getUsers() {
|
|
250
|
-
const rows = await TrafficLog.findAll({
|
|
251
|
-
attributes: [
|
|
252
|
-
"userId",
|
|
253
|
-
[fn("COUNT", col("id")), "totalRequests"],
|
|
254
|
-
[fn("MAX", col("createdAt")), "lastSeenAt"],
|
|
255
|
-
[fn("AVG", col("durationMs")), "averageDurationMs"],
|
|
256
|
-
],
|
|
257
|
-
where: {
|
|
258
|
-
userId: { [Op.not]: null },
|
|
259
|
-
},
|
|
260
|
-
group: ["userId"],
|
|
261
|
-
order: [[literal("totalRequests"), "DESC"]],
|
|
262
|
-
raw: true,
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
return rows.map((row) => ({
|
|
266
|
-
userId: row.userId,
|
|
267
|
-
totalRequests: Number(row.totalRequests),
|
|
268
|
-
lastSeenAt: row.lastSeenAt,
|
|
269
|
-
averageDurationMs: toNumber(row.averageDurationMs),
|
|
270
|
-
}));
|
|
271
|
-
},
|
|
272
|
-
|
|
273
|
-
async getErrors() {
|
|
274
|
-
const rows = await TrafficLog.findAll({
|
|
275
|
-
where: {
|
|
276
|
-
statusCode: { [Op.gte]: 400 },
|
|
277
|
-
},
|
|
278
|
-
order: [["createdAt", "DESC"]],
|
|
279
|
-
limit: 100,
|
|
280
|
-
raw: true,
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
return rows.map(normalizeLog);
|
|
284
|
-
},
|
|
285
|
-
};
|
|
286
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
function parseBasicAuthHeader(headerValue) {
|
|
2
|
-
if (!headerValue || !headerValue.startsWith("Basic ")) {
|
|
3
|
-
return null;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
try {
|
|
7
|
-
const decoded = Buffer.from(headerValue.slice(6), "base64").toString(
|
|
8
|
-
"utf8",
|
|
9
|
-
);
|
|
10
|
-
const separatorIndex = decoded.indexOf(":");
|
|
11
|
-
|
|
12
|
-
if (separatorIndex === -1) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
username: decoded.slice(0, separatorIndex),
|
|
18
|
-
password: decoded.slice(separatorIndex + 1),
|
|
19
|
-
};
|
|
20
|
-
} catch (_error) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function isDashboardAuthEnabled(dashboard = {}) {
|
|
26
|
-
return Boolean(dashboard.enabled && dashboard.username && dashboard.password);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function createBasicAuthMiddleware(dashboard = {}) {
|
|
30
|
-
if (!isDashboardAuthEnabled(dashboard)) {
|
|
31
|
-
return (_req, _res, next) => next();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return (req, res, next) => {
|
|
35
|
-
const credentials = parseBasicAuthHeader(req.headers.authorization);
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
credentials?.username === dashboard.username &&
|
|
39
|
-
credentials?.password === dashboard.password
|
|
40
|
-
) {
|
|
41
|
-
next();
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
res.setHeader("WWW-Authenticate", 'Basic realm="Traffic Dashboard"');
|
|
46
|
-
res.status(401).json({ error: "Dashboard authentication required." });
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function createSocketAuthMiddleware(dashboard = {}) {
|
|
51
|
-
return (socket, next) => {
|
|
52
|
-
if (!isDashboardAuthEnabled(dashboard)) {
|
|
53
|
-
next();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const credentials = parseBasicAuthHeader(
|
|
58
|
-
socket.handshake.headers.authorization,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
credentials?.username === dashboard.username &&
|
|
63
|
-
credentials?.password === dashboard.password
|
|
64
|
-
) {
|
|
65
|
-
next();
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
next(new Error("Dashboard authentication required."));
|
|
70
|
-
};
|
|
71
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
function normalizeRouteCandidate(route) {
|
|
2
|
-
if (Array.isArray(route)) {
|
|
3
|
-
return route[0] || "/";
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
if (route instanceof RegExp) {
|
|
7
|
-
return route.toString();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
return route || null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function resolveTrackedRoute(req) {
|
|
14
|
-
const routePath = normalizeRouteCandidate(req.route?.path);
|
|
15
|
-
|
|
16
|
-
if (req.baseUrl && routePath) {
|
|
17
|
-
return routePath === "/" ? req.baseUrl || "/" : `${req.baseUrl}${routePath}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return routePath || req.path || req.originalUrl || "/";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function matchesIgnoredRoute(matcher, candidate) {
|
|
24
|
-
if (!candidate) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (matcher instanceof RegExp) {
|
|
29
|
-
return matcher.test(candidate);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (typeof matcher === "function") {
|
|
33
|
-
return Boolean(matcher(candidate));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (typeof matcher === "string") {
|
|
37
|
-
return candidate === matcher || candidate.startsWith(`${matcher}?`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function shouldIgnoreRoute({
|
|
44
|
-
route,
|
|
45
|
-
originalUrl,
|
|
46
|
-
ignoredRoutes = [],
|
|
47
|
-
}) {
|
|
48
|
-
return ignoredRoutes.some(
|
|
49
|
-
(matcher) =>
|
|
50
|
-
matchesIgnoredRoute(matcher, route) ||
|
|
51
|
-
matchesIgnoredRoute(matcher, originalUrl),
|
|
52
|
-
);
|
|
53
|
-
}
|
package/src/utils/safeAsync.js
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|