@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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { RequestMetrics } from '../../src/resources/requestMetrics.js';
4
+ describe('RequestMetrics', () => {
5
+ describe('initial state', () => {
6
+ it('starts with zero api call count', () => {
7
+ const metrics = RequestMetrics.startRequest();
8
+ const result = metrics.endRequest();
9
+ assert.equal(result.apiCallCount, 0);
10
+ });
11
+ it('starts with zero total api duration', () => {
12
+ const metrics = RequestMetrics.startRequest();
13
+ const result = metrics.endRequest();
14
+ assert.equal(result.totalApiDurationNs, 0);
15
+ });
16
+ });
17
+ describe('endRequest', () => {
18
+ it('returns elapsed duration since construction', () => {
19
+ const before = process.hrtime.bigint();
20
+ const metrics = RequestMetrics.startRequest();
21
+ const result = metrics.endRequest();
22
+ const after = process.hrtime.bigint();
23
+ assert.ok(result.durationNs >= 0n);
24
+ assert.ok(result.durationNs <= after - before);
25
+ });
26
+ });
27
+ describe('recordExternalApiCall', () => {
28
+ it('increments api call count', () => {
29
+ const metrics = RequestMetrics.startRequest();
30
+ const start1 = process.hrtime.bigint();
31
+ metrics.recordExternalApiCall(start1);
32
+ const start2 = process.hrtime.bigint();
33
+ metrics.recordExternalApiCall(start2);
34
+ const result = metrics.endRequest();
35
+ assert.equal(result.apiCallCount, 2);
36
+ });
37
+ it('accumulates api duration', () => {
38
+ const metrics = RequestMetrics.startRequest();
39
+ const start = process.hrtime.bigint();
40
+ metrics.recordExternalApiCall(start);
41
+ const result = metrics.endRequest();
42
+ assert.ok(result.totalApiDurationNs >= 0);
43
+ });
44
+ });
45
+ });
@@ -1 +1 @@
1
- {"root":["../src/errors.ts","../src/handler.ts","../src/helpers.ts","../src/httpErrors.ts","../src/index.ts","../src/integration.ts","../src/middlewares/correlationId.ts","../src/middlewares/credentials.ts","../src/middlewares/errors.ts","../src/middlewares/filters.ts","../src/middlewares/finish.ts","../src/middlewares/health.ts","../src/middlewares/logger.ts","../src/middlewares/notFound.ts","../src/middlewares/relations.ts","../src/middlewares/search.ts","../src/middlewares/secrets.ts","../src/middlewares/selects.ts","../src/middlewares/signal.ts","../src/middlewares/start.ts","../src/resources/cache.ts","../src/resources/context.ts","../src/resources/logger.ts","../src/resources/provider.ts","../test/errors.test.ts","../test/handler.test.ts","../test/helpers.test.ts","../test/integration.test.ts","../test/middlewares/correlationId.test.ts","../test/middlewares/credentials.test.ts","../test/middlewares/errors.test.ts","../test/middlewares/filters.test.ts","../test/middlewares/finish.test.ts","../test/middlewares/health.test.ts","../test/middlewares/logger.test.ts","../test/middlewares/notFound.test.ts","../test/middlewares/relations.test.ts","../test/middlewares/search.test.ts","../test/middlewares/secrets.test.ts","../test/middlewares/selects.test.ts","../test/middlewares/signal.test.ts","../test/middlewares/start.test.ts","../test/resources/cache.test.ts","../test/resources/logger.test.ts","../test/resources/provider.test.ts"],"version":"5.9.3"}
1
+ {"root":["../src/errors.ts","../src/handler.ts","../src/helpers.ts","../src/httpErrors.ts","../src/index.ts","../src/integration.ts","../src/middlewares/correlationId.ts","../src/middlewares/credentials.ts","../src/middlewares/errors.ts","../src/middlewares/filters.ts","../src/middlewares/finish.ts","../src/middlewares/health.ts","../src/middlewares/logger.ts","../src/middlewares/notFound.ts","../src/middlewares/relations.ts","../src/middlewares/search.ts","../src/middlewares/secrets.ts","../src/middlewares/selects.ts","../src/middlewares/signal.ts","../src/middlewares/start.ts","../src/resources/cache.ts","../src/resources/context.ts","../src/resources/logger.ts","../src/resources/provider.ts","../src/resources/requestMetrics.ts","../test/errors.test.ts","../test/handler.test.ts","../test/helpers.test.ts","../test/integration.test.ts","../test/middlewares/correlationId.test.ts","../test/middlewares/credentials.test.ts","../test/middlewares/errors.test.ts","../test/middlewares/filters.test.ts","../test/middlewares/finish.test.ts","../test/middlewares/health.test.ts","../test/middlewares/logger.test.ts","../test/middlewares/notFound.test.ts","../test/middlewares/relations.test.ts","../test/middlewares/search.test.ts","../test/middlewares/secrets.test.ts","../test/middlewares/selects.test.ts","../test/middlewares/signal.test.ts","../test/middlewares/start.test.ts","../test/resources/cache.test.ts","../test/resources/logger.test.ts","../test/resources/provider.test.ts","../test/resources/requestMetrics.test.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
package/src/handler.ts CHANGED
@@ -308,6 +308,7 @@ export class Handler {
308
308
  filters: res.locals.filters,
309
309
  logger: res.locals.logger,
310
310
  signal: res.locals.signal,
311
+ requestMetrics: res.locals.requestMetrics,
311
312
  params: req.params,
312
313
  query: req.query,
313
314
  relations: res.locals.relations,
@@ -335,6 +336,7 @@ export class Handler {
335
336
  body: req.body,
336
337
  logger: res.locals.logger,
337
338
  signal: res.locals.signal,
339
+ requestMetrics: res.locals.requestMetrics,
338
340
  params: req.params,
339
341
  query: req.query,
340
342
  });
@@ -383,6 +385,7 @@ export class Handler {
383
385
  body,
384
386
  logger: res.locals.logger,
385
387
  signal: res.locals.signal,
388
+ requestMetrics: res.locals.requestMetrics,
386
389
  params: req.params,
387
390
  query: req.query,
388
391
  });
@@ -417,6 +420,7 @@ export class Handler {
417
420
  secrets: res.locals.secrets,
418
421
  logger: res.locals.logger,
419
422
  signal: res.locals.signal,
423
+ requestMetrics: res.locals.requestMetrics,
420
424
  params: req.params,
421
425
  query: req.query,
422
426
  });
@@ -443,6 +447,7 @@ export class Handler {
443
447
  body: req.body,
444
448
  logger: res.locals.logger,
445
449
  signal: res.locals.signal,
450
+ requestMetrics: res.locals.requestMetrics,
446
451
  params: req.params,
447
452
  query: req.query,
448
453
  });
@@ -466,6 +471,7 @@ export class Handler {
466
471
  secrets: res.locals.secrets,
467
472
  logger: res.locals.logger,
468
473
  signal: res.locals.signal,
474
+ requestMetrics: res.locals.requestMetrics,
469
475
  params: req.params,
470
476
  query: req.query,
471
477
  });
@@ -489,6 +495,7 @@ export class Handler {
489
495
  secrets: res.locals.secrets,
490
496
  logger: res.locals.logger,
491
497
  signal: res.locals.signal,
498
+ requestMetrics: res.locals.requestMetrics,
492
499
  params: req.params,
493
500
  query: req.query,
494
501
  });
@@ -533,6 +540,7 @@ export class Handler {
533
540
  secrets: res.locals.secrets,
534
541
  logger: res.locals.logger,
535
542
  signal: res.locals.signal,
543
+ requestMetrics: res.locals.requestMetrics,
536
544
  params: req.params,
537
545
  query: req.query,
538
546
  });
@@ -553,6 +561,7 @@ export class Handler {
553
561
  secrets: res.locals.secrets,
554
562
  logger: res.locals.logger,
555
563
  signal: res.locals.signal,
564
+ requestMetrics: res.locals.requestMetrics,
556
565
  params: req.params,
557
566
  query: req.query,
558
567
  body: req.body,
@@ -574,6 +583,7 @@ export class Handler {
574
583
  secrets: res.locals.secrets,
575
584
  logger: res.locals.logger,
576
585
  signal: res.locals.signal,
586
+ requestMetrics: res.locals.requestMetrics,
577
587
  params: req.params,
578
588
  query: req.query,
579
589
  body: req.body,
@@ -601,6 +611,7 @@ export class Handler {
601
611
  body: req.body,
602
612
  logger: res.locals.logger,
603
613
  signal: res.locals.signal,
614
+ requestMetrics: res.locals.requestMetrics,
604
615
  params: req.params,
605
616
  query: req.query,
606
617
  });
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export {
10
10
  type RequestOptions as ProviderRequestOptions,
11
11
  type RateLimiter,
12
12
  } from './resources/provider.js';
13
+ export { RequestMetrics } from './resources/requestMetrics.js';
13
14
  export type { Secrets } from './middlewares/secrets.js';
14
15
  export type { Credentials } from './middlewares/credentials.js';
15
16
  export type { Filter } from './middlewares/filters.js';
@@ -8,7 +8,6 @@ declare global {
8
8
  interface Locals {
9
9
  logger: Logger;
10
10
  error?: ApiError;
11
- requestStartTime: bigint;
12
11
  }
13
12
  }
14
13
  }
@@ -18,12 +17,15 @@ function onFinish(req: Request, res: Response, next: NextFunction) {
18
17
  const logger = res.locals.logger ?? new Logger();
19
18
  const error = res.locals.error;
20
19
 
21
- const durationInNs = Number(process.hrtime.bigint() - res.locals.requestStartTime);
20
+ const endMetrics = res.locals.requestMetrics.endRequest();
21
+ const durationInNs = Number(endMetrics.durationNs);
22
22
  const durationInMs = (durationInNs / 1_000_000) | 0;
23
23
 
24
24
  const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
25
25
  const metadata: Metadata = {
26
26
  duration: durationInNs,
27
+ externalApiCount: endMetrics.apiCallCount,
28
+ externalApiTotalDuration: endMetrics.totalApiDurationNs,
27
29
  // Use reserved and standard attributes of Datadog
28
30
  // https://app.datadoghq.com/logs/pipelines/standard-attributes
29
31
  http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
@@ -1,16 +1,17 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
+ import { RequestMetrics } from '../resources/requestMetrics.js';
2
3
 
3
4
  declare global {
4
5
  // eslint-disable-next-line @typescript-eslint/no-namespace
5
6
  namespace Express {
6
7
  interface Locals {
7
- requestStartTime: bigint;
8
+ requestMetrics: RequestMetrics;
8
9
  }
9
10
  }
10
11
  }
11
12
 
12
13
  function start(_: Request, res: Response, next: NextFunction) {
13
- res.locals.requestStartTime = process.hrtime.bigint();
14
+ res.locals.requestMetrics = RequestMetrics.startRequest();
14
15
 
15
16
  next();
16
17
  }
@@ -1,6 +1,7 @@
1
1
  /* c8 ignore start */
2
2
  import * as API from '@unito/integration-api';
3
3
  import Logger from './logger.js';
4
+ import { RequestMetrics } from './requestMetrics.js';
4
5
  import { Credentials } from '../middlewares/credentials.js';
5
6
  import { Secrets } from '../middlewares/secrets.js';
6
7
  import { Filter } from '../middlewares/filters.js';
@@ -43,6 +44,11 @@ export type Context<P extends Maybe<Params> = Maybe<Params>, Q extends Maybe<Que
43
44
  * to be timed out. You can use this signal to abort any operation that would exceed this time frame.
44
45
  */
45
46
  signal: AbortSignal | undefined;
47
+ /**
48
+ * Per-request metrics accumulator for Provider API calls.
49
+ * Automatically populated by the SDK middleware and used by Provider to track call count and duration.
50
+ */
51
+ requestMetrics: RequestMetrics | undefined;
46
52
  /**
47
53
  * The request params.
48
54
  *
@@ -7,6 +7,7 @@ import { buildHttpError } from '../errors.js';
7
7
  import * as HttpErrors from '../httpErrors.js';
8
8
  import { Credentials } from '../middlewares/credentials.js';
9
9
  import Logger from '../resources/logger.js';
10
+ import { RequestMetrics } from './requestMetrics.js';
10
11
 
11
12
  /**
12
13
  * RateLimiter is a wrapper function that you can provide to limit the rate of calls to the provider based on the
@@ -44,6 +45,7 @@ export type RequestOptions = {
44
45
  queryParams?: { [key: string]: string };
45
46
  additionnalheaders?: { [key: string]: string };
46
47
  rawBody?: boolean;
48
+ requestMetrics?: RequestMetrics;
47
49
  };
48
50
 
49
51
  /**
@@ -278,7 +280,7 @@ export class Provider {
278
280
  });
279
281
  };
280
282
 
281
- return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
283
+ return this.timedCallToProvider(callToProvider, options);
282
284
  }
283
285
 
284
286
  /**
@@ -406,7 +408,7 @@ export class Provider {
406
408
  });
407
409
  };
408
410
 
409
- return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
411
+ return this.timedCallToProvider(callToProvider, options);
410
412
  }
411
413
 
412
414
  /**
@@ -687,7 +689,7 @@ export class Provider {
687
689
  return { status: response.status, headers: Object.fromEntries(response.headers.entries()), body };
688
690
  };
689
691
 
690
- return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
692
+ return this.timedCallToProvider(callToProvider, options);
691
693
  }
692
694
 
693
695
  /**
@@ -702,6 +704,21 @@ export class Provider {
702
704
  );
703
705
  }
704
706
 
707
+ private async timedCallToProvider<T>(
708
+ callToProvider: () => Promise<Response<T>>,
709
+ options: RequestOptions,
710
+ ): Promise<Response<T>> {
711
+ const timedCall = async (): Promise<Response<T>> => {
712
+ const start = process.hrtime.bigint();
713
+ try {
714
+ return await callToProvider();
715
+ } finally {
716
+ options.requestMetrics?.recordExternalApiCall(start);
717
+ }
718
+ };
719
+ return this.rateLimiter ? this.rateLimiter(options, timedCall) : timedCall();
720
+ }
721
+
705
722
  private handleError(responseStatus: number, message: string, options: RequestOptions): HttpErrors.HttpError {
706
723
  const customError = this.customErrorHandler?.(responseStatus, message, options);
707
724
 
@@ -0,0 +1,37 @@
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
+ private _apiCallCount = 0;
9
+ private _totalApiDurationNs = 0;
10
+ private _requestStartTime: bigint;
11
+
12
+ private constructor() {
13
+ this._requestStartTime = process.hrtime.bigint();
14
+ }
15
+
16
+ static startRequest(): RequestMetrics {
17
+ return new RequestMetrics();
18
+ }
19
+
20
+ endRequest(): { durationNs: bigint; apiCallCount: number; totalApiDurationNs: number } {
21
+ return {
22
+ durationNs: process.hrtime.bigint() - this._requestStartTime,
23
+ apiCallCount: this._apiCallCount,
24
+ totalApiDurationNs: this._totalApiDurationNs,
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Record a completed API call.
30
+ * Computes the duration from the given start timestamp, increments the call counter,
31
+ * and accumulates the duration.
32
+ */
33
+ recordExternalApiCall(startTime: bigint): void {
34
+ this._apiCallCount++;
35
+ this._totalApiDurationNs += Number(process.hrtime.bigint() - startTime);
36
+ }
37
+ }
@@ -2,6 +2,7 @@ import express from 'express';
2
2
  import assert from 'node:assert/strict';
3
3
  import { describe, it } from 'node:test';
4
4
  import onFinish from '../../src/middlewares/finish.js';
5
+ import { RequestMetrics } from '../../src/resources/requestMetrics.js';
5
6
 
6
7
  describe('finish middleware', () => {
7
8
  it('logs info', () => {
@@ -10,11 +11,11 @@ describe('finish middleware', () => {
10
11
 
11
12
  const request = { originalUrl: '/' } as express.Request;
12
13
  const response = {
13
- on: (_event, func: () => void) => {
14
+ on: (_event: string, func: () => void) => {
14
15
  eventHandler = func;
15
16
  },
16
17
  locals: {
17
- requestStartTime: 0n,
18
+ requestMetrics: RequestMetrics.startRequest(),
18
19
  logger: {
19
20
  info: (_message: string) => {
20
21
  expected = 'works!';
@@ -22,7 +23,7 @@ describe('finish middleware', () => {
22
23
  },
23
24
  },
24
25
  statusCode: 200,
25
- } as express.Response;
26
+ } as unknown as express.Response;
26
27
 
27
28
  onFinish(request, response, () => {});
28
29
  eventHandler();
@@ -36,11 +37,11 @@ describe('finish middleware', () => {
36
37
 
37
38
  const request = { originalUrl: '/' } as express.Request;
38
39
  const response = {
39
- on: (_event, func: () => void) => {
40
+ on: (_event: string, func: () => void) => {
40
41
  eventHandler = func;
41
42
  },
42
43
  locals: {
43
- requestStartTime: 0n,
44
+ requestMetrics: RequestMetrics.startRequest(),
44
45
  logger: {
45
46
  error: (_message: string) => {
46
47
  expected = 'works!';
@@ -48,7 +49,7 @@ describe('finish middleware', () => {
48
49
  },
49
50
  },
50
51
  statusCode: 500,
51
- } as express.Response;
52
+ } as unknown as express.Response;
52
53
 
53
54
  onFinish(request, response, () => {});
54
55
  eventHandler();
@@ -62,11 +63,11 @@ describe('finish middleware', () => {
62
63
 
63
64
  const request = { originalUrl: '/health' } as express.Request;
64
65
  const response = {
65
- on: (_event, func: () => void) => {
66
+ on: (_event: string, func: () => void) => {
66
67
  eventHandler = func;
67
68
  },
68
69
  locals: {
69
- requestStartTime: 0n,
70
+ requestMetrics: RequestMetrics.startRequest(),
70
71
  logger: {
71
72
  info: (_message: string) => {
72
73
  expected = 'ohoh!';
@@ -74,7 +75,7 @@ describe('finish middleware', () => {
74
75
  },
75
76
  },
76
77
  statusCode: 200,
77
- } as express.Response;
78
+ } as unknown as express.Response;
78
79
 
79
80
  onFinish(request, response, () => {});
80
81
  eventHandler();
@@ -88,11 +89,11 @@ describe('finish middleware', () => {
88
89
 
89
90
  const request = { originalUrl: '/health' } as express.Request;
90
91
  const response = {
91
- on: (_event, func: () => void) => {
92
+ on: (_event: string, func: () => void) => {
92
93
  eventHandler = func;
93
94
  },
94
95
  locals: {
95
- requestStartTime: 0n,
96
+ requestMetrics: RequestMetrics.startRequest(),
96
97
  logger: {
97
98
  error: (_message: string) => {
98
99
  expected = 'works!';
@@ -100,7 +101,7 @@ describe('finish middleware', () => {
100
101
  },
101
102
  },
102
103
  statusCode: 500,
103
- } as express.Response;
104
+ } as unknown as express.Response;
104
105
 
105
106
  onFinish(request, response, () => {});
106
107
  eventHandler();
@@ -2,13 +2,24 @@ import express from 'express';
2
2
  import assert from 'node:assert/strict';
3
3
  import { describe, it } from 'node:test';
4
4
  import start from '../../src/middlewares/start.js';
5
+ import { RequestMetrics } from '../../src/resources/requestMetrics.js';
5
6
 
6
7
  describe('start middleware', () => {
7
- it('adds request start time to locals', () => {
8
+ it('initializes request metrics on locals', () => {
8
9
  const request = {} as express.Request;
9
10
  const response = { locals: {} } as express.Response;
11
+ start(request, response, () => {});
12
+ assert.ok(response.locals.requestMetrics instanceof RequestMetrics);
13
+ });
10
14
 
15
+ it('records request start time in metrics', () => {
16
+ const request = {} as express.Request;
17
+ const response = { locals: {} } as express.Response;
18
+ const before = process.hrtime.bigint();
11
19
  start(request, response, () => {});
12
- assert.ok(response.locals.requestStartTime);
20
+ const result = response.locals.requestMetrics.endRequest();
21
+ const after = process.hrtime.bigint();
22
+ assert.ok(result.durationNs >= 0n);
23
+ assert.ok(result.durationNs <= after - before);
13
24
  });
14
25
  });
@@ -8,6 +8,7 @@ import https from 'https';
8
8
  import { Provider } from '../../src/resources/provider.js';
9
9
  import * as HttpErrors from '../../src/httpErrors.js';
10
10
  import Logger from '../../src/resources/logger.js';
11
+ import { RequestMetrics } from '../../src/resources/requestMetrics.js';
11
12
 
12
13
  // There is currently an issue with node 20.12 and fetch mocking. A quick fix is to first call fetch so it's getter
13
14
  // get properly instantiated, which allow it to be mocked properly.
@@ -1586,4 +1587,129 @@ describe('Provider', () => {
1586
1587
  }
1587
1588
  });
1588
1589
  });
1590
+
1591
+ describe('request metrics', () => {
1592
+ it('records api call duration in requestMetrics via fetchWrapper', async context => {
1593
+ const metrics = RequestMetrics.startRequest();
1594
+ const response = new Response('{"data": "value"}', {
1595
+ status: 200,
1596
+ headers: new Headers({ 'Content-Type': 'application/json' }),
1597
+ });
1598
+
1599
+ context.mock.method(global, 'fetch', () => Promise.resolve(response));
1600
+
1601
+ await provider.get('/endpoint', {
1602
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1603
+ logger,
1604
+ requestMetrics: metrics,
1605
+ });
1606
+
1607
+ const result = metrics.endRequest();
1608
+ assert.equal(result.apiCallCount, 1);
1609
+ assert.ok(result.totalApiDurationNs >= 0, 'Duration should be recorded');
1610
+ });
1611
+
1612
+ it('records multiple api calls in the same requestMetrics', async context => {
1613
+ const metrics = RequestMetrics.startRequest();
1614
+
1615
+ context.mock.method(global, 'fetch', () =>
1616
+ Promise.resolve(
1617
+ new Response('{"data": "value"}', {
1618
+ status: 200,
1619
+ headers: new Headers({ 'Content-Type': 'application/json' }),
1620
+ }),
1621
+ ),
1622
+ );
1623
+
1624
+ await provider.get('/endpoint1', {
1625
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1626
+ logger,
1627
+ requestMetrics: metrics,
1628
+ });
1629
+
1630
+ await provider.post(
1631
+ '/endpoint2',
1632
+ { key: 'value' },
1633
+ {
1634
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1635
+ logger,
1636
+ requestMetrics: metrics,
1637
+ },
1638
+ );
1639
+
1640
+ const result = metrics.endRequest();
1641
+ assert.equal(result.apiCallCount, 2);
1642
+ assert.ok(result.totalApiDurationNs >= 0);
1643
+ });
1644
+
1645
+ it('works without requestMetrics (backward compatible)', async context => {
1646
+ const response = new Response('{"data": "value"}', {
1647
+ status: 200,
1648
+ headers: new Headers({ 'Content-Type': 'application/json' }),
1649
+ });
1650
+
1651
+ context.mock.method(global, 'fetch', () => Promise.resolve(response));
1652
+
1653
+ // No requestMetrics passed — should not throw
1654
+ const result = await provider.get('/endpoint', {
1655
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1656
+ logger,
1657
+ });
1658
+
1659
+ assert.equal(result.status, 200);
1660
+ });
1661
+
1662
+ it('records api call duration from postForm', async () => {
1663
+ const metrics = RequestMetrics.startRequest();
1664
+ const httpsProvider = new Provider({
1665
+ prepareRequest: () => ({
1666
+ url: 'https://www.myApi.com',
1667
+ headers: {},
1668
+ }),
1669
+ });
1670
+
1671
+ const scope = nock('https://www.myApi.com').post('/upload').reply(201, { success: true });
1672
+
1673
+ const FormData = (await import('form-data')).default;
1674
+ const form = new FormData();
1675
+ form.append('file', Buffer.from('test data'), 'test.txt');
1676
+
1677
+ await httpsProvider.postForm('/upload', form, {
1678
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1679
+ logger,
1680
+ requestMetrics: metrics,
1681
+ });
1682
+
1683
+ const postFormResult = metrics.endRequest();
1684
+ assert.equal(postFormResult.apiCallCount, 1);
1685
+ assert.ok(postFormResult.totalApiDurationNs > 0);
1686
+ scope.isDone();
1687
+ });
1688
+
1689
+ it('records api call duration from postStream', async () => {
1690
+ const metrics = RequestMetrics.startRequest();
1691
+ const testData = 'binary stream data';
1692
+ const httpsProvider = new Provider({
1693
+ prepareRequest: () => ({
1694
+ url: 'https://www.myApi.com',
1695
+ headers: {},
1696
+ }),
1697
+ });
1698
+
1699
+ const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
1700
+
1701
+ const stream = Readable.from(Buffer.from(testData));
1702
+
1703
+ await httpsProvider.postStream('/upload', stream, {
1704
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1705
+ logger,
1706
+ requestMetrics: metrics,
1707
+ });
1708
+
1709
+ const postStreamResult = metrics.endRequest();
1710
+ assert.equal(postStreamResult.apiCallCount, 1);
1711
+ assert.ok(postStreamResult.totalApiDurationNs > 0);
1712
+ scope.isDone();
1713
+ });
1714
+ });
1589
1715
  });
@@ -0,0 +1,50 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { RequestMetrics } from '../../src/resources/requestMetrics.js';
4
+
5
+ describe('RequestMetrics', () => {
6
+ describe('initial state', () => {
7
+ it('starts with zero api call count', () => {
8
+ const metrics = RequestMetrics.startRequest();
9
+ const result = metrics.endRequest();
10
+ assert.equal(result.apiCallCount, 0);
11
+ });
12
+
13
+ it('starts with zero total api duration', () => {
14
+ const metrics = RequestMetrics.startRequest();
15
+ const result = metrics.endRequest();
16
+ assert.equal(result.totalApiDurationNs, 0);
17
+ });
18
+ });
19
+
20
+ describe('endRequest', () => {
21
+ it('returns elapsed duration since construction', () => {
22
+ const before = process.hrtime.bigint();
23
+ const metrics = RequestMetrics.startRequest();
24
+ const result = metrics.endRequest();
25
+ const after = process.hrtime.bigint();
26
+ assert.ok(result.durationNs >= 0n);
27
+ assert.ok(result.durationNs <= after - before);
28
+ });
29
+ });
30
+
31
+ describe('recordExternalApiCall', () => {
32
+ it('increments api call count', () => {
33
+ const metrics = RequestMetrics.startRequest();
34
+ const start1 = process.hrtime.bigint();
35
+ metrics.recordExternalApiCall(start1);
36
+ const start2 = process.hrtime.bigint();
37
+ metrics.recordExternalApiCall(start2);
38
+ const result = metrics.endRequest();
39
+ assert.equal(result.apiCallCount, 2);
40
+ });
41
+
42
+ it('accumulates api duration', () => {
43
+ const metrics = RequestMetrics.startRequest();
44
+ const start = process.hrtime.bigint();
45
+ metrics.recordExternalApiCall(start);
46
+ const result = metrics.endRequest();
47
+ assert.ok(result.totalApiDurationNs >= 0);
48
+ });
49
+ });
50
+ });