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
@@ -4,7 +4,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
 
6
6
  // src/common/client.ts
7
- import { randomUUID } from "crypto";
7
+ import { randomUUID as randomUUID3 } from "crypto";
8
8
  import fetchRetry from "fetch-retry";
9
9
 
10
10
  // src/common/consumerRegistry.ts
@@ -159,6 +159,338 @@ var _RequestCounter = class _RequestCounter {
159
159
  __name(_RequestCounter, "RequestCounter");
160
160
  var RequestCounter = _RequestCounter;
161
161
 
162
+ // src/common/requestLogger.ts
163
+ import { Buffer as Buffer3 } from "buffer";
164
+ import { randomUUID as randomUUID2 } from "crypto";
165
+ import { unlinkSync as unlinkSync2, writeFileSync } from "fs";
166
+ import { tmpdir as tmpdir2 } from "os";
167
+ import { join as join2 } from "path";
168
+
169
+ // src/common/tempGzipFile.ts
170
+ import { Buffer as Buffer2 } from "buffer";
171
+ import { randomUUID } from "crypto";
172
+ import { createWriteStream, readFile, unlinkSync } from "fs";
173
+ import { tmpdir } from "os";
174
+ import { join } from "path";
175
+ import { createGzip } from "zlib";
176
+ var _TempGzipFile = class _TempGzipFile {
177
+ uuid;
178
+ filePath;
179
+ gzip;
180
+ writeStream;
181
+ readyPromise;
182
+ constructor() {
183
+ this.uuid = randomUUID();
184
+ this.filePath = join(tmpdir(), `apitally-${this.uuid}.gz`);
185
+ this.writeStream = createWriteStream(this.filePath);
186
+ this.readyPromise = new Promise((resolve, reject) => {
187
+ this.writeStream.once("ready", resolve);
188
+ this.writeStream.once("error", reject);
189
+ });
190
+ this.gzip = createGzip();
191
+ this.gzip.pipe(this.writeStream);
192
+ }
193
+ get size() {
194
+ return this.writeStream.bytesWritten;
195
+ }
196
+ async writeLine(data) {
197
+ await this.readyPromise;
198
+ return new Promise((resolve, reject) => {
199
+ this.gzip.write(Buffer2.concat([
200
+ data,
201
+ Buffer2.from("\n")
202
+ ]), (error) => {
203
+ if (error) {
204
+ reject(error);
205
+ } else {
206
+ resolve();
207
+ }
208
+ });
209
+ });
210
+ }
211
+ async getContent() {
212
+ return new Promise((resolve, reject) => {
213
+ readFile(this.filePath, (error, data) => {
214
+ if (error) {
215
+ reject(error);
216
+ } else {
217
+ resolve(data);
218
+ }
219
+ });
220
+ });
221
+ }
222
+ async close() {
223
+ return new Promise((resolve) => {
224
+ this.gzip.end(() => {
225
+ resolve();
226
+ });
227
+ });
228
+ }
229
+ async delete() {
230
+ await this.close();
231
+ unlinkSync(this.filePath);
232
+ }
233
+ };
234
+ __name(_TempGzipFile, "TempGzipFile");
235
+ var TempGzipFile = _TempGzipFile;
236
+
237
+ // src/common/requestLogger.ts
238
+ var MAX_BODY_SIZE = 5e4;
239
+ var MAX_FILE_SIZE = 1e6;
240
+ var MAX_FILES = 50;
241
+ var MAX_PENDING_WRITES = 100;
242
+ var BODY_TOO_LARGE = Buffer3.from("<body too large>");
243
+ var BODY_MASKED = Buffer3.from("<masked>");
244
+ var MASKED = "******";
245
+ var ALLOWED_CONTENT_TYPES = [
246
+ "application/json",
247
+ "text/plain"
248
+ ];
249
+ var EXCLUDE_PATH_PATTERNS = [
250
+ /\/_?healthz?$/,
251
+ /\/_?health[_-]?checks?$/,
252
+ /\/_?heart[_-]?beats?$/,
253
+ /\/ping$/,
254
+ /\/ready$/,
255
+ /\/live$/
256
+ ];
257
+ var MASK_QUERY_PARAM_PATTERNS = [
258
+ /auth/i,
259
+ /api-?key/i,
260
+ /secret/i,
261
+ /token/i,
262
+ /password/i,
263
+ /pwd/i
264
+ ];
265
+ var MASK_HEADER_PATTERNS = [
266
+ /auth/i,
267
+ /api-?key/i,
268
+ /secret/i,
269
+ /token/i,
270
+ /cookie/i
271
+ ];
272
+ var DEFAULT_CONFIG = {
273
+ enabled: false,
274
+ logQueryParams: true,
275
+ logRequestHeaders: false,
276
+ logRequestBody: false,
277
+ logResponseHeaders: true,
278
+ logResponseBody: false,
279
+ maskQueryParams: [],
280
+ maskHeaders: [],
281
+ excludePaths: []
282
+ };
283
+ var _RequestLogger = class _RequestLogger {
284
+ config;
285
+ enabled;
286
+ suspendUntil = null;
287
+ pendingWrites = [];
288
+ currentFile = null;
289
+ files = [];
290
+ maintainIntervalId;
291
+ constructor(config) {
292
+ this.config = {
293
+ ...DEFAULT_CONFIG,
294
+ ...config
295
+ };
296
+ this.enabled = this.config.enabled && checkWritableFs();
297
+ if (this.enabled) {
298
+ this.maintainIntervalId = setInterval(() => {
299
+ this.maintain();
300
+ }, 1e3);
301
+ }
302
+ }
303
+ shouldExcludePath(urlPath) {
304
+ const patterns = [
305
+ ...this.config.excludePaths,
306
+ ...EXCLUDE_PATH_PATTERNS
307
+ ];
308
+ return matchPatterns(urlPath, patterns);
309
+ }
310
+ shouldMaskQueryParam(name) {
311
+ const patterns = [
312
+ ...this.config.maskQueryParams,
313
+ ...MASK_QUERY_PARAM_PATTERNS
314
+ ];
315
+ return matchPatterns(name, patterns);
316
+ }
317
+ shouldMaskHeader(name) {
318
+ const patterns = [
319
+ ...this.config.maskHeaders,
320
+ ...MASK_HEADER_PATTERNS
321
+ ];
322
+ return matchPatterns(name, patterns);
323
+ }
324
+ hasSupportedContentType(headers) {
325
+ var _a2;
326
+ const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
327
+ return contentType !== void 0 && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
328
+ }
329
+ maskQueryParams(search) {
330
+ const params = new URLSearchParams(search);
331
+ for (const [key] of params) {
332
+ if (this.shouldMaskQueryParam(key)) {
333
+ params.set(key, MASKED);
334
+ }
335
+ }
336
+ return params.toString();
337
+ }
338
+ maskHeaders(headers) {
339
+ return headers.map(([k, v]) => [
340
+ k,
341
+ this.shouldMaskHeader(k) ? MASKED : v
342
+ ]);
343
+ }
344
+ logRequest(request, response) {
345
+ var _a2, _b;
346
+ if (!this.enabled || this.suspendUntil !== null) return;
347
+ const url = new URL(request.url);
348
+ const path = request.path ?? url.pathname;
349
+ if (this.shouldExcludePath(path) || (((_b = (_a2 = this.config).excludeCallback) == null ? void 0 : _b.call(_a2, request, response)) ?? false)) {
350
+ return;
351
+ }
352
+ url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
353
+ request.url = url.toString();
354
+ request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
355
+ response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
356
+ if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
357
+ request.body = void 0;
358
+ } else if (request.body) {
359
+ if (request.body.length > MAX_BODY_SIZE) {
360
+ request.body = BODY_TOO_LARGE;
361
+ } else if (this.config.maskRequestBodyCallback) {
362
+ try {
363
+ request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
364
+ if (request.body.length > MAX_BODY_SIZE) {
365
+ request.body = BODY_TOO_LARGE;
366
+ }
367
+ } catch {
368
+ request.body = void 0;
369
+ }
370
+ }
371
+ }
372
+ if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
373
+ response.body = void 0;
374
+ } else if (response.body) {
375
+ if (response.body.length > MAX_BODY_SIZE) {
376
+ response.body = BODY_TOO_LARGE;
377
+ } else if (this.config.maskResponseBodyCallback) {
378
+ try {
379
+ response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
380
+ if (response.body.length > MAX_BODY_SIZE) {
381
+ response.body = BODY_TOO_LARGE;
382
+ }
383
+ } catch {
384
+ response.body = void 0;
385
+ }
386
+ }
387
+ }
388
+ const item = {
389
+ uuid: randomUUID2(),
390
+ request: skipEmptyValues(request),
391
+ response: skipEmptyValues(response)
392
+ };
393
+ [
394
+ item.request.body,
395
+ item.response.body
396
+ ].forEach((body) => {
397
+ if (body) {
398
+ body.toJSON = function() {
399
+ return this.toString("base64");
400
+ };
401
+ }
402
+ });
403
+ this.pendingWrites.push(JSON.stringify(item));
404
+ if (this.pendingWrites.length > MAX_PENDING_WRITES) {
405
+ this.pendingWrites.shift();
406
+ }
407
+ }
408
+ async writeToFile() {
409
+ if (!this.enabled || this.pendingWrites.length === 0) {
410
+ return;
411
+ }
412
+ if (!this.currentFile) {
413
+ this.currentFile = new TempGzipFile();
414
+ }
415
+ while (this.pendingWrites.length > 0) {
416
+ const item = this.pendingWrites.shift();
417
+ if (item) {
418
+ await this.currentFile.writeLine(Buffer3.from(item));
419
+ }
420
+ }
421
+ }
422
+ getFile() {
423
+ return this.files.shift();
424
+ }
425
+ retryFileLater(file) {
426
+ this.files.unshift(file);
427
+ }
428
+ async rotateFile() {
429
+ if (this.currentFile) {
430
+ await this.currentFile.close();
431
+ this.files.push(this.currentFile);
432
+ this.currentFile = null;
433
+ }
434
+ }
435
+ async maintain() {
436
+ await this.writeToFile();
437
+ if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {
438
+ await this.rotateFile();
439
+ }
440
+ while (this.files.length > MAX_FILES) {
441
+ const file = this.files.shift();
442
+ file == null ? void 0 : file.delete();
443
+ }
444
+ if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {
445
+ this.suspendUntil = null;
446
+ }
447
+ }
448
+ async clear() {
449
+ this.pendingWrites = [];
450
+ await this.rotateFile();
451
+ this.files.forEach((file) => {
452
+ file.delete();
453
+ });
454
+ this.files = [];
455
+ }
456
+ async close() {
457
+ this.enabled = false;
458
+ await this.clear();
459
+ if (this.maintainIntervalId) {
460
+ clearInterval(this.maintainIntervalId);
461
+ }
462
+ }
463
+ };
464
+ __name(_RequestLogger, "RequestLogger");
465
+ var RequestLogger = _RequestLogger;
466
+ function matchPatterns(value, patterns) {
467
+ return patterns.some((pattern) => {
468
+ return pattern.test(value);
469
+ });
470
+ }
471
+ __name(matchPatterns, "matchPatterns");
472
+ function skipEmptyValues(data) {
473
+ return Object.fromEntries(Object.entries(data).filter(([_, v]) => {
474
+ if (v == null || Number.isNaN(v)) return false;
475
+ if (Array.isArray(v) || Buffer3.isBuffer(v) || typeof v === "string") {
476
+ return v.length > 0;
477
+ }
478
+ return true;
479
+ }));
480
+ }
481
+ __name(skipEmptyValues, "skipEmptyValues");
482
+ function checkWritableFs() {
483
+ try {
484
+ const testPath = join2(tmpdir2(), `apitally-${randomUUID2()}`);
485
+ writeFileSync(testPath, "test");
486
+ unlinkSync2(testPath);
487
+ return true;
488
+ } catch (error) {
489
+ return false;
490
+ }
491
+ }
492
+ __name(checkWritableFs, "checkWritableFs");
493
+
162
494
  // src/common/serverErrorCounter.ts
163
495
  import { createHash } from "crypto";
164
496
  var MAX_MSG_LENGTH = 2048;
@@ -331,11 +663,12 @@ var _ApitallyClient = class _ApitallyClient {
331
663
  startupData;
332
664
  startupDataSent = false;
333
665
  requestCounter;
666
+ requestLogger;
334
667
  validationErrorCounter;
335
668
  serverErrorCounter;
336
669
  consumerRegistry;
337
670
  logger;
338
- constructor({ clientId, env = "dev", logger }) {
671
+ constructor({ clientId, env = "dev", requestLoggingConfig, logger }) {
339
672
  if (_ApitallyClient.instance) {
340
673
  throw new Error("Apitally client is already initialized");
341
674
  }
@@ -348,9 +681,10 @@ var _ApitallyClient = class _ApitallyClient {
348
681
  _ApitallyClient.instance = this;
349
682
  this.clientId = clientId;
350
683
  this.env = env;
351
- this.instanceUuid = randomUUID();
684
+ this.instanceUuid = randomUUID3();
352
685
  this.syncDataQueue = [];
353
686
  this.requestCounter = new RequestCounter();
687
+ this.requestLogger = new RequestLogger(requestLoggingConfig);
354
688
  this.validationErrorCounter = new ValidationErrorCounter();
355
689
  this.serverErrorCounter = new ServerErrorCounter();
356
690
  this.consumerRegistry = new ConsumerRegistry();
@@ -372,6 +706,8 @@ var _ApitallyClient = class _ApitallyClient {
372
706
  async handleShutdown() {
373
707
  this.stopSync();
374
708
  await this.sendSyncData();
709
+ await this.sendLogData();
710
+ await this.requestLogger.close();
375
711
  _ApitallyClient.instance = void 0;
376
712
  }
377
713
  getHubUrlPrefix() {
@@ -418,7 +754,8 @@ var _ApitallyClient = class _ApitallyClient {
418
754
  async sync() {
419
755
  try {
420
756
  const promises = [
421
- this.sendSyncData()
757
+ this.sendSyncData(),
758
+ this.sendLogData()
422
759
  ];
423
760
  if (!this.startupDataSent) {
424
761
  promises.push(this.sendStartupData());
@@ -446,7 +783,7 @@ var _ApitallyClient = class _ApitallyClient {
446
783
  this.logger.debug("Sending startup data to Apitally Hub");
447
784
  const payload = {
448
785
  instance_uuid: this.instanceUuid,
449
- message_uuid: randomUUID(),
786
+ message_uuid: randomUUID3(),
450
787
  ...this.startupData
451
788
  };
452
789
  try {
@@ -468,7 +805,7 @@ var _ApitallyClient = class _ApitallyClient {
468
805
  const newPayload = {
469
806
  time_offset: 0,
470
807
  instance_uuid: this.instanceUuid,
471
- message_uuid: randomUUID(),
808
+ message_uuid: randomUUID3(),
472
809
  requests: this.requestCounter.getAndResetRequests(),
473
810
  validation_errors: this.validationErrorCounter.getAndResetValidationErrors(),
474
811
  server_errors: this.serverErrorCounter.getAndResetServerErrors(),
@@ -487,8 +824,7 @@ var _ApitallyClient = class _ApitallyClient {
487
824
  const timeOffset = Date.now() - time;
488
825
  if (timeOffset <= MAX_QUEUE_TIME) {
489
826
  if (i > 0) {
490
- const waitMs = 100 + Math.random() * 200;
491
- await new Promise((resolve) => setTimeout(resolve, waitMs));
827
+ await this.randomDelay();
492
828
  }
493
829
  payload.time_offset = timeOffset / 1e3;
494
830
  await this.sendData("sync", payload);
@@ -507,6 +843,52 @@ var _ApitallyClient = class _ApitallyClient {
507
843
  }
508
844
  }
509
845
  }
846
+ async sendLogData() {
847
+ this.logger.debug("Sending request log data to Apitally Hub");
848
+ await this.requestLogger.rotateFile();
849
+ const fetchWithRetry = fetchRetry(fetch, {
850
+ retries: 3,
851
+ retryDelay: 1e3,
852
+ retryOn: [
853
+ 408,
854
+ 429,
855
+ 500,
856
+ 502,
857
+ 503,
858
+ 504
859
+ ]
860
+ });
861
+ let i = 0;
862
+ let logFile;
863
+ while (logFile = this.requestLogger.getFile()) {
864
+ if (i > 0) {
865
+ await this.randomDelay();
866
+ }
867
+ try {
868
+ const response = await fetchWithRetry(`${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`, {
869
+ method: "POST",
870
+ body: await logFile.getContent()
871
+ });
872
+ if (response.status === 402 && response.headers.has("Retry-After")) {
873
+ const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0");
874
+ if (retryAfter > 0) {
875
+ this.requestLogger.suspendUntil = Date.now() + retryAfter * 1e3;
876
+ this.requestLogger.clear();
877
+ return;
878
+ }
879
+ }
880
+ if (!response.ok) {
881
+ throw new HTTPError(response);
882
+ }
883
+ logFile.delete();
884
+ } catch (error) {
885
+ this.requestLogger.retryFileLater(logFile);
886
+ break;
887
+ }
888
+ i++;
889
+ if (i >= 10) break;
890
+ }
891
+ }
510
892
  handleHubError(error) {
511
893
  if (error instanceof HTTPError) {
512
894
  if (error.response.status === 404) {
@@ -521,6 +903,10 @@ var _ApitallyClient = class _ApitallyClient {
521
903
  }
522
904
  return false;
523
905
  }
906
+ async randomDelay() {
907
+ const delay = 100 + Math.random() * 200;
908
+ await new Promise((resolve) => setTimeout(resolve, delay));
909
+ }
524
910
  };
525
911
  __name(_ApitallyClient, "ApitallyClient");
526
912
  __publicField(_ApitallyClient, "instance");