apitally 0.10.0 → 0.11.1

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.
Files changed (85) hide show
  1. package/README.md +1 -2
  2. package/dist/common/client.cjs +408 -12
  3. package/dist/common/client.cjs.map +1 -1
  4. package/dist/common/client.d.cts +8 -1
  5. package/dist/common/client.d.ts +8 -1
  6. package/dist/common/client.js +404 -8
  7. package/dist/common/client.js.map +1 -1
  8. package/dist/common/consumerRegistry.d.cts +4 -0
  9. package/dist/common/consumerRegistry.d.ts +4 -0
  10. package/dist/common/requestCounter.d.cts +4 -0
  11. package/dist/common/requestCounter.d.ts +4 -0
  12. package/dist/common/requestLogger.cjs +429 -0
  13. package/dist/common/requestLogger.cjs.map +1 -0
  14. package/dist/common/requestLogger.d.cts +64 -0
  15. package/dist/common/requestLogger.d.ts +64 -0
  16. package/dist/common/requestLogger.js +405 -0
  17. package/dist/common/requestLogger.js.map +1 -0
  18. package/dist/common/serverErrorCounter.d.cts +4 -0
  19. package/dist/common/serverErrorCounter.d.ts +4 -0
  20. package/dist/common/tempGzipFile.cjs +93 -0
  21. package/dist/common/tempGzipFile.cjs.map +1 -0
  22. package/dist/common/tempGzipFile.d.cts +17 -0
  23. package/dist/common/tempGzipFile.d.ts +17 -0
  24. package/dist/common/tempGzipFile.js +74 -0
  25. package/dist/common/tempGzipFile.js.map +1 -0
  26. package/dist/common/types.cjs.map +1 -1
  27. package/dist/common/types.d.cts +5 -0
  28. package/dist/common/types.d.ts +5 -0
  29. package/dist/common/validationErrorCounter.d.cts +4 -0
  30. package/dist/common/validationErrorCounter.d.ts +4 -0
  31. package/dist/express/index.cjs +510 -34
  32. package/dist/express/index.cjs.map +1 -1
  33. package/dist/express/index.d.cts +4 -0
  34. package/dist/express/index.d.ts +4 -0
  35. package/dist/express/index.js +506 -30
  36. package/dist/express/index.js.map +1 -1
  37. package/dist/express/middleware.cjs +510 -34
  38. package/dist/express/middleware.cjs.map +1 -1
  39. package/dist/express/middleware.d.cts +4 -0
  40. package/dist/express/middleware.d.ts +4 -0
  41. package/dist/express/middleware.js +506 -30
  42. package/dist/express/middleware.js.map +1 -1
  43. package/dist/fastify/index.cjs +501 -27
  44. package/dist/fastify/index.cjs.map +1 -1
  45. package/dist/fastify/index.d.cts +4 -0
  46. package/dist/fastify/index.d.ts +4 -0
  47. package/dist/fastify/index.js +497 -23
  48. package/dist/fastify/index.js.map +1 -1
  49. package/dist/fastify/plugin.cjs +501 -27
  50. package/dist/fastify/plugin.cjs.map +1 -1
  51. package/dist/fastify/plugin.d.cts +4 -0
  52. package/dist/fastify/plugin.d.ts +4 -0
  53. package/dist/fastify/plugin.js +497 -23
  54. package/dist/fastify/plugin.js.map +1 -1
  55. package/dist/hono/index.cjs +470 -15
  56. package/dist/hono/index.cjs.map +1 -1
  57. package/dist/hono/index.d.cts +4 -0
  58. package/dist/hono/index.d.ts +4 -0
  59. package/dist/hono/index.js +466 -11
  60. package/dist/hono/index.js.map +1 -1
  61. package/dist/hono/middleware.cjs +470 -15
  62. package/dist/hono/middleware.cjs.map +1 -1
  63. package/dist/hono/middleware.d.cts +4 -0
  64. package/dist/hono/middleware.d.ts +4 -0
  65. package/dist/hono/middleware.js +466 -11
  66. package/dist/hono/middleware.js.map +1 -1
  67. package/dist/koa/index.cjs +485 -15
  68. package/dist/koa/index.cjs.map +1 -1
  69. package/dist/koa/index.d.cts +4 -0
  70. package/dist/koa/index.d.ts +4 -0
  71. package/dist/koa/index.js +481 -11
  72. package/dist/koa/index.js.map +1 -1
  73. package/dist/koa/middleware.cjs +485 -15
  74. package/dist/koa/middleware.cjs.map +1 -1
  75. package/dist/koa/middleware.d.cts +4 -0
  76. package/dist/koa/middleware.d.ts +4 -0
  77. package/dist/koa/middleware.js +481 -11
  78. package/dist/koa/middleware.js.map +1 -1
  79. package/dist/nestjs/index.cjs +510 -34
  80. package/dist/nestjs/index.cjs.map +1 -1
  81. package/dist/nestjs/index.d.cts +4 -0
  82. package/dist/nestjs/index.d.ts +4 -0
  83. package/dist/nestjs/index.js +506 -30
  84. package/dist/nestjs/index.js.map +1 -1
  85. package/package.json +8 -2
@@ -13,7 +13,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
13
13
  import { performance } from "perf_hooks";
14
14
 
15
15
  // src/common/client.ts
16
- import { randomUUID } from "crypto";
16
+ import { randomUUID as randomUUID3 } from "crypto";
17
17
  import fetchRetry from "fetch-retry";
18
18
 
19
19
  // src/common/consumerRegistry.ts
@@ -182,6 +182,403 @@ var _RequestCounter = class _RequestCounter {
182
182
  __name(_RequestCounter, "RequestCounter");
183
183
  var RequestCounter = _RequestCounter;
184
184
 
185
+ // src/common/requestLogger.ts
186
+ import { Buffer as Buffer3 } from "buffer";
187
+ import { randomUUID as randomUUID2 } from "crypto";
188
+ import { unlinkSync as unlinkSync2, writeFileSync } from "fs";
189
+ import { tmpdir as tmpdir2 } from "os";
190
+ import { join as join2 } from "path";
191
+
192
+ // src/common/tempGzipFile.ts
193
+ import { Buffer as Buffer2 } from "buffer";
194
+ import { randomUUID } from "crypto";
195
+ import { createWriteStream, readFile, unlinkSync } from "fs";
196
+ import { tmpdir } from "os";
197
+ import { join } from "path";
198
+ import { createGzip } from "zlib";
199
+ var _TempGzipFile = class _TempGzipFile {
200
+ uuid;
201
+ filePath;
202
+ gzip;
203
+ writeStream;
204
+ readyPromise;
205
+ constructor() {
206
+ this.uuid = randomUUID();
207
+ this.filePath = join(tmpdir(), `apitally-${this.uuid}.gz`);
208
+ this.writeStream = createWriteStream(this.filePath);
209
+ this.readyPromise = new Promise((resolve, reject) => {
210
+ this.writeStream.once("ready", resolve);
211
+ this.writeStream.once("error", reject);
212
+ });
213
+ this.gzip = createGzip();
214
+ this.gzip.pipe(this.writeStream);
215
+ }
216
+ get size() {
217
+ return this.writeStream.bytesWritten;
218
+ }
219
+ async writeLine(data) {
220
+ await this.readyPromise;
221
+ return new Promise((resolve, reject) => {
222
+ this.gzip.write(Buffer2.concat([
223
+ data,
224
+ Buffer2.from("\n")
225
+ ]), (error) => {
226
+ if (error) {
227
+ reject(error);
228
+ } else {
229
+ resolve();
230
+ }
231
+ });
232
+ });
233
+ }
234
+ async getContent() {
235
+ return new Promise((resolve, reject) => {
236
+ readFile(this.filePath, (error, data) => {
237
+ if (error) {
238
+ reject(error);
239
+ } else {
240
+ resolve(data);
241
+ }
242
+ });
243
+ });
244
+ }
245
+ async close() {
246
+ return new Promise((resolve) => {
247
+ this.gzip.end(() => {
248
+ resolve();
249
+ });
250
+ });
251
+ }
252
+ async delete() {
253
+ await this.close();
254
+ unlinkSync(this.filePath);
255
+ }
256
+ };
257
+ __name(_TempGzipFile, "TempGzipFile");
258
+ var TempGzipFile = _TempGzipFile;
259
+
260
+ // src/common/requestLogger.ts
261
+ var MAX_BODY_SIZE = 5e4;
262
+ var MAX_FILE_SIZE = 1e6;
263
+ var MAX_FILES = 50;
264
+ var MAX_PENDING_WRITES = 100;
265
+ var BODY_TOO_LARGE = Buffer3.from("<body too large>");
266
+ var BODY_MASKED = Buffer3.from("<masked>");
267
+ var MASKED = "******";
268
+ var ALLOWED_CONTENT_TYPES = [
269
+ "application/json",
270
+ "text/plain"
271
+ ];
272
+ var EXCLUDE_PATH_PATTERNS = [
273
+ /\/_?healthz?$/i,
274
+ /\/_?health[_-]?checks?$/i,
275
+ /\/_?heart[_-]?beats?$/i,
276
+ /\/ping$/i,
277
+ /\/ready$/i,
278
+ /\/live$/i
279
+ ];
280
+ var EXCLUDE_USER_AGENT_PATTERNS = [
281
+ /health[-_ ]?check/i,
282
+ /microsoft-azure-application-lb/i,
283
+ /googlehc/i,
284
+ /kube-probe/i
285
+ ];
286
+ var MASK_QUERY_PARAM_PATTERNS = [
287
+ /auth/i,
288
+ /api-?key/i,
289
+ /secret/i,
290
+ /token/i,
291
+ /password/i,
292
+ /pwd/i
293
+ ];
294
+ var MASK_HEADER_PATTERNS = [
295
+ /auth/i,
296
+ /api-?key/i,
297
+ /secret/i,
298
+ /token/i,
299
+ /cookie/i
300
+ ];
301
+ var DEFAULT_CONFIG = {
302
+ enabled: false,
303
+ logQueryParams: true,
304
+ logRequestHeaders: false,
305
+ logRequestBody: false,
306
+ logResponseHeaders: true,
307
+ logResponseBody: false,
308
+ maskQueryParams: [],
309
+ maskHeaders: [],
310
+ excludePaths: []
311
+ };
312
+ var _RequestLogger = class _RequestLogger {
313
+ config;
314
+ enabled;
315
+ suspendUntil = null;
316
+ pendingWrites = [];
317
+ currentFile = null;
318
+ files = [];
319
+ maintainIntervalId;
320
+ constructor(config) {
321
+ this.config = {
322
+ ...DEFAULT_CONFIG,
323
+ ...config
324
+ };
325
+ this.enabled = this.config.enabled && checkWritableFs();
326
+ if (this.enabled) {
327
+ this.maintainIntervalId = setInterval(() => {
328
+ this.maintain();
329
+ }, 1e3);
330
+ }
331
+ }
332
+ shouldExcludePath(urlPath) {
333
+ const patterns = [
334
+ ...this.config.excludePaths,
335
+ ...EXCLUDE_PATH_PATTERNS
336
+ ];
337
+ return matchPatterns(urlPath, patterns);
338
+ }
339
+ shouldExcludeUserAgent(userAgent) {
340
+ return userAgent ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS) : false;
341
+ }
342
+ shouldMaskQueryParam(name) {
343
+ const patterns = [
344
+ ...this.config.maskQueryParams,
345
+ ...MASK_QUERY_PARAM_PATTERNS
346
+ ];
347
+ return matchPatterns(name, patterns);
348
+ }
349
+ shouldMaskHeader(name) {
350
+ const patterns = [
351
+ ...this.config.maskHeaders,
352
+ ...MASK_HEADER_PATTERNS
353
+ ];
354
+ return matchPatterns(name, patterns);
355
+ }
356
+ hasSupportedContentType(headers) {
357
+ var _a2;
358
+ const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
359
+ return contentType !== void 0 && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
360
+ }
361
+ maskQueryParams(search) {
362
+ const params = new URLSearchParams(search);
363
+ for (const [key] of params) {
364
+ if (this.shouldMaskQueryParam(key)) {
365
+ params.set(key, MASKED);
366
+ }
367
+ }
368
+ return params.toString();
369
+ }
370
+ maskHeaders(headers) {
371
+ return headers.map(([k, v]) => [
372
+ k,
373
+ this.shouldMaskHeader(k) ? MASKED : v
374
+ ]);
375
+ }
376
+ logRequest(request, response) {
377
+ var _a2, _b, _c;
378
+ if (!this.enabled || this.suspendUntil !== null) return;
379
+ const url = new URL(request.url);
380
+ const path = request.path ?? url.pathname;
381
+ const userAgent = (_a2 = request.headers.find(([k]) => k.toLowerCase() === "user-agent")) == null ? void 0 : _a2[1];
382
+ if (this.shouldExcludePath(path) || this.shouldExcludeUserAgent(userAgent) || (((_c = (_b = this.config).excludeCallback) == null ? void 0 : _c.call(_b, request, response)) ?? false)) {
383
+ return;
384
+ }
385
+ url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
386
+ request.url = url.toString();
387
+ request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
388
+ response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
389
+ if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
390
+ request.body = void 0;
391
+ } else if (request.body) {
392
+ if (request.body.length > MAX_BODY_SIZE) {
393
+ request.body = BODY_TOO_LARGE;
394
+ } else if (this.config.maskRequestBodyCallback) {
395
+ try {
396
+ request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
397
+ if (request.body.length > MAX_BODY_SIZE) {
398
+ request.body = BODY_TOO_LARGE;
399
+ }
400
+ } catch {
401
+ request.body = void 0;
402
+ }
403
+ }
404
+ }
405
+ if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
406
+ response.body = void 0;
407
+ } else if (response.body) {
408
+ if (response.body.length > MAX_BODY_SIZE) {
409
+ response.body = BODY_TOO_LARGE;
410
+ } else if (this.config.maskResponseBodyCallback) {
411
+ try {
412
+ response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
413
+ if (response.body.length > MAX_BODY_SIZE) {
414
+ response.body = BODY_TOO_LARGE;
415
+ }
416
+ } catch {
417
+ response.body = void 0;
418
+ }
419
+ }
420
+ }
421
+ const item = {
422
+ uuid: randomUUID2(),
423
+ request: skipEmptyValues(request),
424
+ response: skipEmptyValues(response)
425
+ };
426
+ [
427
+ item.request.body,
428
+ item.response.body
429
+ ].forEach((body) => {
430
+ if (body) {
431
+ body.toJSON = function() {
432
+ return this.toString("base64");
433
+ };
434
+ }
435
+ });
436
+ this.pendingWrites.push(JSON.stringify(item));
437
+ if (this.pendingWrites.length > MAX_PENDING_WRITES) {
438
+ this.pendingWrites.shift();
439
+ }
440
+ }
441
+ async writeToFile() {
442
+ if (!this.enabled || this.pendingWrites.length === 0) {
443
+ return;
444
+ }
445
+ if (!this.currentFile) {
446
+ this.currentFile = new TempGzipFile();
447
+ }
448
+ while (this.pendingWrites.length > 0) {
449
+ const item = this.pendingWrites.shift();
450
+ if (item) {
451
+ await this.currentFile.writeLine(Buffer3.from(item));
452
+ }
453
+ }
454
+ }
455
+ getFile() {
456
+ return this.files.shift();
457
+ }
458
+ retryFileLater(file) {
459
+ this.files.unshift(file);
460
+ }
461
+ async rotateFile() {
462
+ if (this.currentFile) {
463
+ await this.currentFile.close();
464
+ this.files.push(this.currentFile);
465
+ this.currentFile = null;
466
+ }
467
+ }
468
+ async maintain() {
469
+ await this.writeToFile();
470
+ if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {
471
+ await this.rotateFile();
472
+ }
473
+ while (this.files.length > MAX_FILES) {
474
+ const file = this.files.shift();
475
+ file == null ? void 0 : file.delete();
476
+ }
477
+ if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {
478
+ this.suspendUntil = null;
479
+ }
480
+ }
481
+ async clear() {
482
+ this.pendingWrites = [];
483
+ await this.rotateFile();
484
+ this.files.forEach((file) => {
485
+ file.delete();
486
+ });
487
+ this.files = [];
488
+ }
489
+ async close() {
490
+ this.enabled = false;
491
+ await this.clear();
492
+ if (this.maintainIntervalId) {
493
+ clearInterval(this.maintainIntervalId);
494
+ }
495
+ }
496
+ };
497
+ __name(_RequestLogger, "RequestLogger");
498
+ var RequestLogger = _RequestLogger;
499
+ function convertHeaders(headers) {
500
+ if (headers instanceof Headers) {
501
+ return Array.from(headers.entries());
502
+ }
503
+ return Object.entries(headers).flatMap(([key, value]) => {
504
+ if (value === void 0) {
505
+ return [];
506
+ }
507
+ if (Array.isArray(value)) {
508
+ return value.map((v) => [
509
+ key,
510
+ v
511
+ ]);
512
+ }
513
+ return [
514
+ [
515
+ key,
516
+ value.toString()
517
+ ]
518
+ ];
519
+ });
520
+ }
521
+ __name(convertHeaders, "convertHeaders");
522
+ function convertBody(body, contentType) {
523
+ if (!body || !contentType) {
524
+ return;
525
+ }
526
+ try {
527
+ if (contentType.startsWith("application/json")) {
528
+ if (isValidJsonString(body)) {
529
+ return Buffer3.from(body);
530
+ } else {
531
+ return Buffer3.from(JSON.stringify(body));
532
+ }
533
+ }
534
+ if (contentType.startsWith("text/") && typeof body === "string") {
535
+ return Buffer3.from(body);
536
+ }
537
+ } catch (error) {
538
+ return;
539
+ }
540
+ }
541
+ __name(convertBody, "convertBody");
542
+ function isValidJsonString(body) {
543
+ if (typeof body !== "string") {
544
+ return false;
545
+ }
546
+ try {
547
+ JSON.parse(body);
548
+ return true;
549
+ } catch {
550
+ return false;
551
+ }
552
+ }
553
+ __name(isValidJsonString, "isValidJsonString");
554
+ function matchPatterns(value, patterns) {
555
+ return patterns.some((pattern) => {
556
+ return pattern.test(value);
557
+ });
558
+ }
559
+ __name(matchPatterns, "matchPatterns");
560
+ function skipEmptyValues(data) {
561
+ return Object.fromEntries(Object.entries(data).filter(([_, v]) => {
562
+ if (v == null || Number.isNaN(v)) return false;
563
+ if (Array.isArray(v) || Buffer3.isBuffer(v) || typeof v === "string") {
564
+ return v.length > 0;
565
+ }
566
+ return true;
567
+ }));
568
+ }
569
+ __name(skipEmptyValues, "skipEmptyValues");
570
+ function checkWritableFs() {
571
+ try {
572
+ const testPath = join2(tmpdir2(), `apitally-${randomUUID2()}`);
573
+ writeFileSync(testPath, "test");
574
+ unlinkSync2(testPath);
575
+ return true;
576
+ } catch (error) {
577
+ return false;
578
+ }
579
+ }
580
+ __name(checkWritableFs, "checkWritableFs");
581
+
185
582
  // src/common/serverErrorCounter.ts
186
583
  import { createHash } from "crypto";
187
584
  var MAX_MSG_LENGTH = 2048;
@@ -354,11 +751,12 @@ var _ApitallyClient = class _ApitallyClient {
354
751
  startupData;
355
752
  startupDataSent = false;
356
753
  requestCounter;
754
+ requestLogger;
357
755
  validationErrorCounter;
358
756
  serverErrorCounter;
359
757
  consumerRegistry;
360
758
  logger;
361
- constructor({ clientId, env = "dev", logger }) {
759
+ constructor({ clientId, env = "dev", requestLoggingConfig, logger }) {
362
760
  if (_ApitallyClient.instance) {
363
761
  throw new Error("Apitally client is already initialized");
364
762
  }
@@ -371,9 +769,10 @@ var _ApitallyClient = class _ApitallyClient {
371
769
  _ApitallyClient.instance = this;
372
770
  this.clientId = clientId;
373
771
  this.env = env;
374
- this.instanceUuid = randomUUID();
772
+ this.instanceUuid = randomUUID3();
375
773
  this.syncDataQueue = [];
376
774
  this.requestCounter = new RequestCounter();
775
+ this.requestLogger = new RequestLogger(requestLoggingConfig);
377
776
  this.validationErrorCounter = new ValidationErrorCounter();
378
777
  this.serverErrorCounter = new ServerErrorCounter();
379
778
  this.consumerRegistry = new ConsumerRegistry();
@@ -395,6 +794,8 @@ var _ApitallyClient = class _ApitallyClient {
395
794
  async handleShutdown() {
396
795
  this.stopSync();
397
796
  await this.sendSyncData();
797
+ await this.sendLogData();
798
+ await this.requestLogger.close();
398
799
  _ApitallyClient.instance = void 0;
399
800
  }
400
801
  getHubUrlPrefix() {
@@ -441,7 +842,8 @@ var _ApitallyClient = class _ApitallyClient {
441
842
  async sync() {
442
843
  try {
443
844
  const promises = [
444
- this.sendSyncData()
845
+ this.sendSyncData(),
846
+ this.sendLogData()
445
847
  ];
446
848
  if (!this.startupDataSent) {
447
849
  promises.push(this.sendStartupData());
@@ -469,7 +871,7 @@ var _ApitallyClient = class _ApitallyClient {
469
871
  this.logger.debug("Sending startup data to Apitally Hub");
470
872
  const payload = {
471
873
  instance_uuid: this.instanceUuid,
472
- message_uuid: randomUUID(),
874
+ message_uuid: randomUUID3(),
473
875
  ...this.startupData
474
876
  };
475
877
  try {
@@ -491,7 +893,7 @@ var _ApitallyClient = class _ApitallyClient {
491
893
  const newPayload = {
492
894
  time_offset: 0,
493
895
  instance_uuid: this.instanceUuid,
494
- message_uuid: randomUUID(),
896
+ message_uuid: randomUUID3(),
495
897
  requests: this.requestCounter.getAndResetRequests(),
496
898
  validation_errors: this.validationErrorCounter.getAndResetValidationErrors(),
497
899
  server_errors: this.serverErrorCounter.getAndResetServerErrors(),
@@ -510,8 +912,7 @@ var _ApitallyClient = class _ApitallyClient {
510
912
  const timeOffset = Date.now() - time;
511
913
  if (timeOffset <= MAX_QUEUE_TIME) {
512
914
  if (i > 0) {
513
- const waitMs = 100 + Math.random() * 200;
514
- await new Promise((resolve) => setTimeout(resolve, waitMs));
915
+ await this.randomDelay();
515
916
  }
516
917
  payload.time_offset = timeOffset / 1e3;
517
918
  await this.sendData("sync", payload);
@@ -530,6 +931,52 @@ var _ApitallyClient = class _ApitallyClient {
530
931
  }
531
932
  }
532
933
  }
934
+ async sendLogData() {
935
+ this.logger.debug("Sending request log data to Apitally Hub");
936
+ await this.requestLogger.rotateFile();
937
+ const fetchWithRetry = fetchRetry(fetch, {
938
+ retries: 3,
939
+ retryDelay: 1e3,
940
+ retryOn: [
941
+ 408,
942
+ 429,
943
+ 500,
944
+ 502,
945
+ 503,
946
+ 504
947
+ ]
948
+ });
949
+ let i = 0;
950
+ let logFile;
951
+ while (logFile = this.requestLogger.getFile()) {
952
+ if (i > 0) {
953
+ await this.randomDelay();
954
+ }
955
+ try {
956
+ const response = await fetchWithRetry(`${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`, {
957
+ method: "POST",
958
+ body: await logFile.getContent()
959
+ });
960
+ if (response.status === 402 && response.headers.has("Retry-After")) {
961
+ const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0");
962
+ if (retryAfter > 0) {
963
+ this.requestLogger.suspendUntil = Date.now() + retryAfter * 1e3;
964
+ this.requestLogger.clear();
965
+ return;
966
+ }
967
+ }
968
+ if (!response.ok) {
969
+ throw new HTTPError(response);
970
+ }
971
+ logFile.delete();
972
+ } catch (error) {
973
+ this.requestLogger.retryFileLater(logFile);
974
+ break;
975
+ }
976
+ i++;
977
+ if (i >= 10) break;
978
+ }
979
+ }
533
980
  handleHubError(error) {
534
981
  if (error instanceof HTTPError) {
535
982
  if (error.response.status === 404) {
@@ -544,6 +991,10 @@ var _ApitallyClient = class _ApitallyClient {
544
991
  }
545
992
  return false;
546
993
  }
994
+ async randomDelay() {
995
+ const delay = 100 + Math.random() * 200;
996
+ await new Promise((resolve) => setTimeout(resolve, delay));
997
+ }
547
998
  };
548
999
  __name(_ApitallyClient, "ApitallyClient");
549
1000
  __publicField(_ApitallyClient, "instance");
@@ -713,18 +1164,18 @@ var getMiddleware = /* @__PURE__ */ __name((app, client) => {
713
1164
  }
714
1165
  try {
715
1166
  const startTime = performance.now();
716
- const originalJson = res.json;
717
- res.json = (body) => {
1167
+ const originalSend = res.send;
1168
+ res.send = (body) => {
718
1169
  res.locals.body = body;
719
- return originalJson.call(res, body);
1170
+ return originalSend.call(res, body);
720
1171
  };
721
1172
  res.on("finish", () => {
722
1173
  try {
1174
+ const responseTime = performance.now() - startTime;
723
1175
  const path = getRoutePath(req);
1176
+ const consumer = getConsumer(req);
1177
+ client.consumerRegistry.addOrUpdateConsumer(consumer);
724
1178
  if (path) {
725
- const responseTime = performance.now() - startTime;
726
- const consumer = getConsumer(req);
727
- client.consumerRegistry.addOrUpdateConsumer(consumer);
728
1179
  client.requestCounter.addRequest({
729
1180
  consumer: consumer == null ? void 0 : consumer.identifier,
730
1181
  method: req.method,
@@ -735,24 +1186,31 @@ var getMiddleware = /* @__PURE__ */ __name((app, client) => {
735
1186
  responseSize: res.get("content-length")
736
1187
  });
737
1188
  if ((res.statusCode === 400 || res.statusCode === 422) && res.locals.body) {
738
- const validationErrors = [];
739
- if (validationErrors.length === 0) {
740
- validationErrors.push(...extractExpressValidatorErrors(res.locals.body));
741
- }
742
- if (validationErrors.length === 0) {
743
- validationErrors.push(...extractCelebrateErrors(res.locals.body));
1189
+ let jsonBody;
1190
+ try {
1191
+ jsonBody = JSON.parse(res.locals.body);
1192
+ } catch {
744
1193
  }
745
- if (validationErrors.length === 0) {
746
- validationErrors.push(...extractNestValidationErrors(res.locals.body));
747
- }
748
- validationErrors.forEach((error) => {
749
- client.validationErrorCounter.addValidationError({
750
- consumer: consumer == null ? void 0 : consumer.identifier,
751
- method: req.method,
752
- path: req.route.path,
753
- ...error
1194
+ if (jsonBody) {
1195
+ const validationErrors = [];
1196
+ if (validationErrors.length === 0) {
1197
+ validationErrors.push(...extractExpressValidatorErrors(jsonBody));
1198
+ }
1199
+ if (validationErrors.length === 0) {
1200
+ validationErrors.push(...extractCelebrateErrors(jsonBody));
1201
+ }
1202
+ if (validationErrors.length === 0) {
1203
+ validationErrors.push(...extractNestValidationErrors(jsonBody));
1204
+ }
1205
+ validationErrors.forEach((error) => {
1206
+ client.validationErrorCounter.addValidationError({
1207
+ consumer: consumer == null ? void 0 : consumer.identifier,
1208
+ method: req.method,
1209
+ path: req.route.path,
1210
+ ...error
1211
+ });
754
1212
  });
755
- });
1213
+ }
756
1214
  }
757
1215
  if (res.statusCode === 500 && res.locals.serverError) {
758
1216
  const serverError = res.locals.serverError;
@@ -766,6 +1224,24 @@ var getMiddleware = /* @__PURE__ */ __name((app, client) => {
766
1224
  });
767
1225
  }
768
1226
  }
1227
+ if (client.requestLogger.enabled) {
1228
+ client.requestLogger.logRequest({
1229
+ timestamp: Date.now() / 1e3,
1230
+ method: req.method,
1231
+ path,
1232
+ url: `${req.protocol}://${req.host}${req.originalUrl}`,
1233
+ headers: convertHeaders(req.headers),
1234
+ size: Number(req.get("content-length")),
1235
+ consumer: consumer == null ? void 0 : consumer.identifier,
1236
+ body: convertBody(req.body, req.get("content-type"))
1237
+ }, {
1238
+ statusCode: res.statusCode,
1239
+ responseTime: responseTime / 1e3,
1240
+ headers: convertHeaders(res.getHeaders()),
1241
+ size: Number(res.get("content-length")),
1242
+ body: convertBody(res.locals.body, res.get("content-type"))
1243
+ });
1244
+ }
769
1245
  } catch (error) {
770
1246
  client.logger.error("Error while logging request in Apitally middleware.", {
771
1247
  request: req,