@outputai/http 0.4.1-dev.92bc2fb.0 → 0.4.1-dev.c0b98d8.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.
@@ -13,6 +13,7 @@ RequestInit };
13
13
  * Behaves the same as any fetch function except:
14
14
  * - Sets a request header called `x-request--trace-id` with a random UUID;
15
15
  * - Sends the request, response, error and/or failure to the Trace system;
16
+ * - Emits a `http:request` event on every call (success, http_error, network_error).
16
17
  *
17
18
  * @see {@link https://fetch.spec.whatwg.org/}
18
19
  * @param input - URL string, URL object or Request object (undici's or Node's)
@@ -15,6 +15,7 @@ export * as undici from 'undici';
15
15
  * Behaves the same as any fetch function except:
16
16
  * - Sets a request header called `x-request--trace-id` with a random UUID;
17
17
  * - Sends the request, response, error and/or failure to the Trace system;
18
+ * - Emits a `http:request` event on every call (success, http_error, network_error).
18
19
  *
19
20
  * @see {@link https://fetch.spec.whatwg.org/}
20
21
  * @param input - URL string, URL object or Request object (undici's or Node's)
@@ -29,20 +30,25 @@ export const fetch = async (input, init) => {
29
30
  const requestId = randomUUID();
30
31
  headers.set('x-request-trace-id', requestId);
31
32
  const request = new undici.Request(base, { headers });
33
+ const method = request.method;
34
+ const url = request.url;
35
+ const startedAt = performance.now();
32
36
  await logRequest({ requestId, request });
33
37
  try {
34
38
  const response = await undici.fetch(request);
39
+ const durationMs = Math.round(performance.now() - startedAt);
35
40
  // This enriches the response of the request id, so it is identifiable later.
36
41
  addRequestIdToResponse(response, requestId);
37
42
  if (response.status > 399) {
38
- await logError({ requestId, response });
43
+ await logError({ requestId, response, method, url, durationMs });
39
44
  return response;
40
45
  }
41
- await logResponse({ requestId, response });
46
+ await logResponse({ requestId, response, method, url, durationMs });
42
47
  return response;
43
48
  }
44
49
  catch (error) {
45
- logFailure({ requestId, error: error });
50
+ const durationMs = Math.round(performance.now() - startedAt);
51
+ logFailure({ requestId, error: error, method, url, durationMs });
46
52
  throw error;
47
53
  }
48
54
  };
@@ -62,8 +62,13 @@ describe('fetch/index', () => {
62
62
  expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('GET');
63
63
  expect(loggerMock.logRequest.mock.calls[0][0].request.url).toBe(`${MOCK_ORIGIN}/ok`);
64
64
  expect(loggerMock.logResponse).toHaveBeenCalledTimes(1);
65
- expect(loggerMock.logResponse.mock.calls[0][0].requestId).toBe(FIXED_REQUEST_ID);
66
- expect(loggerMock.logResponse.mock.calls[0][0].response).toBe(response);
65
+ const responseCall = loggerMock.logResponse.mock.calls[0][0];
66
+ expect(responseCall.requestId).toBe(FIXED_REQUEST_ID);
67
+ expect(responseCall.response).toBe(response);
68
+ expect(responseCall.method).toBe('GET');
69
+ expect(responseCall.url).toBe(`${MOCK_ORIGIN}/ok`);
70
+ expect(typeof responseCall.durationMs).toBe('number');
71
+ expect(responseCall.durationMs).toBeGreaterThanOrEqual(0);
67
72
  expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledTimes(1);
68
73
  expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledWith(response, FIXED_REQUEST_ID);
69
74
  expect(loggerMock.logError).not.toHaveBeenCalled();
@@ -12,34 +12,55 @@ export declare const logRequest: ({ requestId, request }: {
12
12
  }) => Promise<void>;
13
13
  /**
14
14
  * Sends the trace error event for an http response with error status
15
+ * and emits a `http:request` event with `outcome: 'http_error'`.
15
16
  *
16
17
  * @param options
17
18
  * @param options.requestId - id of the request
18
19
  * @param options.response - The HTTP Response object
20
+ * @param options.method - HTTP method of the request
21
+ * @param options.url - URL of the request
22
+ * @param options.durationMs - elapsed time from request issuance to response, in milliseconds
19
23
  */
20
- export declare const logError: ({ requestId, response }: {
24
+ export declare const logError: ({ requestId, response, method, url, durationMs }: {
21
25
  requestId: string;
22
26
  response: Response;
27
+ method: string;
28
+ url: string;
29
+ durationMs: number;
23
30
  }) => Promise<void>;
24
31
  /**
25
32
  * Sends the trace end event for an http response
33
+ * and emits a `http:request` event with `outcome: 'success'`.
26
34
  *
27
35
  * @param {object} options
28
36
  * @param options.requestId - id of the request
29
37
  * @param {Response} options.response - The HTTP Response object
38
+ * @param options.method - HTTP method of the request
39
+ * @param options.url - URL of the request
40
+ * @param options.durationMs - elapsed time from request issuance to response, in milliseconds
30
41
  */
31
- export declare const logResponse: ({ requestId, response }: {
42
+ export declare const logResponse: ({ requestId, response, method, url, durationMs }: {
32
43
  requestId: string;
33
44
  response: Response;
45
+ method: string;
46
+ url: string;
47
+ durationMs: number;
34
48
  }) => Promise<void>;
35
49
  /**
36
50
  * Creates the trace error event for a network/connection failure
51
+ * and emits a `http:request` event with `outcome: 'network_error'`.
37
52
  *
38
53
  * @param options
39
54
  * @param options.requestId - id of the request
40
55
  * @param options.error - The error thrown
56
+ * @param options.method - HTTP method of the request
57
+ * @param options.url - URL of the request
58
+ * @param options.durationMs - elapsed time from request issuance to failure, in milliseconds
41
59
  */
42
- export declare const logFailure: ({ requestId, error }: {
60
+ export declare const logFailure: ({ requestId, error, method, url, durationMs }: {
43
61
  requestId: string;
44
62
  error: Error;
63
+ method: string;
64
+ url: string;
65
+ durationMs: number;
45
66
  }) => void;
@@ -1,4 +1,4 @@
1
- import { Tracing } from '@outputai/core/sdk_activity_integration';
1
+ import { Tracing, emitEvent } from '@outputai/core/sdk_activity_integration';
2
2
  import { config } from '../config.js';
3
3
  import { parseBody, redactHeaders, serializeError } from './utils.js';
4
4
  /**
@@ -20,38 +20,80 @@ export const logRequest = async ({ requestId, request }) => {
20
20
  };
21
21
  /**
22
22
  * Sends the trace error event for an http response with error status
23
+ * and emits a `http:request` event with `outcome: 'http_error'`.
23
24
  *
24
25
  * @param options
25
26
  * @param options.requestId - id of the request
26
27
  * @param options.response - The HTTP Response object
28
+ * @param options.method - HTTP method of the request
29
+ * @param options.url - URL of the request
30
+ * @param options.durationMs - elapsed time from request issuance to response, in milliseconds
27
31
  */
28
- export const logError = async ({ requestId, response }) => Tracing.addEventError({
29
- id: requestId, details: {
32
+ export const logError = async ({ requestId, response, method, url, durationMs }) => {
33
+ await Tracing.addEventError({
34
+ id: requestId, details: {
35
+ status: response.status,
36
+ statusText: response.statusText,
37
+ headers: redactHeaders(response.headers),
38
+ body: await parseBody(response)
39
+ }
40
+ });
41
+ emitEvent('http:request', {
42
+ requestId,
43
+ method,
44
+ url,
30
45
  status: response.status,
31
- statusText: response.statusText,
32
- headers: redactHeaders(response.headers),
33
- body: await parseBody(response)
34
- }
35
- });
46
+ durationMs,
47
+ outcome: 'http_error'
48
+ });
49
+ };
36
50
  /**
37
51
  * Sends the trace end event for an http response
52
+ * and emits a `http:request` event with `outcome: 'success'`.
38
53
  *
39
54
  * @param {object} options
40
55
  * @param options.requestId - id of the request
41
56
  * @param {Response} options.response - The HTTP Response object
57
+ * @param options.method - HTTP method of the request
58
+ * @param options.url - URL of the request
59
+ * @param options.durationMs - elapsed time from request issuance to response, in milliseconds
42
60
  */
43
- export const logResponse = async ({ requestId, response }) => Tracing.addEventEnd({
44
- id: requestId, details: {
61
+ export const logResponse = async ({ requestId, response, method, url, durationMs }) => {
62
+ await Tracing.addEventEnd({
63
+ id: requestId, details: {
64
+ status: response.status,
65
+ statusText: response.statusText,
66
+ ...(config.logVerbose && { headers: redactHeaders(response.headers), body: await parseBody(response) })
67
+ }
68
+ });
69
+ emitEvent('http:request', {
70
+ requestId,
71
+ method,
72
+ url,
45
73
  status: response.status,
46
- statusText: response.statusText,
47
- ...(config.logVerbose && { headers: redactHeaders(response.headers), body: await parseBody(response) })
48
- }
49
- });
74
+ durationMs,
75
+ outcome: 'success'
76
+ });
77
+ };
50
78
  /**
51
79
  * Creates the trace error event for a network/connection failure
80
+ * and emits a `http:request` event with `outcome: 'network_error'`.
52
81
  *
53
82
  * @param options
54
83
  * @param options.requestId - id of the request
55
84
  * @param options.error - The error thrown
85
+ * @param options.method - HTTP method of the request
86
+ * @param options.url - URL of the request
87
+ * @param options.durationMs - elapsed time from request issuance to failure, in milliseconds
56
88
  */
57
- export const logFailure = ({ requestId, error }) => Tracing.addEventError({ id: requestId, details: serializeError(error) });
89
+ export const logFailure = ({ requestId, error, method, url, durationMs }) => {
90
+ Tracing.addEventError({ id: requestId, details: serializeError(error) });
91
+ emitEvent('http:request', {
92
+ requestId,
93
+ method,
94
+ url,
95
+ status: undefined,
96
+ durationMs,
97
+ outcome: 'network_error'
98
+ });
99
+ };
@@ -6,10 +6,12 @@ vi.mock('@outputai/core/sdk_activity_integration', () => ({
6
6
  addEventEnd: vi.fn(),
7
7
  addEventError: vi.fn(),
8
8
  addEventAttribute: vi.fn()
9
- }
9
+ },
10
+ emitEvent: vi.fn()
10
11
  }));
11
- import { Tracing } from '@outputai/core/sdk_activity_integration';
12
+ import { Tracing, emitEvent } from '@outputai/core/sdk_activity_integration';
12
13
  const tracing = vi.mocked(Tracing, true);
14
+ const emit = vi.mocked(emitEvent, true);
13
15
  /** Loads logger with optional verbose tracing env so `config.js` is evaluated fresh. */
14
16
  async function logLogger(verbose) {
15
17
  vi.resetModules();
@@ -26,6 +28,7 @@ beforeEach(() => {
26
28
  tracing.addEventEnd.mockClear();
27
29
  tracing.addEventError.mockClear();
28
30
  tracing.addEventAttribute.mockClear();
31
+ emit.mockClear();
29
32
  });
30
33
  describe('fetch/logger', () => {
31
34
  describe('logRequest', () => {
@@ -97,7 +100,9 @@ describe('fetch/logger', () => {
97
100
  'content-type': 'application/json'
98
101
  }
99
102
  });
100
- await logError({ requestId: 'e1', response });
103
+ await logError({
104
+ requestId: 'e1', response, method: 'GET', url: 'https://upstream.test/x', durationMs: 1
105
+ });
101
106
  expect(tracing.addEventError).toHaveBeenCalledWith({
102
107
  id: 'e1',
103
108
  details: {
@@ -120,7 +125,9 @@ describe('fetch/logger', () => {
120
125
  statusText: 'Bad Gateway',
121
126
  headers: { 'content-type': 'text/plain' }
122
127
  });
123
- await logError({ requestId: 'e2', response });
128
+ await logError({
129
+ requestId: 'e2', response, method: 'GET', url: 'https://upstream.test/y', durationMs: 1
130
+ });
124
131
  expect(tracing.addEventError).toHaveBeenCalledWith({
125
132
  id: 'e2',
126
133
  details: {
@@ -140,7 +147,9 @@ describe('fetch/logger', () => {
140
147
  statusText: 'OK',
141
148
  headers: { 'content-type': 'application/json', Authorization: 'x' }
142
149
  });
143
- await logResponse({ requestId: 'lr1', response });
150
+ await logResponse({
151
+ requestId: 'lr1', response, method: 'GET', url: 'https://x.test/a', durationMs: 1
152
+ });
144
153
  expect(tracing.addEventEnd).toHaveBeenCalledWith({
145
154
  id: 'lr1',
146
155
  details: {
@@ -159,7 +168,9 @@ describe('fetch/logger', () => {
159
168
  'Set-Cookie': 'a=b'
160
169
  }
161
170
  });
162
- await logResponse({ requestId: 'lr-v', response });
171
+ await logResponse({
172
+ requestId: 'lr-v', response, method: 'POST', url: 'https://x.test/b', durationMs: 1
173
+ });
163
174
  expect(tracing.addEventEnd).toHaveBeenCalledWith({
164
175
  id: 'lr-v',
165
176
  details: {
@@ -178,7 +189,7 @@ describe('fetch/logger', () => {
178
189
  it('forwards serialized error details (including stack) to Tracing.addEventError', async () => {
179
190
  const { logFailure } = await logLogger(false);
180
191
  const err = new TypeError('network');
181
- logFailure({ requestId: 'f1', error: err });
192
+ logFailure({ requestId: 'f1', error: err, method: 'GET', url: 'https://example.test/x', durationMs: 12 });
182
193
  expect(tracing.addEventError).toHaveBeenCalledWith({
183
194
  id: 'f1',
184
195
  details: {
@@ -191,4 +202,63 @@ describe('fetch/logger', () => {
191
202
  });
192
203
  });
193
204
  });
205
+ describe('http:request event emission', () => {
206
+ it('emits http:request with outcome=success on logResponse', async () => {
207
+ const { logResponse } = await logLogger(false);
208
+ const response = new Response('', { status: 200 });
209
+ await logResponse({
210
+ requestId: 'r-ok',
211
+ response,
212
+ method: 'GET',
213
+ url: 'https://api.example.com/ok',
214
+ durationMs: 42
215
+ });
216
+ expect(emit).toHaveBeenCalledWith('http:request', {
217
+ requestId: 'r-ok',
218
+ method: 'GET',
219
+ url: 'https://api.example.com/ok',
220
+ status: 200,
221
+ durationMs: 42,
222
+ outcome: 'success'
223
+ });
224
+ });
225
+ it('emits http:request with outcome=http_error on logError', async () => {
226
+ const { logError } = await logLogger(false);
227
+ const response = new Response('boom', { status: 500 });
228
+ await logError({
229
+ requestId: 'r-err',
230
+ response,
231
+ method: 'POST',
232
+ url: 'https://api.example.com/err',
233
+ durationMs: 15
234
+ });
235
+ expect(emit).toHaveBeenCalledWith('http:request', {
236
+ requestId: 'r-err',
237
+ method: 'POST',
238
+ url: 'https://api.example.com/err',
239
+ status: 500,
240
+ durationMs: 15,
241
+ outcome: 'http_error'
242
+ });
243
+ });
244
+ it('emits http:request with outcome=network_error on logFailure (status undefined)', async () => {
245
+ const { logFailure } = await logLogger(false);
246
+ const err = new TypeError('network');
247
+ logFailure({
248
+ requestId: 'r-net',
249
+ error: err,
250
+ method: 'GET',
251
+ url: 'https://api.example.com/net',
252
+ durationMs: 9
253
+ });
254
+ expect(emit).toHaveBeenCalledWith('http:request', {
255
+ requestId: 'r-net',
256
+ method: 'GET',
257
+ url: 'https://api.example.com/net',
258
+ status: undefined,
259
+ durationMs: 9,
260
+ outcome: 'network_error'
261
+ });
262
+ });
263
+ });
194
264
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/http",
3
- "version": "0.4.1-dev.92bc2fb.0",
3
+ "version": "0.4.1-dev.c0b98d8.0",
4
4
  "description": "Framework abstraction to make HTTP calls with tracing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,7 @@
11
11
  "dependencies": {
12
12
  "ky": "1.14.3",
13
13
  "undici": "8.1.0",
14
- "@outputai/core": "0.4.1-dev.92bc2fb.0"
14
+ "@outputai/core": "0.4.1-dev.c0b98d8.0"
15
15
  },
16
16
  "license": "Apache-2.0",
17
17
  "publishConfig": {