@outputai/http 0.2.1-next.af8a069.0 → 0.2.1-next.b87b58f.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/consts.d.ts +4 -0
  2. package/dist/consts.js +4 -0
  3. package/dist/cost.d.ts +16 -0
  4. package/dist/cost.js +18 -0
  5. package/dist/cost.spec.js +88 -0
  6. package/dist/fetch/index.d.ts +22 -0
  7. package/dist/fetch/index.js +48 -0
  8. package/dist/fetch/index.spec.js +218 -0
  9. package/dist/fetch/logger.d.ts +45 -0
  10. package/dist/fetch/logger.js +57 -0
  11. package/dist/fetch/logger.spec.js +194 -0
  12. package/dist/fetch/utils.d.ts +45 -0
  13. package/dist/fetch/utils.js +101 -0
  14. package/dist/fetch/utils.spec.js +274 -0
  15. package/dist/index.d.ts +3 -1
  16. package/dist/index.js +5 -26
  17. package/dist/index.spec.js +11 -389
  18. package/package.json +3 -2
  19. package/dist/hooks/assign_request_id.d.ts +0 -9
  20. package/dist/hooks/assign_request_id.js +0 -15
  21. package/dist/hooks/index.d.ts +0 -4
  22. package/dist/hooks/index.js +0 -4
  23. package/dist/hooks/trace_error.d.ts +0 -14
  24. package/dist/hooks/trace_error.js +0 -61
  25. package/dist/hooks/trace_error.spec.js +0 -35
  26. package/dist/hooks/trace_request.d.ts +0 -6
  27. package/dist/hooks/trace_request.js +0 -25
  28. package/dist/hooks/trace_request.spec.js +0 -60
  29. package/dist/hooks/trace_response.d.ts +0 -6
  30. package/dist/hooks/trace_response.js +0 -26
  31. package/dist/hooks/trace_response.spec.js +0 -68
  32. package/dist/index.integration.test.js +0 -389
  33. package/dist/utils/create_trace_id.d.ts +0 -13
  34. package/dist/utils/create_trace_id.js +0 -20
  35. package/dist/utils/create_trace_id.spec.d.ts +0 -1
  36. package/dist/utils/create_trace_id.spec.js +0 -20
  37. package/dist/utils/index.d.ts +0 -4
  38. package/dist/utils/index.js +0 -4
  39. package/dist/utils/parse_request_body.d.ts +0 -7
  40. package/dist/utils/parse_request_body.js +0 -19
  41. package/dist/utils/parse_request_body.spec.d.ts +0 -1
  42. package/dist/utils/parse_request_body.spec.js +0 -19
  43. package/dist/utils/parse_response_body.d.ts +0 -10
  44. package/dist/utils/parse_response_body.js +0 -14
  45. package/dist/utils/parse_response_body.spec.d.ts +0 -1
  46. package/dist/utils/parse_response_body.spec.js +0 -19
  47. package/dist/utils/redact_headers.d.ts +0 -6
  48. package/dist/utils/redact_headers.js +0 -27
  49. package/dist/utils/redact_headers.spec.d.ts +0 -1
  50. package/dist/utils/redact_headers.spec.js +0 -245
  51. /package/dist/{hooks/trace_error.spec.d.ts → cost.spec.d.ts} +0 -0
  52. /package/dist/{hooks/trace_request.spec.d.ts → fetch/index.spec.d.ts} +0 -0
  53. /package/dist/{hooks/trace_response.spec.d.ts → fetch/logger.spec.d.ts} +0 -0
  54. /package/dist/{index.integration.test.d.ts → fetch/utils.spec.d.ts} +0 -0
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Symbol used to store request id in the response object.
3
+ */
4
+ export declare const requestIdSymbol: unique symbol;
package/dist/consts.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Symbol used to store request id in the response object.
3
+ */
4
+ export const requestIdSymbol = Symbol('request_id');
package/dist/cost.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { KyResponse } from 'ky';
2
+ export type RequestCost = {
3
+ total: number;
4
+ components?: Array<{
5
+ name: string;
6
+ value: number;
7
+ }>;
8
+ };
9
+ /**
10
+ * Attach cost information to the trace of an HTTP Request using the response
11
+ *
12
+ * @param response - The response of the HTTP Request to attach the information
13
+ * @param cost - The cost information
14
+ * @returns
15
+ */
16
+ export declare const addRequestCost: (response: KyResponse | Response, cost: RequestCost) => void;
package/dist/cost.js ADDED
@@ -0,0 +1,18 @@
1
+ import { Tracing, emitEvent } from '@outputai/core/sdk_activity_integration';
2
+ import { requestIdSymbol } from './consts.js';
3
+ /**
4
+ * Attach cost information to the trace of an HTTP Request using the response
5
+ *
6
+ * @param response - The response of the HTTP Request to attach the information
7
+ * @param cost - The cost information
8
+ * @returns
9
+ */
10
+ export const addRequestCost = (response, cost) => {
11
+ const eventId = Reflect.get(response, requestIdSymbol);
12
+ if (!eventId) {
13
+ console.warn('addRequestCost(): The "response" argument did not originate from @outputai/http, no costs were added.');
14
+ return;
15
+ }
16
+ Tracing.addEventAttribute({ eventId, name: Tracing.Attribute.COST, value: cost });
17
+ emitEvent('cost:http:request', { requestId: eventId, url: response.url, cost });
18
+ };
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { requestIdSymbol } from './consts.js';
3
+ vi.mock('@outputai/core/sdk_activity_integration', () => ({
4
+ Tracing: {
5
+ addEventAttribute: vi.fn(),
6
+ Attribute: {
7
+ COST: 'cost'
8
+ }
9
+ },
10
+ emitEvent: vi.fn()
11
+ }));
12
+ import { Tracing, emitEvent } from '@outputai/core/sdk_activity_integration';
13
+ import { addRequestCost } from './cost.js';
14
+ const tracing = vi.mocked(Tracing, true);
15
+ const emit = vi.mocked(emitEvent, true);
16
+ describe('addRequestCost', () => {
17
+ beforeEach(() => {
18
+ tracing.addEventAttribute.mockClear();
19
+ emit.mockClear();
20
+ vi.spyOn(console, 'warn').mockImplementation(() => { });
21
+ });
22
+ afterEach(() => {
23
+ vi.restoreAllMocks();
24
+ });
25
+ it('shortcircuits when the response has no http request id', () => {
26
+ const response = new Response();
27
+ const cost = { total: 1 };
28
+ addRequestCost(response, cost);
29
+ expect(console.warn).toHaveBeenCalledWith('addRequestCost(): The "response" argument did not originate from @outputai/http, no costs were added.');
30
+ expect(tracing.addEventAttribute).not.toHaveBeenCalled();
31
+ expect(emit).not.toHaveBeenCalled();
32
+ });
33
+ it('records cost on the trace event when the response carries the request id', () => {
34
+ const response = new Response(undefined, { status: 200 });
35
+ Reflect.set(response, requestIdSymbol, 'evt-cost-1');
36
+ const cost = { total: 2.5 };
37
+ addRequestCost(response, cost);
38
+ expect(console.warn).not.toHaveBeenCalled();
39
+ expect(tracing.addEventAttribute).toHaveBeenCalledWith({
40
+ eventId: 'evt-cost-1',
41
+ name: Tracing.Attribute.COST,
42
+ value: cost
43
+ });
44
+ expect(emit).toHaveBeenCalledWith('cost:http:request', {
45
+ requestId: 'evt-cost-1',
46
+ url: response.url,
47
+ cost
48
+ });
49
+ });
50
+ it('forwards multiple components to tracing', () => {
51
+ const response = new Response();
52
+ Reflect.set(response, requestIdSymbol, 'evt-cost-2');
53
+ const cost = {
54
+ total: 10,
55
+ components: [
56
+ { name: 'input', value: 3 },
57
+ { name: 'output', value: 7 }
58
+ ]
59
+ };
60
+ addRequestCost(response, cost);
61
+ expect(tracing.addEventAttribute).toHaveBeenCalledWith({
62
+ eventId: 'evt-cost-2',
63
+ name: Tracing.Attribute.COST,
64
+ value: cost
65
+ });
66
+ expect(emit).toHaveBeenCalledWith('cost:http:request', {
67
+ requestId: 'evt-cost-2',
68
+ url: response.url,
69
+ cost
70
+ });
71
+ });
72
+ it('forwards an empty components array to tracing', () => {
73
+ const response = new Response();
74
+ Reflect.set(response, requestIdSymbol, 'evt-cost-3');
75
+ const cost = { total: 1, components: [] };
76
+ addRequestCost(response, cost);
77
+ expect(tracing.addEventAttribute).toHaveBeenCalledWith({
78
+ eventId: 'evt-cost-3',
79
+ name: Tracing.Attribute.COST,
80
+ value: cost
81
+ });
82
+ expect(emit).toHaveBeenCalledWith('cost:http:request', {
83
+ requestId: 'evt-cost-3',
84
+ url: response.url,
85
+ cost
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,22 @@
1
+ import type { RequestInfo, RequestInit } from 'undici';
2
+ /** Re-export undici library for convenience. */
3
+ export * as undici from 'undici';
4
+ /** Export fetch input types. Also available under in undici.* export. */
5
+ export type {
6
+ /** Undici's fetch first argument: Either a URL string, a URL object or a undici.Request object. */
7
+ RequestInfo,
8
+ /** Undici's fetch second argument: A plain object containing HTTP options. */
9
+ RequestInit };
10
+ /**
11
+ * A fetch compliant function, that wraps undici's fetch.
12
+ *
13
+ * Behaves the same as any fetch function except:
14
+ * - Sets a request header called `x-request--trace-id` with a random UUID;
15
+ * - Sends the request, response, error and/or failure to the Trace system;
16
+ *
17
+ * @see {@link https://fetch.spec.whatwg.org/}
18
+ * @param input - URL string, URL object or Request object (undici's or Node's)
19
+ * @param init - Request options
20
+ * @returns The HTTP response
21
+ */
22
+ export declare const fetch: (input: RequestInfo | Request, init?: RequestInit) => Promise<Response>;
@@ -0,0 +1,48 @@
1
+ import * as undici from 'undici';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { logRequest, logResponse, logError, logFailure } from './logger.js';
4
+ import { addRequestIdToResponse } from './utils.js';
5
+ /*
6
+ * Unifies undici and nodes realms
7
+ * https://github.com/nodejs/undici#keep-fetch-and-formdata-together
8
+ */
9
+ undici.install();
10
+ /** Re-export undici library for convenience. */
11
+ export * as undici from 'undici';
12
+ /**
13
+ * A fetch compliant function, that wraps undici's fetch.
14
+ *
15
+ * Behaves the same as any fetch function except:
16
+ * - Sets a request header called `x-request--trace-id` with a random UUID;
17
+ * - Sends the request, response, error and/or failure to the Trace system;
18
+ *
19
+ * @see {@link https://fetch.spec.whatwg.org/}
20
+ * @param input - URL string, URL object or Request object (undici's or Node's)
21
+ * @param init - Request options
22
+ * @returns The HTTP response
23
+ */
24
+ export const fetch = async (input, init) => {
25
+ // Creates a Request object with the many shapes RequestInfo can have
26
+ const base = new undici.Request(input, init);
27
+ // Creates a headers object with the many shapes Request.Headers can have (object, array, Headers)
28
+ const headers = new undici.Headers(base.headers);
29
+ const requestId = randomUUID();
30
+ headers.set('x-request-trace-id', requestId);
31
+ const request = new undici.Request(base, { headers });
32
+ await logRequest({ requestId, request });
33
+ try {
34
+ const response = await undici.fetch(request);
35
+ // This enriches the response of the request id, so it is identifiable later.
36
+ addRequestIdToResponse(response, requestId);
37
+ if (response.status > 399) {
38
+ await logError({ requestId, response });
39
+ return response;
40
+ }
41
+ await logResponse({ requestId, response });
42
+ return response;
43
+ }
44
+ catch (error) {
45
+ logFailure({ requestId, error: error });
46
+ throw error;
47
+ }
48
+ };
@@ -0,0 +1,218 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { getGlobalDispatcher, Headers, MockAgent, Request, setGlobalDispatcher } from 'undici';
3
+ const FIXED_REQUEST_ID = 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee';
4
+ const randomUUIDMock = vi.hoisted(() => vi.fn(() => FIXED_REQUEST_ID));
5
+ const loggerMock = vi.hoisted(() => ({
6
+ logRequest: vi.fn(async (_args) => { }),
7
+ logResponse: vi.fn(async (_args) => { }),
8
+ logError: vi.fn((_args) => { }),
9
+ logFailure: vi.fn((_args) => { })
10
+ }));
11
+ const utilsMock = vi.hoisted(() => ({
12
+ addRequestIdToResponse: vi.fn()
13
+ }));
14
+ vi.mock('node:crypto', () => ({
15
+ randomUUID: () => randomUUIDMock()
16
+ }));
17
+ vi.mock('./logger.js', () => ({
18
+ logRequest: loggerMock.logRequest,
19
+ logResponse: loggerMock.logResponse,
20
+ logError: loggerMock.logError,
21
+ logFailure: loggerMock.logFailure
22
+ }));
23
+ vi.mock('./utils.js', () => ({
24
+ addRequestIdToResponse: utilsMock.addRequestIdToResponse
25
+ }));
26
+ import { fetch } from './index.js';
27
+ const MOCK_ORIGIN = 'https://fetch-index.undici.test';
28
+ describe('fetch/index', () => {
29
+ const undiciCtx = {
30
+ mockAgent: undefined,
31
+ previousDispatcher: undefined
32
+ };
33
+ beforeEach(() => {
34
+ undiciCtx.mockAgent = new MockAgent();
35
+ undiciCtx.mockAgent.disableNetConnect();
36
+ undiciCtx.previousDispatcher = getGlobalDispatcher();
37
+ setGlobalDispatcher(undiciCtx.mockAgent);
38
+ loggerMock.logRequest.mockClear();
39
+ loggerMock.logResponse.mockClear();
40
+ loggerMock.logError.mockClear();
41
+ loggerMock.logFailure.mockClear();
42
+ utilsMock.addRequestIdToResponse.mockClear();
43
+ randomUUIDMock.mockClear();
44
+ randomUUIDMock.mockImplementation(() => FIXED_REQUEST_ID);
45
+ });
46
+ afterEach(async () => {
47
+ if (undiciCtx.previousDispatcher !== undefined) {
48
+ setGlobalDispatcher(undiciCtx.previousDispatcher);
49
+ }
50
+ if (undiciCtx.mockAgent !== undefined) {
51
+ await undiciCtx.mockAgent.close();
52
+ }
53
+ });
54
+ describe('fetch with MockAgent', () => {
55
+ it('returns 200, traces request start and response end', async () => {
56
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/ok', method: 'GET' }).reply(200, 'hello', { headers: { 'content-type': 'text/plain' } });
57
+ const response = await fetch(`${MOCK_ORIGIN}/ok`);
58
+ expect(response.status).toBe(200);
59
+ expect(await response.text()).toBe('hello');
60
+ expect(loggerMock.logRequest).toHaveBeenCalledTimes(1);
61
+ expect(loggerMock.logRequest.mock.calls[0][0].requestId).toBe(FIXED_REQUEST_ID);
62
+ expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('GET');
63
+ expect(loggerMock.logRequest.mock.calls[0][0].request.url).toBe(`${MOCK_ORIGIN}/ok`);
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);
67
+ expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledTimes(1);
68
+ expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledWith(response, FIXED_REQUEST_ID);
69
+ expect(loggerMock.logError).not.toHaveBeenCalled();
70
+ expect(loggerMock.logFailure).not.toHaveBeenCalled();
71
+ });
72
+ it('uses logError for status > 399 without logging response end', async () => {
73
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/missing', method: 'GET' }).reply(404, 'nope', { headers: { 'content-type': 'text/plain' } });
74
+ const response = await fetch(`${MOCK_ORIGIN}/missing`);
75
+ expect(response.status).toBe(404);
76
+ expect(loggerMock.logRequest).toHaveBeenCalledTimes(1);
77
+ expect(loggerMock.logResponse).not.toHaveBeenCalled();
78
+ expect(loggerMock.logError).toHaveBeenCalledTimes(1);
79
+ expect(loggerMock.logError.mock.calls[0][0].requestId).toBe(FIXED_REQUEST_ID);
80
+ expect(loggerMock.logError.mock.calls[0][0].response).toBe(response);
81
+ expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledTimes(1);
82
+ expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledWith(response, FIXED_REQUEST_ID);
83
+ });
84
+ it('treats status 399 as success (logs response end, not HTTP error)', async () => {
85
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/edge', method: 'GET' }).reply(399, '', { headers: { 'content-type': 'text/plain' } });
86
+ const response = await fetch(`${MOCK_ORIGIN}/edge`);
87
+ expect(response.status).toBe(399);
88
+ expect(loggerMock.logResponse).toHaveBeenCalledTimes(1);
89
+ expect(loggerMock.logError).not.toHaveBeenCalled();
90
+ });
91
+ it('sends x-request-trace-id and custom headers when init.headers is a plain object', async () => {
92
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({
93
+ path: '/with-plain-headers',
94
+ method: 'GET',
95
+ headers: {
96
+ 'x-request-trace-id': FIXED_REQUEST_ID,
97
+ 'x-custom': 'plain-value'
98
+ }
99
+ }).reply(200, 'ok');
100
+ const response = await fetch(`${MOCK_ORIGIN}/with-plain-headers`, {
101
+ headers: { 'X-Custom': 'plain-value' }
102
+ });
103
+ expect(response.status).toBe(200);
104
+ const { request } = loggerMock.logRequest.mock.calls[0][0];
105
+ expect(request.headers.get('x-request-trace-id')).toBe(FIXED_REQUEST_ID);
106
+ expect(request.headers.get('x-custom')).toBe('plain-value');
107
+ });
108
+ it('sends x-request-trace-id and custom headers when init.headers is a Headers instance', async () => {
109
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({
110
+ path: '/with-headers-instance',
111
+ method: 'GET',
112
+ headers: {
113
+ 'x-request-trace-id': FIXED_REQUEST_ID,
114
+ 'x-from-headers': 'yes'
115
+ }
116
+ }).reply(200, 'ok');
117
+ const userHeaders = new Headers();
118
+ userHeaders.set('X-From-Headers', 'yes');
119
+ const response = await fetch(`${MOCK_ORIGIN}/with-headers-instance`, {
120
+ headers: userHeaders
121
+ });
122
+ expect(response.status).toBe(200);
123
+ const { request } = loggerMock.logRequest.mock.calls[0][0];
124
+ expect(request.headers.get('x-request-trace-id')).toBe(FIXED_REQUEST_ID);
125
+ expect(request.headers.get('x-from-headers')).toBe('yes');
126
+ });
127
+ it('calls logFailure when the mock responds with replyWithError', async () => {
128
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/boom', method: 'GET' }).replyWithError(new Error('simulated network failure'));
129
+ await expect(fetch(`${MOCK_ORIGIN}/boom`)).rejects.toThrow('fetch failed');
130
+ expect(loggerMock.logRequest).toHaveBeenCalledTimes(1);
131
+ expect(loggerMock.logResponse).not.toHaveBeenCalled();
132
+ expect(loggerMock.logFailure).toHaveBeenCalledTimes(1);
133
+ expect(loggerMock.logFailure.mock.calls[0][0].requestId).toBe(FIXED_REQUEST_ID);
134
+ const failure = loggerMock.logFailure.mock.calls[0][0].error;
135
+ expect(failure).toBeInstanceOf(TypeError);
136
+ expect(failure.message).toBe('fetch failed');
137
+ expect(failure.cause).toBeInstanceOf(Error);
138
+ expect(failure.cause.message).toBe('simulated network failure');
139
+ });
140
+ it('fails when no mock matches (disabled net)', async () => {
141
+ await expect(fetch(`${MOCK_ORIGIN}/unmocked`)).rejects.toThrow();
142
+ expect(loggerMock.logRequest).toHaveBeenCalledTimes(1);
143
+ expect(loggerMock.logResponse).not.toHaveBeenCalled();
144
+ expect(loggerMock.logFailure).toHaveBeenCalledTimes(1);
145
+ expect(utilsMock.addRequestIdToResponse).not.toHaveBeenCalled();
146
+ });
147
+ it('passes method and body through to undici for POST JSON', async () => {
148
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/create', method: 'POST' }).reply(201, JSON.stringify({ id: 1 }), { headers: { 'content-type': 'application/json' } });
149
+ const response = await fetch(`${MOCK_ORIGIN}/create`, {
150
+ method: 'POST',
151
+ headers: { 'content-type': 'application/json' },
152
+ body: JSON.stringify({ name: 'a' })
153
+ });
154
+ expect(response.status).toBe(201);
155
+ expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('POST');
156
+ expect(loggerMock.logResponse).toHaveBeenCalledTimes(1);
157
+ });
158
+ it('works when the second argument is omitted', async () => {
159
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/bare', method: 'GET' }).reply(204);
160
+ const response = await fetch(`${MOCK_ORIGIN}/bare`);
161
+ expect(response.status).toBe(204);
162
+ expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('GET');
163
+ });
164
+ });
165
+ describe('fetch RequestInfo / RequestInit shapes', () => {
166
+ it('accepts a URL object as the first argument', async () => {
167
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/from-url', method: 'GET' }).reply(200, 'url-ok', { headers: { 'content-type': 'text/plain' } });
168
+ const href = new URL('/from-url', `${MOCK_ORIGIN}/`);
169
+ const response = await fetch(href);
170
+ expect(response.status).toBe(200);
171
+ expect(await response.text()).toBe('url-ok');
172
+ expect(loggerMock.logRequest.mock.calls[0][0].request.url).toBe(href.href);
173
+ });
174
+ it('accepts a Request as the first argument (no init)', async () => {
175
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/req-only', method: 'GET' }).reply(200, 'r1');
176
+ const input = new Request(`${MOCK_ORIGIN}/req-only`, { method: 'GET' });
177
+ const response = await fetch(input);
178
+ expect(response.status).toBe(200);
179
+ expect(await response.text()).toBe('r1');
180
+ const { request } = loggerMock.logRequest.mock.calls[0][0];
181
+ expect(request.method).toBe('GET');
182
+ expect(request.url).toBe(`${MOCK_ORIGIN}/req-only`);
183
+ });
184
+ it('accepts Request plus init that overrides method and body', async () => {
185
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/req-plus-init', method: 'POST' }).reply(201, JSON.stringify({ saved: true }), { headers: { 'content-type': 'application/json' } });
186
+ const input = new Request(`${MOCK_ORIGIN}/req-plus-init`, { method: 'GET' });
187
+ const response = await fetch(input, {
188
+ method: 'POST',
189
+ headers: { 'content-type': 'application/json' },
190
+ body: JSON.stringify({ name: 'override' })
191
+ });
192
+ expect(response.status).toBe(201);
193
+ expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('POST');
194
+ expect(loggerMock.logResponse).toHaveBeenCalledTimes(1);
195
+ });
196
+ it('accepts string URL with explicit undefined init', async () => {
197
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/explicit-undefined', method: 'GET' }).reply(200, 'ok');
198
+ const response = await fetch(`${MOCK_ORIGIN}/explicit-undefined`, undefined);
199
+ expect(response.status).toBe(200);
200
+ expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('GET');
201
+ });
202
+ it('accepts URL plus init with method and headers', async () => {
203
+ undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({
204
+ path: '/url-post',
205
+ method: 'POST',
206
+ headers: { 'content-type': 'application/json', 'x-request-trace-id': FIXED_REQUEST_ID }
207
+ }).reply(200, '{}');
208
+ const href = new URL('/url-post', `${MOCK_ORIGIN}/`);
209
+ const response = await fetch(href, {
210
+ method: 'POST',
211
+ headers: { 'Content-Type': 'application/json' },
212
+ body: '{}'
213
+ });
214
+ expect(response.status).toBe(200);
215
+ expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('POST');
216
+ });
217
+ });
218
+ });
@@ -0,0 +1,45 @@
1
+ import type { Request, Response } from 'undici';
2
+ /**
3
+ * Sends the trace start event for an http request
4
+ *
5
+ * @param options
6
+ * @param options.requestId - id of the request
7
+ * @param options.request - The HTTP Request object
8
+ */
9
+ export declare const logRequest: ({ requestId, request }: {
10
+ requestId: string;
11
+ request: Request;
12
+ }) => Promise<void>;
13
+ /**
14
+ * Sends the trace error event for an http response with error status
15
+ *
16
+ * @param options
17
+ * @param options.requestId - id of the request
18
+ * @param options.response - The HTTP Response object
19
+ */
20
+ export declare const logError: ({ requestId, response }: {
21
+ requestId: string;
22
+ response: Response;
23
+ }) => Promise<void>;
24
+ /**
25
+ * Sends the trace end event for an http response
26
+ *
27
+ * @param {object} options
28
+ * @param options.requestId - id of the request
29
+ * @param {Response} options.response - The HTTP Response object
30
+ */
31
+ export declare const logResponse: ({ requestId, response }: {
32
+ requestId: string;
33
+ response: Response;
34
+ }) => Promise<void>;
35
+ /**
36
+ * Creates the trace error event for a network/connection failure
37
+ *
38
+ * @param options
39
+ * @param options.requestId - id of the request
40
+ * @param options.error - The error thrown
41
+ */
42
+ export declare const logFailure: ({ requestId, error }: {
43
+ requestId: string;
44
+ error: Error;
45
+ }) => void;
@@ -0,0 +1,57 @@
1
+ import { Tracing } from '@outputai/core/sdk_activity_integration';
2
+ import { config } from '../config.js';
3
+ import { parseBody, redactHeaders, serializeError } from './utils.js';
4
+ /**
5
+ * Sends the trace start event for an http request
6
+ *
7
+ * @param options
8
+ * @param options.requestId - id of the request
9
+ * @param options.request - The HTTP Request object
10
+ */
11
+ export const logRequest = async ({ requestId, request }) => {
12
+ Tracing.addEventStart({
13
+ id: requestId, kind: 'http', name: 'request', details: {
14
+ method: request.method,
15
+ url: request.url,
16
+ ...(config.logVerbose && { headers: redactHeaders(request.headers), body: await parseBody(request) })
17
+ }
18
+ });
19
+ Tracing.addEventAttribute({ eventId: requestId, name: 'requestId', value: requestId });
20
+ };
21
+ /**
22
+ * Sends the trace error event for an http response with error status
23
+ *
24
+ * @param options
25
+ * @param options.requestId - id of the request
26
+ * @param options.response - The HTTP Response object
27
+ */
28
+ export const logError = async ({ requestId, response }) => Tracing.addEventError({
29
+ id: requestId, details: {
30
+ status: response.status,
31
+ statusText: response.statusText,
32
+ headers: redactHeaders(response.headers),
33
+ body: await parseBody(response)
34
+ }
35
+ });
36
+ /**
37
+ * Sends the trace end event for an http response
38
+ *
39
+ * @param {object} options
40
+ * @param options.requestId - id of the request
41
+ * @param {Response} options.response - The HTTP Response object
42
+ */
43
+ export const logResponse = async ({ requestId, response }) => Tracing.addEventEnd({
44
+ id: requestId, details: {
45
+ status: response.status,
46
+ statusText: response.statusText,
47
+ ...(config.logVerbose && { headers: redactHeaders(response.headers), body: await parseBody(response) })
48
+ }
49
+ });
50
+ /**
51
+ * Creates the trace error event for a network/connection failure
52
+ *
53
+ * @param options
54
+ * @param options.requestId - id of the request
55
+ * @param options.error - The error thrown
56
+ */
57
+ export const logFailure = ({ requestId, error }) => Tracing.addEventError({ id: requestId, details: serializeError(error) });