apitally 0.11.4 → 0.12.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 (81) hide show
  1. package/README.md +27 -14
  2. package/dist/common/client.cjs +130 -110
  3. package/dist/common/client.cjs.map +1 -1
  4. package/dist/common/client.d.cts +2 -0
  5. package/dist/common/client.d.ts +2 -0
  6. package/dist/common/client.js +125 -105
  7. package/dist/common/client.js.map +1 -1
  8. package/dist/common/consumerRegistry.cjs.map +1 -1
  9. package/dist/common/consumerRegistry.js.map +1 -1
  10. package/dist/common/logging.cjs.map +1 -1
  11. package/dist/common/logging.js.map +1 -1
  12. package/dist/common/packageVersions.cjs.map +1 -1
  13. package/dist/common/packageVersions.js.map +1 -1
  14. package/dist/common/paramValidation.cjs.map +1 -1
  15. package/dist/common/paramValidation.js.map +1 -1
  16. package/dist/common/requestCounter.cjs.map +1 -1
  17. package/dist/common/requestCounter.js.map +1 -1
  18. package/dist/common/requestLogger.cjs +63 -7
  19. package/dist/common/requestLogger.cjs.map +1 -1
  20. package/dist/common/requestLogger.d.cts +3 -1
  21. package/dist/common/requestLogger.d.ts +3 -1
  22. package/dist/common/requestLogger.js +63 -7
  23. package/dist/common/requestLogger.js.map +1 -1
  24. package/dist/common/sentry.cjs +55 -0
  25. package/dist/common/sentry.cjs.map +1 -0
  26. package/dist/common/sentry.d.cts +6 -0
  27. package/dist/common/sentry.d.ts +6 -0
  28. package/dist/common/sentry.js +22 -0
  29. package/dist/common/sentry.js.map +1 -0
  30. package/dist/common/serverErrorCounter.cjs +58 -45
  31. package/dist/common/serverErrorCounter.cjs.map +1 -1
  32. package/dist/common/serverErrorCounter.d.cts +3 -6
  33. package/dist/common/serverErrorCounter.d.ts +3 -6
  34. package/dist/common/serverErrorCounter.js +53 -45
  35. package/dist/common/serverErrorCounter.js.map +1 -1
  36. package/dist/common/tempGzipFile.cjs +2 -2
  37. package/dist/common/tempGzipFile.cjs.map +1 -1
  38. package/dist/common/tempGzipFile.js +2 -2
  39. package/dist/common/tempGzipFile.js.map +1 -1
  40. package/dist/common/types.cjs.map +1 -1
  41. package/dist/common/validationErrorCounter.cjs.map +1 -1
  42. package/dist/common/validationErrorCounter.js.map +1 -1
  43. package/dist/express/index.cjs +139 -112
  44. package/dist/express/index.cjs.map +1 -1
  45. package/dist/express/index.js +134 -107
  46. package/dist/express/index.js.map +1 -1
  47. package/dist/express/middleware.cjs +139 -112
  48. package/dist/express/middleware.cjs.map +1 -1
  49. package/dist/express/middleware.js +134 -107
  50. package/dist/express/middleware.js.map +1 -1
  51. package/dist/express/utils.cjs.map +1 -1
  52. package/dist/express/utils.js.map +1 -1
  53. package/dist/fastify/index.cjs +136 -113
  54. package/dist/fastify/index.cjs.map +1 -1
  55. package/dist/fastify/index.js +131 -108
  56. package/dist/fastify/index.js.map +1 -1
  57. package/dist/fastify/plugin.cjs +136 -113
  58. package/dist/fastify/plugin.cjs.map +1 -1
  59. package/dist/fastify/plugin.js +131 -108
  60. package/dist/fastify/plugin.js.map +1 -1
  61. package/dist/hono/index.cjs +137 -112
  62. package/dist/hono/index.cjs.map +1 -1
  63. package/dist/hono/index.js +132 -107
  64. package/dist/hono/index.js.map +1 -1
  65. package/dist/hono/middleware.cjs +137 -112
  66. package/dist/hono/middleware.cjs.map +1 -1
  67. package/dist/hono/middleware.js +132 -107
  68. package/dist/hono/middleware.js.map +1 -1
  69. package/dist/koa/index.cjs +137 -111
  70. package/dist/koa/index.cjs.map +1 -1
  71. package/dist/koa/index.js +132 -106
  72. package/dist/koa/index.js.map +1 -1
  73. package/dist/koa/middleware.cjs +137 -111
  74. package/dist/koa/middleware.cjs.map +1 -1
  75. package/dist/koa/middleware.js +132 -106
  76. package/dist/koa/middleware.js.map +1 -1
  77. package/dist/nestjs/index.cjs +139 -112
  78. package/dist/nestjs/index.cjs.map +1 -1
  79. package/dist/nestjs/index.js +134 -107
  80. package/dist/nestjs/index.js.map +1 -1
  81. package/package.json +1 -1
package/README.md CHANGED
@@ -1,28 +1,29 @@
1
1
  <p align="center">
2
- <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
4
- <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
5
- <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
6
- </picture>
2
+ <a href="https://apitally.io" target="_blank">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
5
+ <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
6
+ <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
7
+ </picture>
8
+ </a>
7
9
  </p>
8
10
 
9
- <p align="center"><b>Analytics, logging & monitoring for REST APIs.</b></p>
11
+ <p align="center"><b>Simple, privacy-focused API monitoring & analytics</b></p>
10
12
 
11
- <p align="center"><i>Apitally helps you understand how your APIs are being used and alerts you when things go wrong.<br>It's super easy to use and designed to protect your data privacy.</i></p>
12
-
13
- <p align="center">🔗 <b><a href="https://apitally.io" target="_blank">apitally.io</a></b></p>
13
+ <p align="center"><i>Apitally helps you understand how your APIs are being used and alerts you when things go wrong.<br>Just add two lines of code to your project to get started.</i></p>
14
+ <br>
14
15
 
15
16
  ![Apitally screenshots](https://assets.apitally.io/screenshots/overview.png)
16
17
 
17
18
  ---
18
19
 
19
- # Apitally client library for Node.js
20
+ # Apitally SDK for Node.js
20
21
 
21
22
  [![Tests](https://github.com/apitally/apitally-js/actions/workflows/tests.yaml/badge.svg?event=push)](https://github.com/apitally/apitally-js/actions)
22
23
  [![Codecov](https://codecov.io/gh/apitally/apitally-js/graph/badge.svg?token=j5jqlrL7Pd)](https://codecov.io/gh/apitally/apitally-js)
23
24
  [![npm](https://img.shields.io/npm/v/apitally?logo=npm&color=%23cb0000)](https://www.npmjs.com/package/apitally)
24
25
 
25
- This client library for Apitally currently supports the following Node.js web
26
+ This SDK for Apitally currently supports the following Node.js web
26
27
  frameworks:
27
28
 
28
29
  - [Express](https://docs.apitally.io/frameworks/express)
@@ -36,9 +37,21 @@ the 📚 [documentation](https://docs.apitally.io).
36
37
 
37
38
  ## Key features
38
39
 
39
- - Middleware/plugins for different frameworks to capture metadata about API
40
- endpoints, requests and responses
41
- - Non-blocking client that aggregates and sends captured data to Apitally in regular intervals
40
+ ### API analytics
41
+
42
+ Track traffic, error and performance metrics for your API, each endpoint and individual API consumers, allowing you to make informed, data-driven engineering and product decisions.
43
+
44
+ ### Error tracking
45
+
46
+ Understand which validation rules in your endpoints cause client errors. Capture error details and stack traces for 500 error responses, and have them linked to Sentry issues automatically.
47
+
48
+ ### Request logging
49
+
50
+ Drill down from insights to individual requests or use powerful filtering to understand how consumers have interacted with your API. Configure exactly what is included in the logs to meet your requirements.
51
+
52
+ ### API monitoring & alerting
53
+
54
+ Get notified immediately if something isn't right using custom alerts, synthetic uptime checks and heartbeat monitoring. Notifications can be delivered via email, Slack or Microsoft Teams.
42
55
 
43
56
  ## Installation
44
57
 
@@ -194,14 +194,116 @@ var RequestCounter = _RequestCounter;
194
194
  // src/common/requestLogger.ts
195
195
  var import_async_lock = __toESM(require("async-lock"), 1);
196
196
  var import_buffer2 = require("buffer");
197
- var import_crypto2 = require("crypto");
197
+ var import_crypto3 = require("crypto");
198
198
  var import_fs2 = require("fs");
199
199
  var import_os2 = require("os");
200
200
  var import_path2 = require("path");
201
201
 
202
+ // src/common/sentry.ts
203
+ var sentry;
204
+ (async () => {
205
+ try {
206
+ sentry = await import("@sentry/node");
207
+ } catch (e) {
208
+ }
209
+ })();
210
+ function getSentryEventId() {
211
+ if (sentry && sentry.lastEventId) {
212
+ return sentry.lastEventId();
213
+ }
214
+ return void 0;
215
+ }
216
+ __name(getSentryEventId, "getSentryEventId");
217
+
218
+ // src/common/serverErrorCounter.ts
219
+ var import_crypto = require("crypto");
220
+ var MAX_MSG_LENGTH = 2048;
221
+ var MAX_STACKTRACE_LENGTH = 65536;
222
+ var _ServerErrorCounter = class _ServerErrorCounter {
223
+ errorCounts;
224
+ errorDetails;
225
+ sentryEventIds;
226
+ constructor() {
227
+ this.errorCounts = /* @__PURE__ */ new Map();
228
+ this.errorDetails = /* @__PURE__ */ new Map();
229
+ this.sentryEventIds = /* @__PURE__ */ new Map();
230
+ }
231
+ addServerError(serverError) {
232
+ const key = this.getKey(serverError);
233
+ if (!this.errorDetails.has(key)) {
234
+ this.errorDetails.set(key, serverError);
235
+ }
236
+ this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);
237
+ const sentryEventId = getSentryEventId();
238
+ if (sentryEventId) {
239
+ this.sentryEventIds.set(key, sentryEventId);
240
+ }
241
+ }
242
+ getAndResetServerErrors() {
243
+ const data = [];
244
+ this.errorCounts.forEach((count, key) => {
245
+ const serverError = this.errorDetails.get(key);
246
+ if (serverError) {
247
+ data.push({
248
+ consumer: serverError.consumer || null,
249
+ method: serverError.method,
250
+ path: serverError.path,
251
+ type: serverError.type,
252
+ msg: truncateExceptionMessage(serverError.msg),
253
+ traceback: truncateExceptionStackTrace(serverError.traceback),
254
+ sentry_event_id: this.sentryEventIds.get(key) || null,
255
+ error_count: count
256
+ });
257
+ }
258
+ });
259
+ this.errorCounts.clear();
260
+ this.errorDetails.clear();
261
+ return data;
262
+ }
263
+ getKey(serverError) {
264
+ const hashInput = [
265
+ serverError.consumer || "",
266
+ serverError.method.toUpperCase(),
267
+ serverError.path,
268
+ serverError.type,
269
+ serverError.msg.trim(),
270
+ serverError.traceback.trim()
271
+ ].join("|");
272
+ return (0, import_crypto.createHash)("md5").update(hashInput).digest("hex");
273
+ }
274
+ };
275
+ __name(_ServerErrorCounter, "ServerErrorCounter");
276
+ var ServerErrorCounter = _ServerErrorCounter;
277
+ function truncateExceptionMessage(msg) {
278
+ if (msg.length <= MAX_MSG_LENGTH) {
279
+ return msg;
280
+ }
281
+ const suffix = "... (truncated)";
282
+ const cutoff = MAX_MSG_LENGTH - suffix.length;
283
+ return msg.substring(0, cutoff) + suffix;
284
+ }
285
+ __name(truncateExceptionMessage, "truncateExceptionMessage");
286
+ function truncateExceptionStackTrace(stack) {
287
+ const suffix = "... (truncated) ...";
288
+ const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;
289
+ const lines = stack.trim().split("\n");
290
+ const truncatedLines = [];
291
+ let length = 0;
292
+ for (const line of lines) {
293
+ if (length + line.length + 1 > cutoff) {
294
+ truncatedLines.push(suffix);
295
+ break;
296
+ }
297
+ truncatedLines.push(line);
298
+ length += line.length + 1;
299
+ }
300
+ return truncatedLines.join("\n");
301
+ }
302
+ __name(truncateExceptionStackTrace, "truncateExceptionStackTrace");
303
+
202
304
  // src/common/tempGzipFile.ts
203
305
  var import_buffer = require("buffer");
204
- var import_crypto = require("crypto");
306
+ var import_crypto2 = require("crypto");
205
307
  var import_fs = require("fs");
206
308
  var import_os = require("os");
207
309
  var import_path = require("path");
@@ -214,16 +316,16 @@ var _TempGzipFile = class _TempGzipFile {
214
316
  readyPromise;
215
317
  closedPromise;
216
318
  constructor() {
217
- this.uuid = (0, import_crypto.randomUUID)();
319
+ this.uuid = (0, import_crypto2.randomUUID)();
218
320
  this.filePath = (0, import_path.join)((0, import_os.tmpdir)(), `apitally-${this.uuid}.gz`);
219
321
  this.writeStream = (0, import_fs.createWriteStream)(this.filePath);
220
322
  this.readyPromise = new Promise((resolve, reject) => {
221
323
  this.writeStream.once("ready", resolve);
222
324
  this.writeStream.once("error", reject);
223
325
  });
224
- this.closedPromise = new Promise((resolve) => {
326
+ this.closedPromise = new Promise((resolve, reject) => {
225
327
  this.writeStream.once("close", resolve);
226
- this.writeStream.once("error", resolve);
328
+ this.writeStream.once("error", reject);
227
329
  });
228
330
  this.gzip = (0, import_zlib.createGzip)();
229
331
  this.gzip.pipe(this.writeStream);
@@ -321,6 +423,7 @@ var DEFAULT_CONFIG = {
321
423
  logRequestBody: false,
322
424
  logResponseHeaders: true,
323
425
  logResponseBody: false,
426
+ logException: true,
324
427
  maskQueryParams: [],
325
428
  maskHeaders: [],
326
429
  excludePaths: []
@@ -373,7 +476,10 @@ var _RequestLogger = class _RequestLogger {
373
476
  hasSupportedContentType(headers) {
374
477
  var _a2;
375
478
  const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
376
- return contentType !== void 0 && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
479
+ return this.isSupportedContentType(contentType);
480
+ }
481
+ isSupportedContentType(contentType) {
482
+ return typeof contentType === "string" && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
377
483
  }
378
484
  maskQueryParams(search) {
379
485
  const params = new URLSearchParams(search);
@@ -390,7 +496,7 @@ var _RequestLogger = class _RequestLogger {
390
496
  this.shouldMaskHeader(k) ? MASKED : v
391
497
  ]);
392
498
  }
393
- logRequest(request, response) {
499
+ logRequest(request, response, error) {
394
500
  var _a2, _b, _c;
395
501
  if (!this.enabled || this.suspendUntil !== null) return;
396
502
  const url = new URL(request.url);
@@ -401,8 +507,6 @@ var _RequestLogger = class _RequestLogger {
401
507
  }
402
508
  url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
403
509
  request.url = url.toString();
404
- request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
405
- response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
406
510
  if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
407
511
  request.body = void 0;
408
512
  } else if (request.body) {
@@ -435,10 +539,18 @@ var _RequestLogger = class _RequestLogger {
435
539
  }
436
540
  }
437
541
  }
542
+ request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
543
+ response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
438
544
  const item = {
439
- uuid: (0, import_crypto2.randomUUID)(),
545
+ uuid: (0, import_crypto3.randomUUID)(),
440
546
  request: skipEmptyValues(request),
441
- response: skipEmptyValues(response)
547
+ response: skipEmptyValues(response),
548
+ exception: error && this.config.logException ? {
549
+ type: error.name,
550
+ message: truncateExceptionMessage(error.message),
551
+ stacktrace: truncateExceptionStackTrace(error.stack || ""),
552
+ sentryEventId: getSentryEventId()
553
+ } : null
442
554
  };
443
555
  [
444
556
  item.request.body,
@@ -535,7 +647,7 @@ function skipEmptyValues(data) {
535
647
  __name(skipEmptyValues, "skipEmptyValues");
536
648
  function checkWritableFs() {
537
649
  try {
538
- const testPath = (0, import_path2.join)((0, import_os2.tmpdir)(), `apitally-${(0, import_crypto2.randomUUID)()}`);
650
+ const testPath = (0, import_path2.join)((0, import_os2.tmpdir)(), `apitally-${(0, import_crypto3.randomUUID)()}`);
539
651
  (0, import_fs2.writeFileSync)(testPath, "test");
540
652
  (0, import_fs2.unlinkSync)(testPath);
541
653
  return true;
@@ -545,104 +657,6 @@ function checkWritableFs() {
545
657
  }
546
658
  __name(checkWritableFs, "checkWritableFs");
547
659
 
548
- // src/common/serverErrorCounter.ts
549
- var import_crypto3 = require("crypto");
550
- var MAX_MSG_LENGTH = 2048;
551
- var MAX_STACKTRACE_LENGTH = 65536;
552
- var _ServerErrorCounter = class _ServerErrorCounter {
553
- errorCounts;
554
- errorDetails;
555
- sentryEventIds;
556
- sentry;
557
- constructor() {
558
- this.errorCounts = /* @__PURE__ */ new Map();
559
- this.errorDetails = /* @__PURE__ */ new Map();
560
- this.sentryEventIds = /* @__PURE__ */ new Map();
561
- this.tryImportSentry();
562
- }
563
- addServerError(serverError) {
564
- const key = this.getKey(serverError);
565
- if (!this.errorDetails.has(key)) {
566
- this.errorDetails.set(key, serverError);
567
- }
568
- this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);
569
- this.captureSentryEventId(key);
570
- }
571
- getAndResetServerErrors() {
572
- const data = [];
573
- this.errorCounts.forEach((count, key) => {
574
- const serverError = this.errorDetails.get(key);
575
- if (serverError) {
576
- data.push({
577
- consumer: serverError.consumer || null,
578
- method: serverError.method,
579
- path: serverError.path,
580
- type: serverError.type,
581
- msg: this.getTruncatedMessage(serverError.msg),
582
- traceback: this.getTruncatedStack(serverError.traceback),
583
- sentry_event_id: this.sentryEventIds.get(key) || null,
584
- error_count: count
585
- });
586
- }
587
- });
588
- this.errorCounts.clear();
589
- this.errorDetails.clear();
590
- return data;
591
- }
592
- getKey(serverError) {
593
- const hashInput = [
594
- serverError.consumer || "",
595
- serverError.method.toUpperCase(),
596
- serverError.path,
597
- serverError.type,
598
- serverError.msg.trim(),
599
- serverError.traceback.trim()
600
- ].join("|");
601
- return (0, import_crypto3.createHash)("md5").update(hashInput).digest("hex");
602
- }
603
- getTruncatedMessage(msg) {
604
- msg = msg.trim();
605
- if (msg.length <= MAX_MSG_LENGTH) {
606
- return msg;
607
- }
608
- const suffix = "... (truncated)";
609
- const cutoff = MAX_MSG_LENGTH - suffix.length;
610
- return msg.substring(0, cutoff) + suffix;
611
- }
612
- getTruncatedStack(stack) {
613
- const suffix = "... (truncated) ...";
614
- const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;
615
- const lines = stack.trim().split("\n");
616
- const truncatedLines = [];
617
- let length = 0;
618
- for (const line of lines) {
619
- if (length + line.length + 1 > cutoff) {
620
- truncatedLines.push(suffix);
621
- break;
622
- }
623
- truncatedLines.push(line);
624
- length += line.length + 1;
625
- }
626
- return truncatedLines.join("\n");
627
- }
628
- captureSentryEventId(serverErrorKey) {
629
- if (this.sentry && this.sentry.lastEventId) {
630
- const eventId = this.sentry.lastEventId();
631
- if (eventId) {
632
- this.sentryEventIds.set(serverErrorKey, eventId);
633
- }
634
- }
635
- }
636
- async tryImportSentry() {
637
- try {
638
- this.sentry = await import("@sentry/node");
639
- } catch (e) {
640
- }
641
- }
642
- };
643
- __name(_ServerErrorCounter, "ServerErrorCounter");
644
- var ServerErrorCounter = _ServerErrorCounter;
645
-
646
660
  // src/common/validationErrorCounter.ts
647
661
  var import_crypto4 = require("crypto");
648
662
  var _ValidationErrorCounter = class _ValidationErrorCounter {
@@ -716,6 +730,7 @@ var _ApitallyClient = class _ApitallyClient {
716
730
  syncIntervalId;
717
731
  startupData;
718
732
  startupDataSent = false;
733
+ enabled = true;
719
734
  requestCounter;
720
735
  requestLogger;
721
736
  validationErrorCounter;
@@ -752,12 +767,16 @@ var _ApitallyClient = class _ApitallyClient {
752
767
  }
753
768
  return _ApitallyClient.instance;
754
769
  }
770
+ isEnabled() {
771
+ return this.enabled;
772
+ }
755
773
  static async shutdown() {
756
774
  if (_ApitallyClient.instance) {
757
775
  await _ApitallyClient.instance.handleShutdown();
758
776
  }
759
777
  }
760
778
  async handleShutdown() {
779
+ this.enabled = false;
761
780
  this.stopSync();
762
781
  await this.sendSyncData();
763
782
  await this.sendLogData();
@@ -941,6 +960,7 @@ var _ApitallyClient = class _ApitallyClient {
941
960
  if (error instanceof HTTPError) {
942
961
  if (error.response.status === 404) {
943
962
  this.logger.error(`Invalid Apitally client ID: '${this.clientId}'`);
963
+ this.enabled = false;
944
964
  this.stopSync();
945
965
  return true;
946
966
  }