@unito/integration-sdk 2.3.0 → 2.3.2

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.
@@ -67,8 +67,10 @@ const LOGMETA_BLACKLIST = [
67
67
  * Logger class that can be configured with metadata add creation and when logging to add additional context to your logs.
68
68
  */
69
69
  class Logger {
70
+ isDisabled;
70
71
  metadata;
71
- constructor(metadata = {}) {
72
+ constructor(metadata = {}, isDisabled = false) {
73
+ this.isDisabled = isDisabled;
72
74
  this.metadata = structuredClone(metadata);
73
75
  }
74
76
  /**
@@ -142,6 +144,9 @@ class Logger {
142
144
  this.metadata = {};
143
145
  }
144
146
  send(logLevel, message, metadata) {
147
+ if (this.isDisabled) {
148
+ return;
149
+ }
145
150
  // We need to provide the date to Datadog. Otherwise, the date is set to when they receive the log.
146
151
  const date = Date.now();
147
152
  if (message.length > MAX_LOG_MESSAGE_SIZE) {
@@ -188,6 +193,7 @@ class Logger {
188
193
  return prunedMetadata;
189
194
  }
190
195
  }
196
+ const NULL_LOGGER = new Logger({}, true);
191
197
 
192
198
  /**
193
199
  * The Cache class provides caching capabilities that can be used across your integration.
@@ -1409,6 +1415,7 @@ class Provider {
1409
1415
  }
1410
1416
  }
1411
1417
  const callToProvider = async () => {
1418
+ const beforeRequestTimestamp = process.hrtime.bigint();
1412
1419
  let response;
1413
1420
  try {
1414
1421
  response = await fetch(absoluteUrl, {
@@ -1430,6 +1437,20 @@ class Provider {
1430
1437
  }
1431
1438
  throw this.handleError(500, 'Unexpected error while calling the provider - this is not normal, investigate', options);
1432
1439
  }
1440
+ const afterRequestTimestamp = process.hrtime.bigint();
1441
+ const requestDurationInNS = Number(afterRequestTimestamp - beforeRequestTimestamp);
1442
+ const requestDurationInMs = (requestDurationInNS / 1_000_000) | 0;
1443
+ options.logger.info(`Connector API Request ${options.method} ${absoluteUrl} ${response.status} - ${requestDurationInMs} ms`, {
1444
+ duration: requestDurationInNS,
1445
+ http: {
1446
+ method: options.method,
1447
+ status_code: response.status,
1448
+ content_type: headers['Content-Type'],
1449
+ url_details: {
1450
+ path: absoluteUrl,
1451
+ },
1452
+ },
1453
+ });
1433
1454
  if (response.status >= 400) {
1434
1455
  const textResult = await response.text();
1435
1456
  throw this.handleError(response.status, textResult, options);
@@ -1511,5 +1532,6 @@ exports.Cache = Cache;
1511
1532
  exports.Handler = Handler;
1512
1533
  exports.HttpErrors = httpErrors;
1513
1534
  exports.Integration = Integration;
1535
+ exports.NULL_LOGGER = NULL_LOGGER;
1514
1536
  exports.Provider = Provider;
1515
1537
  exports.getApplicableFilters = getApplicableFilters;
@@ -9,4 +9,4 @@ export type { Filter } from './middlewares/filters.js';
9
9
  export * as HttpErrors from './httpErrors.js';
10
10
  export { getApplicableFilters } from './helpers.js';
11
11
  export * from './resources/context.js';
12
- export { type default as Logger } from './resources/logger.js';
12
+ export { type default as Logger, NULL_LOGGER } from './resources/logger.js';
package/dist/src/index.js CHANGED
@@ -7,4 +7,5 @@ export { Provider, } from './resources/provider.js';
7
7
  export * as HttpErrors from './httpErrors.js';
8
8
  export { getApplicableFilters } from './helpers.js';
9
9
  export * from './resources/context.js';
10
+ export { NULL_LOGGER } from './resources/logger.js';
10
11
  /* c8 ignore stop */
@@ -6,9 +6,25 @@ declare global {
6
6
  }
7
7
  }
8
8
  }
9
+ /**
10
+ * The credentials object passed to every handler function.
11
+ *
12
+ * It contains the parsed credentials payload associated with the request through the X-Unito-Credentials header.
13
+ */
9
14
  export type Credentials = {
15
+ /**
16
+ * The access token for the provider.
17
+ */
10
18
  accessToken?: string;
11
- unitoCredentialId: string;
19
+ /**
20
+ * The id of the unito credential record.
21
+ *
22
+ * This is not available on the initial call to /me before the creation of the credential.
23
+ */
24
+ unitoCredentialId?: string;
25
+ /**
26
+ * The id of the unito user record.
27
+ */
12
28
  unitoUserId?: string;
13
29
  [keys: string]: unknown;
14
30
  };
@@ -10,8 +10,9 @@ type ForbidenMetadataKey = 'message';
10
10
  * Logger class that can be configured with metadata add creation and when logging to add additional context to your logs.
11
11
  */
12
12
  export default class Logger {
13
+ private isDisabled;
13
14
  private metadata;
14
- constructor(metadata?: Metadata);
15
+ constructor(metadata?: Metadata, isDisabled?: boolean);
15
16
  /**
16
17
  * Logs a message with the 'log' log level.
17
18
  * @param message The message to be logged.
@@ -68,4 +69,5 @@ export default class Logger {
68
69
  private static snakifyKeys;
69
70
  private static pruneSensitiveMetadata;
70
71
  }
72
+ export declare const NULL_LOGGER: Logger;
71
73
  export {};
@@ -39,8 +39,10 @@ const LOGMETA_BLACKLIST = [
39
39
  * Logger class that can be configured with metadata add creation and when logging to add additional context to your logs.
40
40
  */
41
41
  export default class Logger {
42
+ isDisabled;
42
43
  metadata;
43
- constructor(metadata = {}) {
44
+ constructor(metadata = {}, isDisabled = false) {
45
+ this.isDisabled = isDisabled;
44
46
  this.metadata = structuredClone(metadata);
45
47
  }
46
48
  /**
@@ -114,6 +116,9 @@ export default class Logger {
114
116
  this.metadata = {};
115
117
  }
116
118
  send(logLevel, message, metadata) {
119
+ if (this.isDisabled) {
120
+ return;
121
+ }
117
122
  // We need to provide the date to Datadog. Otherwise, the date is set to when they receive the log.
118
123
  const date = Date.now();
119
124
  if (message.length > MAX_LOG_MESSAGE_SIZE) {
@@ -160,3 +165,4 @@ export default class Logger {
160
165
  return prunedMetadata;
161
166
  }
162
167
  }
168
+ export const NULL_LOGGER = new Logger({}, true);
@@ -284,6 +284,7 @@ export class Provider {
284
284
  }
285
285
  }
286
286
  const callToProvider = async () => {
287
+ const beforeRequestTimestamp = process.hrtime.bigint();
287
288
  let response;
288
289
  try {
289
290
  response = await fetch(absoluteUrl, {
@@ -305,6 +306,20 @@ export class Provider {
305
306
  }
306
307
  throw this.handleError(500, 'Unexpected error while calling the provider - this is not normal, investigate', options);
307
308
  }
309
+ const afterRequestTimestamp = process.hrtime.bigint();
310
+ const requestDurationInNS = Number(afterRequestTimestamp - beforeRequestTimestamp);
311
+ const requestDurationInMs = (requestDurationInNS / 1_000_000) | 0;
312
+ options.logger.info(`Connector API Request ${options.method} ${absoluteUrl} ${response.status} - ${requestDurationInMs} ms`, {
313
+ duration: requestDurationInNS,
314
+ http: {
315
+ method: options.method,
316
+ status_code: response.status,
317
+ content_type: headers['Content-Type'],
318
+ url_details: {
319
+ path: absoluteUrl,
320
+ },
321
+ },
322
+ });
308
323
  if (response.status >= 400) {
309
324
  const textResult = await response.text();
310
325
  throw this.handleError(response.status, textResult, options);
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
- import { default as Logger } from '../../src/resources/logger.js';
3
+ import { default as Logger, NULL_LOGGER } from '../../src/resources/logger.js';
4
4
  describe('Logger', () => {
5
5
  it('metadata', () => {
6
6
  let metadata = { correlation_id: 'test' };
@@ -140,4 +140,22 @@ describe('Logger', () => {
140
140
  assert.deepEqual(actual['http'], { method: 'GET', status_code: 200, jwt: '[REDACTED]' });
141
141
  assert.deepEqual(actual['user']['contact'], { email: '[REDACTED]', first_name: '[REDACTED]' });
142
142
  });
143
+ it(`NULL_LOGGER should not log`, testContext => {
144
+ const logger = NULL_LOGGER;
145
+ const logSpy = testContext.mock.method(global.console, 'log', () => { });
146
+ const errorSpy = testContext.mock.method(global.console, 'error', () => { });
147
+ const warnSpy = testContext.mock.method(global.console, 'warn', () => { });
148
+ const infoSpy = testContext.mock.method(global.console, 'info', () => { });
149
+ const debugSpy = testContext.mock.method(global.console, 'debug', () => { });
150
+ logger.log('test');
151
+ logger.info('test');
152
+ logger.warn('test');
153
+ logger.error('test');
154
+ logger.debug('test');
155
+ assert.strictEqual(logSpy.mock.calls.length, 0);
156
+ assert.strictEqual(errorSpy.mock.calls.length, 0);
157
+ assert.strictEqual(warnSpy.mock.calls.length, 0);
158
+ assert.strictEqual(infoSpy.mock.calls.length, 0);
159
+ assert.strictEqual(debugSpy.mock.calls.length, 0);
160
+ });
143
161
  });
@@ -50,7 +50,7 @@ describe('Provider', () => {
50
50
  ]);
51
51
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
52
52
  });
53
- it('should accept text/html type response', async (context) => {
53
+ it('accepts text/html type response', async (context) => {
54
54
  const response = new Response('', {
55
55
  status: 200,
56
56
  headers: { 'Content-Type': 'text/html; charset=UTF-8' },
@@ -79,7 +79,7 @@ describe('Provider', () => {
79
79
  ]);
80
80
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: '' });
81
81
  });
82
- it('should accept application/schema+json type response', async (context) => {
82
+ it('accepts application/schema+json type response', async (context) => {
83
83
  const response = new Response('{"data": "value"}', {
84
84
  status: 200,
85
85
  headers: { 'Content-Type': 'application/schema+json; charset=UTF-8' },
@@ -108,7 +108,7 @@ describe('Provider', () => {
108
108
  ]);
109
109
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
110
110
  });
111
- it('should accept application/swagger+json type response', async (context) => {
111
+ it('accepts application/swagger+json type response', async (context) => {
112
112
  const response = new Response('{"data": "value"}', {
113
113
  status: 200,
114
114
  headers: { 'Content-Type': 'application/swagger+json; charset=UTF-8' },
@@ -137,7 +137,7 @@ describe('Provider', () => {
137
137
  ]);
138
138
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
139
139
  });
140
- it('should accept application/vnd.oracle.resource+json type response', async (context) => {
140
+ it('accepts application/vnd.oracle.resource+json type response', async (context) => {
141
141
  const response = new Response('{"data": "value"}', {
142
142
  status: 200,
143
143
  headers: { 'Content-Type': 'application/vnd.oracle.resource+json; type=collection; charset=UTF-8' },
@@ -166,7 +166,7 @@ describe('Provider', () => {
166
166
  ]);
167
167
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
168
168
  });
169
- it('should return the raw response body if specified', async (context) => {
169
+ it('returns the raw response body if specified', async (context) => {
170
170
  const response = new Response(`IMAGINE A HUGE PAYLOAD`, {
171
171
  status: 200,
172
172
  headers: { 'Content-Type': 'image/png' },
@@ -244,7 +244,7 @@ describe('Provider', () => {
244
244
  ]);
245
245
  assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
246
246
  });
247
- it('accept an array as body for post request', async (context) => {
247
+ it('accepts an array as body for post request', async (context) => {
248
248
  const response = new Response('{"data": "value"}', {
249
249
  status: 201,
250
250
  headers: { 'Content-Type': 'application/json' },
@@ -485,7 +485,7 @@ describe('Provider', () => {
485
485
  assert.ok(error instanceof HttpErrors.HttpError);
486
486
  assert.equal(error.message, 'Weird provider behavior');
487
487
  });
488
- it('should contain the credential in the custom error handler', async (context) => {
488
+ it('contains the credential in the custom error handler', async (context) => {
489
489
  const provider = new Provider({
490
490
  prepareRequest: requestOptions => {
491
491
  return {
@@ -761,4 +761,16 @@ describe('Provider', () => {
761
761
  assert.ok(error instanceof HttpErrors.RateLimitExceededError);
762
762
  assert.equal(error.message, 'response body');
763
763
  });
764
+ it('logs provider requests', async (context) => {
765
+ const response = new Response(undefined, { status: 201 });
766
+ context.mock.method(global, 'fetch', () => Promise.resolve(response));
767
+ const loggerStub = context.mock.method(logger, 'info');
768
+ await provider.get('/endpoint/123', {
769
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
770
+ signal: new AbortController().signal,
771
+ logger: logger,
772
+ });
773
+ assert.equal(loggerStub.mock.callCount(), 1);
774
+ assert.match(String(loggerStub.mock.calls[0]?.arguments[0]), /Connector API Request GET www.myApi.com\/endpoint\/123 201 - \d+ ms/);
775
+ });
764
776
  });
@@ -0,0 +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.8.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
package/src/index.ts CHANGED
@@ -15,5 +15,5 @@ export type { Filter } from './middlewares/filters.js';
15
15
  export * as HttpErrors from './httpErrors.js';
16
16
  export { getApplicableFilters } from './helpers.js';
17
17
  export * from './resources/context.js';
18
- export { type default as Logger } from './resources/logger.js';
18
+ export { type default as Logger, NULL_LOGGER } from './resources/logger.js';
19
19
  /* c8 ignore stop */
@@ -10,9 +10,25 @@ declare global {
10
10
  }
11
11
  }
12
12
 
13
+ /**
14
+ * The credentials object passed to every handler function.
15
+ *
16
+ * It contains the parsed credentials payload associated with the request through the X-Unito-Credentials header.
17
+ */
13
18
  export type Credentials = {
19
+ /**
20
+ * The access token for the provider.
21
+ */
14
22
  accessToken?: string;
15
- unitoCredentialId: string;
23
+ /**
24
+ * The id of the unito credential record.
25
+ *
26
+ * This is not available on the initial call to /me before the creation of the credential.
27
+ */
28
+ unitoCredentialId?: string;
29
+ /**
30
+ * The id of the unito user record.
31
+ */
16
32
  unitoUserId?: string;
17
33
  [keys: string]: unknown;
18
34
  };
@@ -50,8 +50,10 @@ const LOGMETA_BLACKLIST = [
50
50
  */
51
51
  export default class Logger {
52
52
  private metadata: Metadata;
53
-
54
- constructor(metadata: Metadata = {}) {
53
+ constructor(
54
+ metadata: Metadata = {},
55
+ private isDisabled: boolean = false,
56
+ ) {
55
57
  this.metadata = structuredClone(metadata);
56
58
  }
57
59
 
@@ -138,6 +140,10 @@ export default class Logger {
138
140
  }
139
141
 
140
142
  private send(logLevel: LogLevel, message: string, metadata?: Metadata): void {
143
+ if (this.isDisabled) {
144
+ return;
145
+ }
146
+
141
147
  // We need to provide the date to Datadog. Otherwise, the date is set to when they receive the log.
142
148
  const date = Date.now();
143
149
 
@@ -191,3 +197,5 @@ export default class Logger {
191
197
  return prunedMetadata;
192
198
  }
193
199
  }
200
+
201
+ export const NULL_LOGGER = new Logger({}, true);
@@ -388,6 +388,8 @@ export class Provider {
388
388
  }
389
389
 
390
390
  const callToProvider = async (): Promise<Response<T>> => {
391
+ const beforeRequestTimestamp = process.hrtime.bigint();
392
+
391
393
  let response: globalThis.Response;
392
394
 
393
395
  try {
@@ -420,6 +422,25 @@ export class Provider {
420
422
  );
421
423
  }
422
424
 
425
+ const afterRequestTimestamp = process.hrtime.bigint();
426
+ const requestDurationInNS = Number(afterRequestTimestamp - beforeRequestTimestamp);
427
+ const requestDurationInMs = (requestDurationInNS / 1_000_000) | 0;
428
+
429
+ options.logger.info(
430
+ `Connector API Request ${options.method} ${absoluteUrl} ${response.status} - ${requestDurationInMs} ms`,
431
+ {
432
+ duration: requestDurationInNS,
433
+ http: {
434
+ method: options.method,
435
+ status_code: response.status,
436
+ content_type: headers['Content-Type'],
437
+ url_details: {
438
+ path: absoluteUrl,
439
+ },
440
+ },
441
+ },
442
+ );
443
+
423
444
  if (response.status >= 400) {
424
445
  const textResult = await response.text();
425
446
  throw this.handleError(response.status, textResult, options);
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
- import { default as Logger, Metadata } from '../../src/resources/logger.js';
3
+ import { default as Logger, Metadata, NULL_LOGGER } from '../../src/resources/logger.js';
4
4
 
5
5
  describe('Logger', () => {
6
6
  it('metadata', () => {
@@ -168,4 +168,26 @@ describe('Logger', () => {
168
168
  assert.deepEqual(actual['http'], { method: 'GET', status_code: 200, jwt: '[REDACTED]' });
169
169
  assert.deepEqual(actual['user']['contact'], { email: '[REDACTED]', first_name: '[REDACTED]' });
170
170
  });
171
+
172
+ it(`NULL_LOGGER should not log`, testContext => {
173
+ const logger = NULL_LOGGER;
174
+
175
+ const logSpy = testContext.mock.method(global.console, 'log', () => {});
176
+ const errorSpy = testContext.mock.method(global.console, 'error', () => {});
177
+ const warnSpy = testContext.mock.method(global.console, 'warn', () => {});
178
+ const infoSpy = testContext.mock.method(global.console, 'info', () => {});
179
+ const debugSpy = testContext.mock.method(global.console, 'debug', () => {});
180
+
181
+ logger.log('test');
182
+ logger.info('test');
183
+ logger.warn('test');
184
+ logger.error('test');
185
+ logger.debug('test');
186
+
187
+ assert.strictEqual(logSpy.mock.calls.length, 0);
188
+ assert.strictEqual(errorSpy.mock.calls.length, 0);
189
+ assert.strictEqual(warnSpy.mock.calls.length, 0);
190
+ assert.strictEqual(infoSpy.mock.calls.length, 0);
191
+ assert.strictEqual(debugSpy.mock.calls.length, 0);
192
+ });
171
193
  });
@@ -59,7 +59,7 @@ describe('Provider', () => {
59
59
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
60
60
  });
61
61
 
62
- it('should accept text/html type response', async context => {
62
+ it('accepts text/html type response', async context => {
63
63
  const response = new Response('', {
64
64
  status: 200,
65
65
  headers: { 'Content-Type': 'text/html; charset=UTF-8' },
@@ -94,7 +94,7 @@ describe('Provider', () => {
94
94
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: '' });
95
95
  });
96
96
 
97
- it('should accept application/schema+json type response', async context => {
97
+ it('accepts application/schema+json type response', async context => {
98
98
  const response = new Response('{"data": "value"}', {
99
99
  status: 200,
100
100
  headers: { 'Content-Type': 'application/schema+json; charset=UTF-8' },
@@ -129,7 +129,7 @@ describe('Provider', () => {
129
129
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
130
130
  });
131
131
 
132
- it('should accept application/swagger+json type response', async context => {
132
+ it('accepts application/swagger+json type response', async context => {
133
133
  const response = new Response('{"data": "value"}', {
134
134
  status: 200,
135
135
  headers: { 'Content-Type': 'application/swagger+json; charset=UTF-8' },
@@ -164,7 +164,7 @@ describe('Provider', () => {
164
164
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
165
165
  });
166
166
 
167
- it('should accept application/vnd.oracle.resource+json type response', async context => {
167
+ it('accepts application/vnd.oracle.resource+json type response', async context => {
168
168
  const response = new Response('{"data": "value"}', {
169
169
  status: 200,
170
170
  headers: { 'Content-Type': 'application/vnd.oracle.resource+json; type=collection; charset=UTF-8' },
@@ -199,7 +199,7 @@ describe('Provider', () => {
199
199
  assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
200
200
  });
201
201
 
202
- it('should return the raw response body if specified', async context => {
202
+ it('returns the raw response body if specified', async context => {
203
203
  const response = new Response(`IMAGINE A HUGE PAYLOAD`, {
204
204
  status: 200,
205
205
  headers: { 'Content-Type': 'image/png' },
@@ -293,7 +293,7 @@ describe('Provider', () => {
293
293
  assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
294
294
  });
295
295
 
296
- it('accept an array as body for post request', async context => {
296
+ it('accepts an array as body for post request', async context => {
297
297
  const response = new Response('{"data": "value"}', {
298
298
  status: 201,
299
299
  headers: { 'Content-Type': 'application/json' },
@@ -580,7 +580,7 @@ describe('Provider', () => {
580
580
  assert.equal(error.message, 'Weird provider behavior');
581
581
  });
582
582
 
583
- it('should contain the credential in the custom error handler', async context => {
583
+ it('contains the credential in the custom error handler', async context => {
584
584
  const provider = new Provider({
585
585
  prepareRequest: requestOptions => {
586
586
  return {
@@ -911,4 +911,24 @@ describe('Provider', () => {
911
911
  assert.ok(error instanceof HttpErrors.RateLimitExceededError);
912
912
  assert.equal(error.message, 'response body');
913
913
  });
914
+
915
+ it('logs provider requests', async context => {
916
+ const response = new Response(undefined, { status: 201 });
917
+
918
+ context.mock.method(global, 'fetch', () => Promise.resolve(response));
919
+
920
+ const loggerStub = context.mock.method(logger, 'info');
921
+
922
+ await provider.get('/endpoint/123', {
923
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
924
+ signal: new AbortController().signal,
925
+ logger: logger,
926
+ });
927
+
928
+ assert.equal(loggerStub.mock.callCount(), 1);
929
+ assert.match(
930
+ String(loggerStub.mock.calls[0]?.arguments[0]),
931
+ /Connector API Request GET www.myApi.com\/endpoint\/123 201 - \d+ ms/,
932
+ );
933
+ });
914
934
  });