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
@@ -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,403 @@ 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?$/i,
278
+ /\/_?health[_-]?checks?$/i,
279
+ /\/_?heart[_-]?beats?$/i,
280
+ /\/ping$/i,
281
+ /\/ready$/i,
282
+ /\/live$/i
283
+ ];
284
+ var EXCLUDE_USER_AGENT_PATTERNS = [
285
+ /health[-_ ]?check/i,
286
+ /microsoft-azure-application-lb/i,
287
+ /googlehc/i,
288
+ /kube-probe/i
289
+ ];
290
+ var MASK_QUERY_PARAM_PATTERNS = [
291
+ /auth/i,
292
+ /api-?key/i,
293
+ /secret/i,
294
+ /token/i,
295
+ /password/i,
296
+ /pwd/i
297
+ ];
298
+ var MASK_HEADER_PATTERNS = [
299
+ /auth/i,
300
+ /api-?key/i,
301
+ /secret/i,
302
+ /token/i,
303
+ /cookie/i
304
+ ];
305
+ var DEFAULT_CONFIG = {
306
+ enabled: false,
307
+ logQueryParams: true,
308
+ logRequestHeaders: false,
309
+ logRequestBody: false,
310
+ logResponseHeaders: true,
311
+ logResponseBody: false,
312
+ maskQueryParams: [],
313
+ maskHeaders: [],
314
+ excludePaths: []
315
+ };
316
+ var _RequestLogger = class _RequestLogger {
317
+ config;
318
+ enabled;
319
+ suspendUntil = null;
320
+ pendingWrites = [];
321
+ currentFile = null;
322
+ files = [];
323
+ maintainIntervalId;
324
+ constructor(config) {
325
+ this.config = {
326
+ ...DEFAULT_CONFIG,
327
+ ...config
328
+ };
329
+ this.enabled = this.config.enabled && checkWritableFs();
330
+ if (this.enabled) {
331
+ this.maintainIntervalId = setInterval(() => {
332
+ this.maintain();
333
+ }, 1e3);
334
+ }
335
+ }
336
+ shouldExcludePath(urlPath) {
337
+ const patterns = [
338
+ ...this.config.excludePaths,
339
+ ...EXCLUDE_PATH_PATTERNS
340
+ ];
341
+ return matchPatterns(urlPath, patterns);
342
+ }
343
+ shouldExcludeUserAgent(userAgent) {
344
+ return userAgent ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS) : false;
345
+ }
346
+ shouldMaskQueryParam(name) {
347
+ const patterns = [
348
+ ...this.config.maskQueryParams,
349
+ ...MASK_QUERY_PARAM_PATTERNS
350
+ ];
351
+ return matchPatterns(name, patterns);
352
+ }
353
+ shouldMaskHeader(name) {
354
+ const patterns = [
355
+ ...this.config.maskHeaders,
356
+ ...MASK_HEADER_PATTERNS
357
+ ];
358
+ return matchPatterns(name, patterns);
359
+ }
360
+ hasSupportedContentType(headers) {
361
+ var _a3;
362
+ const contentType = (_a3 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a3[1];
363
+ return contentType !== void 0 && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
364
+ }
365
+ maskQueryParams(search) {
366
+ const params = new URLSearchParams(search);
367
+ for (const [key] of params) {
368
+ if (this.shouldMaskQueryParam(key)) {
369
+ params.set(key, MASKED);
370
+ }
371
+ }
372
+ return params.toString();
373
+ }
374
+ maskHeaders(headers) {
375
+ return headers.map(([k, v]) => [
376
+ k,
377
+ this.shouldMaskHeader(k) ? MASKED : v
378
+ ]);
379
+ }
380
+ logRequest(request, response) {
381
+ var _a3, _b, _c;
382
+ if (!this.enabled || this.suspendUntil !== null) return;
383
+ const url = new URL(request.url);
384
+ const path = request.path ?? url.pathname;
385
+ const userAgent = (_a3 = request.headers.find(([k]) => k.toLowerCase() === "user-agent")) == null ? void 0 : _a3[1];
386
+ if (this.shouldExcludePath(path) || this.shouldExcludeUserAgent(userAgent) || (((_c = (_b = this.config).excludeCallback) == null ? void 0 : _c.call(_b, request, response)) ?? false)) {
387
+ return;
388
+ }
389
+ url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
390
+ request.url = url.toString();
391
+ request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
392
+ response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
393
+ if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
394
+ request.body = void 0;
395
+ } else if (request.body) {
396
+ if (request.body.length > MAX_BODY_SIZE) {
397
+ request.body = BODY_TOO_LARGE;
398
+ } else if (this.config.maskRequestBodyCallback) {
399
+ try {
400
+ request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
401
+ if (request.body.length > MAX_BODY_SIZE) {
402
+ request.body = BODY_TOO_LARGE;
403
+ }
404
+ } catch {
405
+ request.body = void 0;
406
+ }
407
+ }
408
+ }
409
+ if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
410
+ response.body = void 0;
411
+ } else if (response.body) {
412
+ if (response.body.length > MAX_BODY_SIZE) {
413
+ response.body = BODY_TOO_LARGE;
414
+ } else if (this.config.maskResponseBodyCallback) {
415
+ try {
416
+ response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
417
+ if (response.body.length > MAX_BODY_SIZE) {
418
+ response.body = BODY_TOO_LARGE;
419
+ }
420
+ } catch {
421
+ response.body = void 0;
422
+ }
423
+ }
424
+ }
425
+ const item = {
426
+ uuid: randomUUID2(),
427
+ request: skipEmptyValues(request),
428
+ response: skipEmptyValues(response)
429
+ };
430
+ [
431
+ item.request.body,
432
+ item.response.body
433
+ ].forEach((body) => {
434
+ if (body) {
435
+ body.toJSON = function() {
436
+ return this.toString("base64");
437
+ };
438
+ }
439
+ });
440
+ this.pendingWrites.push(JSON.stringify(item));
441
+ if (this.pendingWrites.length > MAX_PENDING_WRITES) {
442
+ this.pendingWrites.shift();
443
+ }
444
+ }
445
+ async writeToFile() {
446
+ if (!this.enabled || this.pendingWrites.length === 0) {
447
+ return;
448
+ }
449
+ if (!this.currentFile) {
450
+ this.currentFile = new TempGzipFile();
451
+ }
452
+ while (this.pendingWrites.length > 0) {
453
+ const item = this.pendingWrites.shift();
454
+ if (item) {
455
+ await this.currentFile.writeLine(Buffer3.from(item));
456
+ }
457
+ }
458
+ }
459
+ getFile() {
460
+ return this.files.shift();
461
+ }
462
+ retryFileLater(file) {
463
+ this.files.unshift(file);
464
+ }
465
+ async rotateFile() {
466
+ if (this.currentFile) {
467
+ await this.currentFile.close();
468
+ this.files.push(this.currentFile);
469
+ this.currentFile = null;
470
+ }
471
+ }
472
+ async maintain() {
473
+ await this.writeToFile();
474
+ if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {
475
+ await this.rotateFile();
476
+ }
477
+ while (this.files.length > MAX_FILES) {
478
+ const file = this.files.shift();
479
+ file == null ? void 0 : file.delete();
480
+ }
481
+ if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {
482
+ this.suspendUntil = null;
483
+ }
484
+ }
485
+ async clear() {
486
+ this.pendingWrites = [];
487
+ await this.rotateFile();
488
+ this.files.forEach((file) => {
489
+ file.delete();
490
+ });
491
+ this.files = [];
492
+ }
493
+ async close() {
494
+ this.enabled = false;
495
+ await this.clear();
496
+ if (this.maintainIntervalId) {
497
+ clearInterval(this.maintainIntervalId);
498
+ }
499
+ }
500
+ };
501
+ __name(_RequestLogger, "RequestLogger");
502
+ var RequestLogger = _RequestLogger;
503
+ function convertHeaders(headers) {
504
+ if (headers instanceof Headers) {
505
+ return Array.from(headers.entries());
506
+ }
507
+ return Object.entries(headers).flatMap(([key, value]) => {
508
+ if (value === void 0) {
509
+ return [];
510
+ }
511
+ if (Array.isArray(value)) {
512
+ return value.map((v) => [
513
+ key,
514
+ v
515
+ ]);
516
+ }
517
+ return [
518
+ [
519
+ key,
520
+ value.toString()
521
+ ]
522
+ ];
523
+ });
524
+ }
525
+ __name(convertHeaders, "convertHeaders");
526
+ function convertBody(body, contentType) {
527
+ if (!body || !contentType) {
528
+ return;
529
+ }
530
+ try {
531
+ if (contentType.startsWith("application/json")) {
532
+ if (isValidJsonString(body)) {
533
+ return Buffer3.from(body);
534
+ } else {
535
+ return Buffer3.from(JSON.stringify(body));
536
+ }
537
+ }
538
+ if (contentType.startsWith("text/") && typeof body === "string") {
539
+ return Buffer3.from(body);
540
+ }
541
+ } catch (error) {
542
+ return;
543
+ }
544
+ }
545
+ __name(convertBody, "convertBody");
546
+ function isValidJsonString(body) {
547
+ if (typeof body !== "string") {
548
+ return false;
549
+ }
550
+ try {
551
+ JSON.parse(body);
552
+ return true;
553
+ } catch {
554
+ return false;
555
+ }
556
+ }
557
+ __name(isValidJsonString, "isValidJsonString");
558
+ function matchPatterns(value, patterns) {
559
+ return patterns.some((pattern) => {
560
+ return pattern.test(value);
561
+ });
562
+ }
563
+ __name(matchPatterns, "matchPatterns");
564
+ function skipEmptyValues(data) {
565
+ return Object.fromEntries(Object.entries(data).filter(([_, v]) => {
566
+ if (v == null || Number.isNaN(v)) return false;
567
+ if (Array.isArray(v) || Buffer3.isBuffer(v) || typeof v === "string") {
568
+ return v.length > 0;
569
+ }
570
+ return true;
571
+ }));
572
+ }
573
+ __name(skipEmptyValues, "skipEmptyValues");
574
+ function checkWritableFs() {
575
+ try {
576
+ const testPath = join2(tmpdir2(), `apitally-${randomUUID2()}`);
577
+ writeFileSync(testPath, "test");
578
+ unlinkSync2(testPath);
579
+ return true;
580
+ } catch (error) {
581
+ return false;
582
+ }
583
+ }
584
+ __name(checkWritableFs, "checkWritableFs");
585
+
189
586
  // src/common/serverErrorCounter.ts
190
587
  import { createHash } from "crypto";
191
588
  var MAX_MSG_LENGTH = 2048;
@@ -358,11 +755,12 @@ var _ApitallyClient = class _ApitallyClient {
358
755
  startupData;
359
756
  startupDataSent = false;
360
757
  requestCounter;
758
+ requestLogger;
361
759
  validationErrorCounter;
362
760
  serverErrorCounter;
363
761
  consumerRegistry;
364
762
  logger;
365
- constructor({ clientId, env = "dev", logger }) {
763
+ constructor({ clientId, env = "dev", requestLoggingConfig, logger }) {
366
764
  if (_ApitallyClient.instance) {
367
765
  throw new Error("Apitally client is already initialized");
368
766
  }
@@ -375,9 +773,10 @@ var _ApitallyClient = class _ApitallyClient {
375
773
  _ApitallyClient.instance = this;
376
774
  this.clientId = clientId;
377
775
  this.env = env;
378
- this.instanceUuid = randomUUID();
776
+ this.instanceUuid = randomUUID3();
379
777
  this.syncDataQueue = [];
380
778
  this.requestCounter = new RequestCounter();
779
+ this.requestLogger = new RequestLogger(requestLoggingConfig);
381
780
  this.validationErrorCounter = new ValidationErrorCounter();
382
781
  this.serverErrorCounter = new ServerErrorCounter();
383
782
  this.consumerRegistry = new ConsumerRegistry();
@@ -399,6 +798,8 @@ var _ApitallyClient = class _ApitallyClient {
399
798
  async handleShutdown() {
400
799
  this.stopSync();
401
800
  await this.sendSyncData();
801
+ await this.sendLogData();
802
+ await this.requestLogger.close();
402
803
  _ApitallyClient.instance = void 0;
403
804
  }
404
805
  getHubUrlPrefix() {
@@ -445,7 +846,8 @@ var _ApitallyClient = class _ApitallyClient {
445
846
  async sync() {
446
847
  try {
447
848
  const promises = [
448
- this.sendSyncData()
849
+ this.sendSyncData(),
850
+ this.sendLogData()
449
851
  ];
450
852
  if (!this.startupDataSent) {
451
853
  promises.push(this.sendStartupData());
@@ -473,7 +875,7 @@ var _ApitallyClient = class _ApitallyClient {
473
875
  this.logger.debug("Sending startup data to Apitally Hub");
474
876
  const payload = {
475
877
  instance_uuid: this.instanceUuid,
476
- message_uuid: randomUUID(),
878
+ message_uuid: randomUUID3(),
477
879
  ...this.startupData
478
880
  };
479
881
  try {
@@ -495,7 +897,7 @@ var _ApitallyClient = class _ApitallyClient {
495
897
  const newPayload = {
496
898
  time_offset: 0,
497
899
  instance_uuid: this.instanceUuid,
498
- message_uuid: randomUUID(),
900
+ message_uuid: randomUUID3(),
499
901
  requests: this.requestCounter.getAndResetRequests(),
500
902
  validation_errors: this.validationErrorCounter.getAndResetValidationErrors(),
501
903
  server_errors: this.serverErrorCounter.getAndResetServerErrors(),
@@ -514,8 +916,7 @@ var _ApitallyClient = class _ApitallyClient {
514
916
  const timeOffset = Date.now() - time;
515
917
  if (timeOffset <= MAX_QUEUE_TIME) {
516
918
  if (i > 0) {
517
- const waitMs = 100 + Math.random() * 200;
518
- await new Promise((resolve) => setTimeout(resolve, waitMs));
919
+ await this.randomDelay();
519
920
  }
520
921
  payload.time_offset = timeOffset / 1e3;
521
922
  await this.sendData("sync", payload);
@@ -534,6 +935,52 @@ var _ApitallyClient = class _ApitallyClient {
534
935
  }
535
936
  }
536
937
  }
938
+ async sendLogData() {
939
+ this.logger.debug("Sending request log data to Apitally Hub");
940
+ await this.requestLogger.rotateFile();
941
+ const fetchWithRetry = fetchRetry(fetch, {
942
+ retries: 3,
943
+ retryDelay: 1e3,
944
+ retryOn: [
945
+ 408,
946
+ 429,
947
+ 500,
948
+ 502,
949
+ 503,
950
+ 504
951
+ ]
952
+ });
953
+ let i = 0;
954
+ let logFile;
955
+ while (logFile = this.requestLogger.getFile()) {
956
+ if (i > 0) {
957
+ await this.randomDelay();
958
+ }
959
+ try {
960
+ const response = await fetchWithRetry(`${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`, {
961
+ method: "POST",
962
+ body: await logFile.getContent()
963
+ });
964
+ if (response.status === 402 && response.headers.has("Retry-After")) {
965
+ const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0");
966
+ if (retryAfter > 0) {
967
+ this.requestLogger.suspendUntil = Date.now() + retryAfter * 1e3;
968
+ this.requestLogger.clear();
969
+ return;
970
+ }
971
+ }
972
+ if (!response.ok) {
973
+ throw new HTTPError(response);
974
+ }
975
+ logFile.delete();
976
+ } catch (error) {
977
+ this.requestLogger.retryFileLater(logFile);
978
+ break;
979
+ }
980
+ i++;
981
+ if (i >= 10) break;
982
+ }
983
+ }
537
984
  handleHubError(error) {
538
985
  if (error instanceof HTTPError) {
539
986
  if (error.response.status === 404) {
@@ -548,6 +995,10 @@ var _ApitallyClient = class _ApitallyClient {
548
995
  }
549
996
  return false;
550
997
  }
998
+ async randomDelay() {
999
+ const delay = 100 + Math.random() * 200;
1000
+ await new Promise((resolve) => setTimeout(resolve, delay));
1001
+ }
551
1002
  };
552
1003
  __name(_ApitallyClient, "ApitallyClient");
553
1004
  __publicField(_ApitallyClient, "instance");
@@ -717,18 +1168,18 @@ var getMiddleware = /* @__PURE__ */ __name((app, client) => {
717
1168
  }
718
1169
  try {
719
1170
  const startTime = performance.now();
720
- const originalJson = res.json;
721
- res.json = (body) => {
1171
+ const originalSend = res.send;
1172
+ res.send = (body) => {
722
1173
  res.locals.body = body;
723
- return originalJson.call(res, body);
1174
+ return originalSend.call(res, body);
724
1175
  };
725
1176
  res.on("finish", () => {
726
1177
  try {
1178
+ const responseTime = performance.now() - startTime;
727
1179
  const path = getRoutePath(req);
1180
+ const consumer = getConsumer(req);
1181
+ client.consumerRegistry.addOrUpdateConsumer(consumer);
728
1182
  if (path) {
729
- const responseTime = performance.now() - startTime;
730
- const consumer = getConsumer(req);
731
- client.consumerRegistry.addOrUpdateConsumer(consumer);
732
1183
  client.requestCounter.addRequest({
733
1184
  consumer: consumer == null ? void 0 : consumer.identifier,
734
1185
  method: req.method,
@@ -739,24 +1190,31 @@ var getMiddleware = /* @__PURE__ */ __name((app, client) => {
739
1190
  responseSize: res.get("content-length")
740
1191
  });
741
1192
  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));
1193
+ let jsonBody;
1194
+ try {
1195
+ jsonBody = JSON.parse(res.locals.body);
1196
+ } catch {
748
1197
  }
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
1198
+ if (jsonBody) {
1199
+ const validationErrors = [];
1200
+ if (validationErrors.length === 0) {
1201
+ validationErrors.push(...extractExpressValidatorErrors(jsonBody));
1202
+ }
1203
+ if (validationErrors.length === 0) {
1204
+ validationErrors.push(...extractCelebrateErrors(jsonBody));
1205
+ }
1206
+ if (validationErrors.length === 0) {
1207
+ validationErrors.push(...extractNestValidationErrors(jsonBody));
1208
+ }
1209
+ validationErrors.forEach((error) => {
1210
+ client.validationErrorCounter.addValidationError({
1211
+ consumer: consumer == null ? void 0 : consumer.identifier,
1212
+ method: req.method,
1213
+ path: req.route.path,
1214
+ ...error
1215
+ });
758
1216
  });
759
- });
1217
+ }
760
1218
  }
761
1219
  if (res.statusCode === 500 && res.locals.serverError) {
762
1220
  const serverError = res.locals.serverError;
@@ -770,6 +1228,24 @@ var getMiddleware = /* @__PURE__ */ __name((app, client) => {
770
1228
  });
771
1229
  }
772
1230
  }
1231
+ if (client.requestLogger.enabled) {
1232
+ client.requestLogger.logRequest({
1233
+ timestamp: Date.now() / 1e3,
1234
+ method: req.method,
1235
+ path,
1236
+ url: `${req.protocol}://${req.host}${req.originalUrl}`,
1237
+ headers: convertHeaders(req.headers),
1238
+ size: Number(req.get("content-length")),
1239
+ consumer: consumer == null ? void 0 : consumer.identifier,
1240
+ body: convertBody(req.body, req.get("content-type"))
1241
+ }, {
1242
+ statusCode: res.statusCode,
1243
+ responseTime: responseTime / 1e3,
1244
+ headers: convertHeaders(res.getHeaders()),
1245
+ size: Number(res.get("content-length")),
1246
+ body: convertBody(res.locals.body, res.get("content-type"))
1247
+ });
1248
+ }
773
1249
  } catch (error) {
774
1250
  client.logger.error("Error while logging request in Apitally middleware.", {
775
1251
  request: req,