@shieldpress/tracker 1.0.0 → 1.1.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/express.mjs CHANGED
@@ -5,6 +5,10 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
+ // src/tracker.ts
9
+ import http2 from "http";
10
+ import https2 from "https";
11
+
8
12
  // src/config.ts
9
13
  var DEFAULT_API_URL = "https://api.shieldpress.net";
10
14
  function resolveConfig(config) {
@@ -81,8 +85,8 @@ function getPublicIpCached() {
81
85
  if (Date.now() - publicIpLastFetch > 6e5) {
82
86
  publicIpLastFetch = Date.now();
83
87
  try {
84
- const http2 = __require("https");
85
- const req = http2.get("https://api.ipify.org", { timeout: 3e3 }, (res) => {
88
+ const http3 = __require("https");
89
+ const req = http3.get("https://api.ipify.org", { timeout: 3e3 }, (res) => {
86
90
  let data = "";
87
91
  res.on("data", (chunk) => {
88
92
  data += chunk;
@@ -1118,23 +1122,49 @@ function sanitizePayload(payload) {
1118
1122
  }
1119
1123
  return p;
1120
1124
  }
1121
- var Reporter = class {
1125
+ var _Reporter = class _Reporter {
1122
1126
  constructor(apiUrl, apiKey, logger) {
1123
1127
  this.queue = [];
1124
1128
  this.sending = false;
1129
+ this.consecutiveFailures = 0;
1130
+ this.backoffUntil = 0;
1125
1131
  this.apiUrl = apiUrl.replace(/\/$/, "");
1126
1132
  this.apiKey = apiKey;
1127
1133
  this.logger = logger;
1128
1134
  }
1129
1135
  async send(payload) {
1130
1136
  try {
1137
+ if (this.queue.length >= _Reporter.MAX_QUEUE_SIZE) {
1138
+ const dropped = this.queue.length - _Reporter.MAX_QUEUE_SIZE + 1;
1139
+ this.queue.splice(0, dropped);
1140
+ this.logger.warn(`Queue full, dropped ${dropped} oldest report(s)`);
1141
+ }
1131
1142
  this.queue.push(payload);
1132
1143
  if (this.sending) return true;
1144
+ const now = Date.now();
1145
+ if (now < this.backoffUntil) {
1146
+ this.logger.info(`Backoff active, will retry in ${Math.round((this.backoffUntil - now) / 1e3)}s`);
1147
+ return true;
1148
+ }
1133
1149
  this.sending = true;
1134
1150
  try {
1135
1151
  while (this.queue.length > 0) {
1152
+ if (Date.now() < this.backoffUntil) break;
1136
1153
  const batch = this.queue.splice(0, 10);
1137
- await this.transmit(batch);
1154
+ try {
1155
+ await this.transmit(batch);
1156
+ this.consecutiveFailures = 0;
1157
+ this.backoffUntil = 0;
1158
+ } catch (err) {
1159
+ this.queue.unshift(...batch);
1160
+ this.consecutiveFailures++;
1161
+ const delay = Math.min(1e3 * Math.pow(2, this.consecutiveFailures - 1), _Reporter.MAX_BACKOFF_MS);
1162
+ this.backoffUntil = Date.now() + delay;
1163
+ this.logger.warn(
1164
+ `Transmit failed (attempt ${this.consecutiveFailures}), backing off ${Math.round(delay / 1e3)}s: ${err instanceof Error ? err.message : err}`
1165
+ );
1166
+ break;
1167
+ }
1138
1168
  }
1139
1169
  return true;
1140
1170
  } finally {
@@ -1157,6 +1187,8 @@ var Reporter = class {
1157
1187
  `Payload too large (${body.length} bytes), truncating arrays (key=${maskApiKey(this.apiKey)})`
1158
1188
  );
1159
1189
  for (const p of sanitized) {
1190
+ p._truncated = true;
1191
+ p._originalSize = body.length;
1160
1192
  if (p.security) {
1161
1193
  p.security.events = p.security.events.slice(0, 10);
1162
1194
  }
@@ -1224,6 +1256,11 @@ var Reporter = class {
1224
1256
  });
1225
1257
  }
1226
1258
  };
1259
+ /** Max queued reports before dropping oldest */
1260
+ _Reporter.MAX_QUEUE_SIZE = 100;
1261
+ /** Max backoff delay in ms (60 seconds) */
1262
+ _Reporter.MAX_BACKOFF_MS = 6e4;
1263
+ var Reporter = _Reporter;
1227
1264
 
1228
1265
  // src/tracker.ts
1229
1266
  var ShieldPressTracker = class {
@@ -1232,6 +1269,7 @@ var ShieldPressTracker = class {
1232
1269
  this.lagTimer = null;
1233
1270
  this.started = false;
1234
1271
  this.headersChecked = false;
1272
+ this.autoHooked = false;
1235
1273
  this.config = resolveConfig(userConfig);
1236
1274
  this.logger = createLogger(this.config.debug);
1237
1275
  this.reporter = new Reporter(this.config.apiUrl, this.config.apiKey, this.logger);
@@ -1259,6 +1297,9 @@ var ShieldPressTracker = class {
1259
1297
  this.errorCollector.capture(err, { type: "unhandledRejection" });
1260
1298
  });
1261
1299
  }
1300
+ if (this.config.httpTracking) {
1301
+ this.autoHookHttpServers();
1302
+ }
1262
1303
  if (this.config.systemMetrics) {
1263
1304
  collectSystemMetrics();
1264
1305
  }
@@ -1378,6 +1419,50 @@ var ShieldPressTracker = class {
1378
1419
  }
1379
1420
  return this.reporter.send(payload);
1380
1421
  }
1422
+ /**
1423
+ * Monkey-patch http.Server and https.Server so every incoming request
1424
+ * is automatically tracked without requiring the user to install middleware.
1425
+ * Safe to call multiple times — patches only once.
1426
+ */
1427
+ autoHookHttpServers() {
1428
+ if (this.autoHooked) return;
1429
+ this.autoHooked = true;
1430
+ const tracker = this;
1431
+ const patchEmit = (ServerProto) => {
1432
+ const originalEmit = ServerProto.emit;
1433
+ ServerProto.emit = function(event, ...args) {
1434
+ if (event === "request") {
1435
+ const req = args[0];
1436
+ const res = args[1];
1437
+ const start = process.hrtime.bigint();
1438
+ const url = req.url || "/";
1439
+ res.on("finish", () => {
1440
+ try {
1441
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
1442
+ const parsedPath = url.split("?")[0] || "/";
1443
+ tracker.recordRequest({
1444
+ path: parsedPath,
1445
+ method: req.method || "GET",
1446
+ statusCode: res.statusCode,
1447
+ durationMs: Math.round(durationMs * 100) / 100
1448
+ });
1449
+ } catch {
1450
+ }
1451
+ });
1452
+ }
1453
+ return originalEmit.apply(this, [event, ...args]);
1454
+ };
1455
+ };
1456
+ try {
1457
+ patchEmit(http2.Server.prototype);
1458
+ patchEmit(https2.Server.prototype);
1459
+ this.logger.info("Auto-hooked http/https servers for request tracking");
1460
+ } catch (err) {
1461
+ this.logger.warn(
1462
+ `Failed to auto-hook HTTP servers: ${err instanceof Error ? err.message : err}`
1463
+ );
1464
+ }
1465
+ }
1381
1466
  shouldIgnorePath(path) {
1382
1467
  return this.config.ignorePaths.some((pattern) => {
1383
1468
  if (pattern.endsWith("/*")) {