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