@unito/integration-sdk 5.0.0 → 5.1.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.
@@ -103,6 +103,7 @@ export class Handler {
103
103
  filters: res.locals.filters,
104
104
  logger: res.locals.logger,
105
105
  signal: res.locals.signal,
106
+ requestMetrics: res.locals.requestMetrics,
106
107
  params: req.params,
107
108
  query: req.query,
108
109
  relations: res.locals.relations,
@@ -124,6 +125,7 @@ export class Handler {
124
125
  body: req.body,
125
126
  logger: res.locals.logger,
126
127
  signal: res.locals.signal,
128
+ requestMetrics: res.locals.requestMetrics,
127
129
  params: req.params,
128
130
  query: req.query,
129
131
  });
@@ -164,6 +166,7 @@ export class Handler {
164
166
  body,
165
167
  logger: res.locals.logger,
166
168
  signal: res.locals.signal,
169
+ requestMetrics: res.locals.requestMetrics,
167
170
  params: req.params,
168
171
  query: req.query,
169
172
  });
@@ -196,6 +199,7 @@ export class Handler {
196
199
  secrets: res.locals.secrets,
197
200
  logger: res.locals.logger,
198
201
  signal: res.locals.signal,
202
+ requestMetrics: res.locals.requestMetrics,
199
203
  params: req.params,
200
204
  query: req.query,
201
205
  });
@@ -216,6 +220,7 @@ export class Handler {
216
220
  body: req.body,
217
221
  logger: res.locals.logger,
218
222
  signal: res.locals.signal,
223
+ requestMetrics: res.locals.requestMetrics,
219
224
  params: req.params,
220
225
  query: req.query,
221
226
  });
@@ -234,6 +239,7 @@ export class Handler {
234
239
  secrets: res.locals.secrets,
235
240
  logger: res.locals.logger,
236
241
  signal: res.locals.signal,
242
+ requestMetrics: res.locals.requestMetrics,
237
243
  params: req.params,
238
244
  query: req.query,
239
245
  });
@@ -252,6 +258,7 @@ export class Handler {
252
258
  secrets: res.locals.secrets,
253
259
  logger: res.locals.logger,
254
260
  signal: res.locals.signal,
261
+ requestMetrics: res.locals.requestMetrics,
255
262
  params: req.params,
256
263
  query: req.query,
257
264
  });
@@ -287,6 +294,7 @@ export class Handler {
287
294
  secrets: res.locals.secrets,
288
295
  logger: res.locals.logger,
289
296
  signal: res.locals.signal,
297
+ requestMetrics: res.locals.requestMetrics,
290
298
  params: req.params,
291
299
  query: req.query,
292
300
  });
@@ -302,6 +310,7 @@ export class Handler {
302
310
  secrets: res.locals.secrets,
303
311
  logger: res.locals.logger,
304
312
  signal: res.locals.signal,
313
+ requestMetrics: res.locals.requestMetrics,
305
314
  params: req.params,
306
315
  query: req.query,
307
316
  body: req.body,
@@ -318,6 +327,7 @@ export class Handler {
318
327
  secrets: res.locals.secrets,
319
328
  logger: res.locals.logger,
320
329
  signal: res.locals.signal,
330
+ requestMetrics: res.locals.requestMetrics,
321
331
  params: req.params,
322
332
  query: req.query,
323
333
  body: req.body,
@@ -339,6 +349,7 @@ export class Handler {
339
349
  body: req.body,
340
350
  logger: res.locals.logger,
341
351
  signal: res.locals.signal,
352
+ requestMetrics: res.locals.requestMetrics,
342
353
  params: req.params,
343
354
  query: req.query,
344
355
  });
@@ -552,6 +552,7 @@ class Handler {
552
552
  filters: res.locals.filters,
553
553
  logger: res.locals.logger,
554
554
  signal: res.locals.signal,
555
+ requestMetrics: res.locals.requestMetrics,
555
556
  params: req.params,
556
557
  query: req.query,
557
558
  relations: res.locals.relations,
@@ -573,6 +574,7 @@ class Handler {
573
574
  body: req.body,
574
575
  logger: res.locals.logger,
575
576
  signal: res.locals.signal,
577
+ requestMetrics: res.locals.requestMetrics,
576
578
  params: req.params,
577
579
  query: req.query,
578
580
  });
@@ -613,6 +615,7 @@ class Handler {
613
615
  body,
614
616
  logger: res.locals.logger,
615
617
  signal: res.locals.signal,
618
+ requestMetrics: res.locals.requestMetrics,
616
619
  params: req.params,
617
620
  query: req.query,
618
621
  });
@@ -645,6 +648,7 @@ class Handler {
645
648
  secrets: res.locals.secrets,
646
649
  logger: res.locals.logger,
647
650
  signal: res.locals.signal,
651
+ requestMetrics: res.locals.requestMetrics,
648
652
  params: req.params,
649
653
  query: req.query,
650
654
  });
@@ -665,6 +669,7 @@ class Handler {
665
669
  body: req.body,
666
670
  logger: res.locals.logger,
667
671
  signal: res.locals.signal,
672
+ requestMetrics: res.locals.requestMetrics,
668
673
  params: req.params,
669
674
  query: req.query,
670
675
  });
@@ -683,6 +688,7 @@ class Handler {
683
688
  secrets: res.locals.secrets,
684
689
  logger: res.locals.logger,
685
690
  signal: res.locals.signal,
691
+ requestMetrics: res.locals.requestMetrics,
686
692
  params: req.params,
687
693
  query: req.query,
688
694
  });
@@ -701,6 +707,7 @@ class Handler {
701
707
  secrets: res.locals.secrets,
702
708
  logger: res.locals.logger,
703
709
  signal: res.locals.signal,
710
+ requestMetrics: res.locals.requestMetrics,
704
711
  params: req.params,
705
712
  query: req.query,
706
713
  });
@@ -736,6 +743,7 @@ class Handler {
736
743
  secrets: res.locals.secrets,
737
744
  logger: res.locals.logger,
738
745
  signal: res.locals.signal,
746
+ requestMetrics: res.locals.requestMetrics,
739
747
  params: req.params,
740
748
  query: req.query,
741
749
  });
@@ -751,6 +759,7 @@ class Handler {
751
759
  secrets: res.locals.secrets,
752
760
  logger: res.locals.logger,
753
761
  signal: res.locals.signal,
762
+ requestMetrics: res.locals.requestMetrics,
754
763
  params: req.params,
755
764
  query: req.query,
756
765
  body: req.body,
@@ -767,6 +776,7 @@ class Handler {
767
776
  secrets: res.locals.secrets,
768
777
  logger: res.locals.logger,
769
778
  signal: res.locals.signal,
779
+ requestMetrics: res.locals.requestMetrics,
770
780
  params: req.params,
771
781
  query: req.query,
772
782
  body: req.body,
@@ -788,6 +798,7 @@ class Handler {
788
798
  body: req.body,
789
799
  logger: res.locals.logger,
790
800
  signal: res.locals.signal,
801
+ requestMetrics: res.locals.requestMetrics,
791
802
  params: req.params,
792
803
  query: req.query,
793
804
  });
@@ -887,11 +898,14 @@ function onFinish(req, res, next) {
887
898
  res.on('finish', function () {
888
899
  const logger = res.locals.logger ?? new Logger();
889
900
  const error = res.locals.error;
890
- const durationInNs = Number(process.hrtime.bigint() - res.locals.requestStartTime);
901
+ const endMetrics = res.locals.requestMetrics.endRequest();
902
+ const durationInNs = Number(endMetrics.durationNs);
891
903
  const durationInMs = (durationInNs / 1_000_000) | 0;
892
904
  const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
893
905
  const metadata = {
894
906
  duration: durationInNs,
907
+ externalApiCount: endMetrics.apiCallCount,
908
+ externalApiTotalDuration: endMetrics.totalApiDurationNs,
895
909
  // Use reserved and standard attributes of Datadog
896
910
  // https://app.datadoghq.com/logs/pipelines/standard-attributes
897
911
  http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
@@ -944,8 +958,42 @@ function injectLogger(req, res, next) {
944
958
  next();
945
959
  }
946
960
 
961
+ /**
962
+ * Accumulates per-request metrics for Provider API calls.
963
+ *
964
+ * Created once per incoming request (in the start middleware), threaded through Context and RequestOptions,
965
+ * incremented by Provider on each API call, and logged by the finish middleware.
966
+ */
967
+ class RequestMetrics {
968
+ _apiCallCount = 0;
969
+ _totalApiDurationNs = 0;
970
+ _requestStartTime;
971
+ constructor() {
972
+ this._requestStartTime = process.hrtime.bigint();
973
+ }
974
+ static startRequest() {
975
+ return new RequestMetrics();
976
+ }
977
+ endRequest() {
978
+ return {
979
+ durationNs: process.hrtime.bigint() - this._requestStartTime,
980
+ apiCallCount: this._apiCallCount,
981
+ totalApiDurationNs: this._totalApiDurationNs,
982
+ };
983
+ }
984
+ /**
985
+ * Record a completed API call.
986
+ * Computes the duration from the given start timestamp, increments the call counter,
987
+ * and accumulates the duration.
988
+ */
989
+ recordExternalApiCall(startTime) {
990
+ this._apiCallCount++;
991
+ this._totalApiDurationNs += Number(process.hrtime.bigint() - startTime);
992
+ }
993
+ }
994
+
947
995
  function start(_, res, next) {
948
- res.locals.requestStartTime = process.hrtime.bigint();
996
+ res.locals.requestMetrics = RequestMetrics.startRequest();
949
997
  next();
950
998
  }
951
999
 
@@ -1352,7 +1400,7 @@ class Provider {
1352
1400
  }
1353
1401
  });
1354
1402
  };
1355
- return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
1403
+ return this.timedCallToProvider(callToProvider, options);
1356
1404
  }
1357
1405
  /**
1358
1406
  * Performs a POST request to the provider streaming a Readable directly without loading it into memory.
@@ -1462,7 +1510,7 @@ class Provider {
1462
1510
  }
1463
1511
  });
1464
1512
  };
1465
- return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
1513
+ return this.timedCallToProvider(callToProvider, options);
1466
1514
  }
1467
1515
  /**
1468
1516
  * Performs a PUT request to the provider.
@@ -1702,7 +1750,7 @@ class Provider {
1702
1750
  }
1703
1751
  return { status: response.status, headers: Object.fromEntries(response.headers.entries()), body };
1704
1752
  };
1705
- return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
1753
+ return this.timedCallToProvider(callToProvider, options);
1706
1754
  }
1707
1755
  /**
1708
1756
  * Normalizes Node.js IncomingHttpHeaders (which may have undefined or string[] values)
@@ -1713,6 +1761,18 @@ class Provider {
1713
1761
  .filter((entry) => entry[1] !== undefined)
1714
1762
  .map(([key, value]) => [key, Array.isArray(value) ? value.join(',') : value]));
1715
1763
  }
1764
+ async timedCallToProvider(callToProvider, options) {
1765
+ const timedCall = async () => {
1766
+ const start = process.hrtime.bigint();
1767
+ try {
1768
+ return await callToProvider();
1769
+ }
1770
+ finally {
1771
+ options.requestMetrics?.recordExternalApiCall(start);
1772
+ }
1773
+ };
1774
+ return this.rateLimiter ? this.rateLimiter(options, timedCall) : timedCall();
1775
+ }
1716
1776
  handleError(responseStatus, message, options) {
1717
1777
  const customError = this.customErrorHandler?.(responseStatus, message, options);
1718
1778
  return customError ?? buildHttpError(responseStatus, message);
@@ -1775,5 +1835,6 @@ exports.HttpErrors = httpErrors;
1775
1835
  exports.Integration = Integration;
1776
1836
  exports.NULL_LOGGER = NULL_LOGGER;
1777
1837
  exports.Provider = Provider;
1838
+ exports.RequestMetrics = RequestMetrics;
1778
1839
  exports.buildCollectionQueryParams = buildCollectionQueryParams;
1779
1840
  exports.getApplicableFilters = getApplicableFilters;
@@ -3,6 +3,7 @@ export { Cache, type CacheInstance } from './resources/cache.js';
3
3
  export { default as Integration } from './integration.js';
4
4
  export * from './handler.js';
5
5
  export { Provider, type Response as ProviderResponse, type RequestOptions as ProviderRequestOptions, type RateLimiter, } from './resources/provider.js';
6
+ export { RequestMetrics } from './resources/requestMetrics.js';
6
7
  export type { Secrets } from './middlewares/secrets.js';
7
8
  export type { Credentials } from './middlewares/credentials.js';
8
9
  export type { Filter } from './middlewares/filters.js';
package/dist/src/index.js CHANGED
@@ -4,6 +4,7 @@ export { Cache } from './resources/cache.js';
4
4
  export { default as Integration } from './integration.js';
5
5
  export * from './handler.js';
6
6
  export { Provider, } from './resources/provider.js';
7
+ export { RequestMetrics } from './resources/requestMetrics.js';
7
8
  export * as HttpErrors from './httpErrors.js';
8
9
  export { buildCollectionQueryParams, getApplicableFilters } from './helpers.js';
9
10
  export * from './resources/context.js';
@@ -6,7 +6,6 @@ declare global {
6
6
  interface Locals {
7
7
  logger: Logger;
8
8
  error?: ApiError;
9
- requestStartTime: bigint;
10
9
  }
11
10
  }
12
11
  }
@@ -3,11 +3,14 @@ function onFinish(req, res, next) {
3
3
  res.on('finish', function () {
4
4
  const logger = res.locals.logger ?? new Logger();
5
5
  const error = res.locals.error;
6
- const durationInNs = Number(process.hrtime.bigint() - res.locals.requestStartTime);
6
+ const endMetrics = res.locals.requestMetrics.endRequest();
7
+ const durationInNs = Number(endMetrics.durationNs);
7
8
  const durationInMs = (durationInNs / 1_000_000) | 0;
8
9
  const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
9
10
  const metadata = {
10
11
  duration: durationInNs,
12
+ externalApiCount: endMetrics.apiCallCount,
13
+ externalApiTotalDuration: endMetrics.totalApiDurationNs,
11
14
  // Use reserved and standard attributes of Datadog
12
15
  // https://app.datadoghq.com/logs/pipelines/standard-attributes
13
16
  http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
@@ -1,8 +1,9 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
+ import { RequestMetrics } from '../resources/requestMetrics.js';
2
3
  declare global {
3
4
  namespace Express {
4
5
  interface Locals {
5
- requestStartTime: bigint;
6
+ requestMetrics: RequestMetrics;
6
7
  }
7
8
  }
8
9
  }
@@ -1,5 +1,6 @@
1
+ import { RequestMetrics } from '../resources/requestMetrics.js';
1
2
  function start(_, res, next) {
2
- res.locals.requestStartTime = process.hrtime.bigint();
3
+ res.locals.requestMetrics = RequestMetrics.startRequest();
3
4
  next();
4
5
  }
5
6
  export default start;
@@ -1,5 +1,6 @@
1
1
  import * as API from '@unito/integration-api';
2
2
  import Logger from './logger.js';
3
+ import { RequestMetrics } from './requestMetrics.js';
3
4
  import { Credentials } from '../middlewares/credentials.js';
4
5
  import { Secrets } from '../middlewares/secrets.js';
5
6
  import { Filter } from '../middlewares/filters.js';
@@ -40,6 +41,11 @@ export type Context<P extends Maybe<Params> = Maybe<Params>, Q extends Maybe<Que
40
41
  * to be timed out. You can use this signal to abort any operation that would exceed this time frame.
41
42
  */
42
43
  signal: AbortSignal | undefined;
44
+ /**
45
+ * Per-request metrics accumulator for Provider API calls.
46
+ * Automatically populated by the SDK middleware and used by Provider to track call count and duration.
47
+ */
48
+ requestMetrics: RequestMetrics | undefined;
43
49
  /**
44
50
  * The request params.
45
51
  *
@@ -3,6 +3,7 @@ import * as stream from 'stream';
3
3
  import * as HttpErrors from '../httpErrors.js';
4
4
  import { Credentials } from '../middlewares/credentials.js';
5
5
  import Logger from '../resources/logger.js';
6
+ import { RequestMetrics } from './requestMetrics.js';
6
7
  /**
7
8
  * RateLimiter is a wrapper function that you can provide to limit the rate of calls to the provider based on the
8
9
  * caller's credentials.
@@ -42,6 +43,7 @@ export type RequestOptions = {
42
43
  [key: string]: string;
43
44
  };
44
45
  rawBody?: boolean;
46
+ requestMetrics?: RequestMetrics;
45
47
  };
46
48
  /**
47
49
  * Response object returned by the Provider's method.
@@ -226,5 +228,6 @@ export declare class Provider {
226
228
  * into a flat Record<string, string> by joining array values and dropping undefined entries.
227
229
  */
228
230
  private static normalizeNodeHeaders;
231
+ private timedCallToProvider;
229
232
  private handleError;
230
233
  }
@@ -181,7 +181,7 @@ export class Provider {
181
181
  }
182
182
  });
183
183
  };
184
- return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
184
+ return this.timedCallToProvider(callToProvider, options);
185
185
  }
186
186
  /**
187
187
  * Performs a POST request to the provider streaming a Readable directly without loading it into memory.
@@ -291,7 +291,7 @@ export class Provider {
291
291
  }
292
292
  });
293
293
  };
294
- return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
294
+ return this.timedCallToProvider(callToProvider, options);
295
295
  }
296
296
  /**
297
297
  * Performs a PUT request to the provider.
@@ -531,7 +531,7 @@ export class Provider {
531
531
  }
532
532
  return { status: response.status, headers: Object.fromEntries(response.headers.entries()), body };
533
533
  };
534
- return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
534
+ return this.timedCallToProvider(callToProvider, options);
535
535
  }
536
536
  /**
537
537
  * Normalizes Node.js IncomingHttpHeaders (which may have undefined or string[] values)
@@ -542,6 +542,18 @@ export class Provider {
542
542
  .filter((entry) => entry[1] !== undefined)
543
543
  .map(([key, value]) => [key, Array.isArray(value) ? value.join(',') : value]));
544
544
  }
545
+ async timedCallToProvider(callToProvider, options) {
546
+ const timedCall = async () => {
547
+ const start = process.hrtime.bigint();
548
+ try {
549
+ return await callToProvider();
550
+ }
551
+ finally {
552
+ options.requestMetrics?.recordExternalApiCall(start);
553
+ }
554
+ };
555
+ return this.rateLimiter ? this.rateLimiter(options, timedCall) : timedCall();
556
+ }
545
557
  handleError(responseStatus, message, options) {
546
558
  const customError = this.customErrorHandler?.(responseStatus, message, options);
547
559
  return customError ?? buildHttpError(responseStatus, message);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Accumulates per-request metrics for Provider API calls.
3
+ *
4
+ * Created once per incoming request (in the start middleware), threaded through Context and RequestOptions,
5
+ * incremented by Provider on each API call, and logged by the finish middleware.
6
+ */
7
+ export declare class RequestMetrics {
8
+ private _apiCallCount;
9
+ private _totalApiDurationNs;
10
+ private _requestStartTime;
11
+ private constructor();
12
+ static startRequest(): RequestMetrics;
13
+ endRequest(): {
14
+ durationNs: bigint;
15
+ apiCallCount: number;
16
+ totalApiDurationNs: number;
17
+ };
18
+ /**
19
+ * Record a completed API call.
20
+ * Computes the duration from the given start timestamp, increments the call counter,
21
+ * and accumulates the duration.
22
+ */
23
+ recordExternalApiCall(startTime: bigint): void;
24
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Accumulates per-request metrics for Provider API calls.
3
+ *
4
+ * Created once per incoming request (in the start middleware), threaded through Context and RequestOptions,
5
+ * incremented by Provider on each API call, and logged by the finish middleware.
6
+ */
7
+ export class RequestMetrics {
8
+ _apiCallCount = 0;
9
+ _totalApiDurationNs = 0;
10
+ _requestStartTime;
11
+ constructor() {
12
+ this._requestStartTime = process.hrtime.bigint();
13
+ }
14
+ static startRequest() {
15
+ return new RequestMetrics();
16
+ }
17
+ endRequest() {
18
+ return {
19
+ durationNs: process.hrtime.bigint() - this._requestStartTime,
20
+ apiCallCount: this._apiCallCount,
21
+ totalApiDurationNs: this._totalApiDurationNs,
22
+ };
23
+ }
24
+ /**
25
+ * Record a completed API call.
26
+ * Computes the duration from the given start timestamp, increments the call counter,
27
+ * and accumulates the duration.
28
+ */
29
+ recordExternalApiCall(startTime) {
30
+ this._apiCallCount++;
31
+ this._totalApiDurationNs += Number(process.hrtime.bigint() - startTime);
32
+ }
33
+ }
@@ -1,6 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
3
  import onFinish from '../../src/middlewares/finish.js';
4
+ import { RequestMetrics } from '../../src/resources/requestMetrics.js';
4
5
  describe('finish middleware', () => {
5
6
  it('logs info', () => {
6
7
  let expected = '';
@@ -11,7 +12,7 @@ describe('finish middleware', () => {
11
12
  eventHandler = func;
12
13
  },
13
14
  locals: {
14
- requestStartTime: 0n,
15
+ requestMetrics: RequestMetrics.startRequest(),
15
16
  logger: {
16
17
  info: (_message) => {
17
18
  expected = 'works!';
@@ -33,7 +34,7 @@ describe('finish middleware', () => {
33
34
  eventHandler = func;
34
35
  },
35
36
  locals: {
36
- requestStartTime: 0n,
37
+ requestMetrics: RequestMetrics.startRequest(),
37
38
  logger: {
38
39
  error: (_message) => {
39
40
  expected = 'works!';
@@ -55,7 +56,7 @@ describe('finish middleware', () => {
55
56
  eventHandler = func;
56
57
  },
57
58
  locals: {
58
- requestStartTime: 0n,
59
+ requestMetrics: RequestMetrics.startRequest(),
59
60
  logger: {
60
61
  info: (_message) => {
61
62
  expected = 'ohoh!';
@@ -77,7 +78,7 @@ describe('finish middleware', () => {
77
78
  eventHandler = func;
78
79
  },
79
80
  locals: {
80
- requestStartTime: 0n,
81
+ requestMetrics: RequestMetrics.startRequest(),
81
82
  logger: {
82
83
  error: (_message) => {
83
84
  expected = 'works!';
@@ -1,11 +1,22 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
3
  import start from '../../src/middlewares/start.js';
4
+ import { RequestMetrics } from '../../src/resources/requestMetrics.js';
4
5
  describe('start middleware', () => {
5
- it('adds request start time to locals', () => {
6
+ it('initializes request metrics on locals', () => {
6
7
  const request = {};
7
8
  const response = { locals: {} };
8
9
  start(request, response, () => { });
9
- assert.ok(response.locals.requestStartTime);
10
+ assert.ok(response.locals.requestMetrics instanceof RequestMetrics);
11
+ });
12
+ it('records request start time in metrics', () => {
13
+ const request = {};
14
+ const response = { locals: {} };
15
+ const before = process.hrtime.bigint();
16
+ start(request, response, () => { });
17
+ const result = response.locals.requestMetrics.endRequest();
18
+ const after = process.hrtime.bigint();
19
+ assert.ok(result.durationNs >= 0n);
20
+ assert.ok(result.durationNs <= after - before);
10
21
  });
11
22
  });
@@ -7,6 +7,7 @@ import https from 'https';
7
7
  import { Provider } from '../../src/resources/provider.js';
8
8
  import * as HttpErrors from '../../src/httpErrors.js';
9
9
  import Logger from '../../src/resources/logger.js';
10
+ import { RequestMetrics } from '../../src/resources/requestMetrics.js';
10
11
  // There is currently an issue with node 20.12 and fetch mocking. A quick fix is to first call fetch so it's getter
11
12
  // get properly instantiated, which allow it to be mocked properly.
12
13
  // Issue: https://github.com/nodejs/node/issues/52015
@@ -1336,4 +1337,98 @@ describe('Provider', () => {
1336
1337
  }
1337
1338
  });
1338
1339
  });
1340
+ describe('request metrics', () => {
1341
+ it('records api call duration in requestMetrics via fetchWrapper', async (context) => {
1342
+ const metrics = RequestMetrics.startRequest();
1343
+ const response = new Response('{"data": "value"}', {
1344
+ status: 200,
1345
+ headers: new Headers({ 'Content-Type': 'application/json' }),
1346
+ });
1347
+ context.mock.method(global, 'fetch', () => Promise.resolve(response));
1348
+ await provider.get('/endpoint', {
1349
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1350
+ logger,
1351
+ requestMetrics: metrics,
1352
+ });
1353
+ const result = metrics.endRequest();
1354
+ assert.equal(result.apiCallCount, 1);
1355
+ assert.ok(result.totalApiDurationNs >= 0, 'Duration should be recorded');
1356
+ });
1357
+ it('records multiple api calls in the same requestMetrics', async (context) => {
1358
+ const metrics = RequestMetrics.startRequest();
1359
+ context.mock.method(global, 'fetch', () => Promise.resolve(new Response('{"data": "value"}', {
1360
+ status: 200,
1361
+ headers: new Headers({ 'Content-Type': 'application/json' }),
1362
+ })));
1363
+ await provider.get('/endpoint1', {
1364
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1365
+ logger,
1366
+ requestMetrics: metrics,
1367
+ });
1368
+ await provider.post('/endpoint2', { key: 'value' }, {
1369
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1370
+ logger,
1371
+ requestMetrics: metrics,
1372
+ });
1373
+ const result = metrics.endRequest();
1374
+ assert.equal(result.apiCallCount, 2);
1375
+ assert.ok(result.totalApiDurationNs >= 0);
1376
+ });
1377
+ it('works without requestMetrics (backward compatible)', async (context) => {
1378
+ const response = new Response('{"data": "value"}', {
1379
+ status: 200,
1380
+ headers: new Headers({ 'Content-Type': 'application/json' }),
1381
+ });
1382
+ context.mock.method(global, 'fetch', () => Promise.resolve(response));
1383
+ // No requestMetrics passed — should not throw
1384
+ const result = await provider.get('/endpoint', {
1385
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1386
+ logger,
1387
+ });
1388
+ assert.equal(result.status, 200);
1389
+ });
1390
+ it('records api call duration from postForm', async () => {
1391
+ const metrics = RequestMetrics.startRequest();
1392
+ const httpsProvider = new Provider({
1393
+ prepareRequest: () => ({
1394
+ url: 'https://www.myApi.com',
1395
+ headers: {},
1396
+ }),
1397
+ });
1398
+ const scope = nock('https://www.myApi.com').post('/upload').reply(201, { success: true });
1399
+ const FormData = (await import('form-data')).default;
1400
+ const form = new FormData();
1401
+ form.append('file', Buffer.from('test data'), 'test.txt');
1402
+ await httpsProvider.postForm('/upload', form, {
1403
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1404
+ logger,
1405
+ requestMetrics: metrics,
1406
+ });
1407
+ const postFormResult = metrics.endRequest();
1408
+ assert.equal(postFormResult.apiCallCount, 1);
1409
+ assert.ok(postFormResult.totalApiDurationNs > 0);
1410
+ scope.isDone();
1411
+ });
1412
+ it('records api call duration from postStream', async () => {
1413
+ const metrics = RequestMetrics.startRequest();
1414
+ const testData = 'binary stream data';
1415
+ const httpsProvider = new Provider({
1416
+ prepareRequest: () => ({
1417
+ url: 'https://www.myApi.com',
1418
+ headers: {},
1419
+ }),
1420
+ });
1421
+ const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
1422
+ const stream = Readable.from(Buffer.from(testData));
1423
+ await httpsProvider.postStream('/upload', stream, {
1424
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1425
+ logger,
1426
+ requestMetrics: metrics,
1427
+ });
1428
+ const postStreamResult = metrics.endRequest();
1429
+ assert.equal(postStreamResult.apiCallCount, 1);
1430
+ assert.ok(postStreamResult.totalApiDurationNs > 0);
1431
+ scope.isDone();
1432
+ });
1433
+ });
1339
1434
  });