@outputai/http 0.5.2-next.93dd22e.0 → 0.5.2-next.b54869d.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.
package/dist/cost.spec.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Response } from 'undici';
2
3
  import { requestIdSymbol } from './consts.js';
3
4
  vi.mock('@outputai/core/sdk_activity_integration', () => {
4
5
  class HTTPRequestCost {
@@ -25,6 +26,7 @@ vi.mock('@outputai/core/sdk_activity_integration', () => {
25
26
  });
26
27
  import { Tracing, emitEvent } from '@outputai/core/sdk_activity_integration';
27
28
  import { addRequestCost } from './cost.js';
29
+ import { addRequestIdToResponse } from './fetch/utils.js';
28
30
  const tracing = vi.mocked(Tracing, true);
29
31
  const emit = vi.mocked(emitEvent, true);
30
32
  describe('addRequestCost', () => {
@@ -79,4 +81,25 @@ describe('addRequestCost', () => {
79
81
  const attribute = tracing.addEventAttribute.mock.calls[0][0].attribute;
80
82
  expect(emit).toHaveBeenCalledWith('cost:http:request', attribute);
81
83
  });
84
+ // ky clones the response before passing it to afterResponse hooks. Without
85
+ // the clone-propagation patch in addRequestIdToResponse, this path silently
86
+ // dropped cost (warned "did not originate from @outputai/http") for every
87
+ // service client that emits cost from inside a ky hook.
88
+ it('records cost on a cloned response (regression: ky afterResponse hooks)', () => {
89
+ const response = new Response(undefined, { status: 200 });
90
+ addRequestIdToResponse(response, 'evt-clone-1');
91
+ const cloned = response.clone();
92
+ addRequestCost(cloned, 4.2);
93
+ expect(console.warn).not.toHaveBeenCalled();
94
+ expect(tracing.addEventAttribute).toHaveBeenCalledWith({
95
+ eventId: 'evt-clone-1',
96
+ attribute: expect.objectContaining({
97
+ type: Tracing.Attribute.HTTPRequestCost.TYPE,
98
+ requestId: 'evt-clone-1',
99
+ total: 4.2
100
+ })
101
+ });
102
+ const attribute = tracing.addEventAttribute.mock.calls[0][0].attribute;
103
+ expect(emit).toHaveBeenCalledWith('cost:http:request', attribute);
104
+ });
82
105
  });
@@ -35,11 +35,11 @@ export declare const redactHeaders: (headers: Headers) => Record<string, unknown
35
35
  */
36
36
  export declare const parseBody: (r: Request | Response) => Promise<string | object>;
37
37
  /**
38
- * Adds a non-enumerable, non-configurable and non-writable property to a response.
39
- *
40
- * This property is identified by a unique symbol and contains the id of the request.
41
- *
42
- * @param response
43
- * @param requestId
38
+ * Tag a response in place with its request id so downstream code (e.g.
39
+ * `addRequestCost`) can correlate. Stores the id under a private symbol AND
40
+ * patches `clone()` so the tag propagates to clones ky clones the response
41
+ * before invoking `afterResponse` hooks, and undici headers are immutable on
42
+ * received responses, so a symbol re-attached inside `clone()` is the only
43
+ * path that survives.
44
44
  */
45
- export declare const addRequestIdToResponse: (response: Response, requestId: string) => Response;
45
+ export declare const addRequestIdToResponse: (response: Response, requestId: string) => void;
@@ -91,11 +91,24 @@ export const parseBody = async (r) => {
91
91
  }
92
92
  };
93
93
  /**
94
- * Adds a non-enumerable, non-configurable and non-writable property to a response.
95
- *
96
- * This property is identified by a unique symbol and contains the id of the request.
97
- *
98
- * @param response
99
- * @param requestId
94
+ * Tag a response in place with its request id so downstream code (e.g.
95
+ * `addRequestCost`) can correlate. Stores the id under a private symbol AND
96
+ * patches `clone()` so the tag propagates to clones ky clones the response
97
+ * before invoking `afterResponse` hooks, and undici headers are immutable on
98
+ * received responses, so a symbol re-attached inside `clone()` is the only
99
+ * path that survives.
100
100
  */
101
- export const addRequestIdToResponse = (response, requestId) => Object.defineProperty(response, requestIdSymbol, { value: requestId, enumerable: false, configurable: false, writable: false });
101
+ export const addRequestIdToResponse = (response, requestId) => {
102
+ Object.defineProperty(response, requestIdSymbol, { value: requestId, enumerable: false, configurable: false, writable: false });
103
+ const originalClone = response.clone.bind(response);
104
+ Object.defineProperty(response, 'clone', {
105
+ value: function clone() {
106
+ const cloned = originalClone();
107
+ addRequestIdToResponse(cloned, requestId);
108
+ return cloned;
109
+ },
110
+ enumerable: false,
111
+ configurable: true,
112
+ writable: true
113
+ });
114
+ };
@@ -252,10 +252,9 @@ describe('fetch/utils', () => {
252
252
  });
253
253
  });
254
254
  describe('addRequestIdToResponse', () => {
255
- it('stores request id under requestIdSymbol and returns the same response', () => {
255
+ it('stores request id under requestIdSymbol on the response', () => {
256
256
  const response = new Response('ok');
257
- const enriched = addRequestIdToResponse(response, 'req-123');
258
- expect(enriched).toBe(response);
257
+ addRequestIdToResponse(response, 'req-123');
259
258
  expect(response[requestIdSymbol]).toBe('req-123');
260
259
  });
261
260
  it('defines request id as non-enumerable, non-writable and non-configurable', () => {
@@ -270,5 +269,14 @@ describe('fetch/utils', () => {
270
269
  value: 'req-456'
271
270
  });
272
271
  });
272
+ it('propagates the request id to clones (ky afterResponse passes the clone)', () => {
273
+ const response = new Response('ok');
274
+ addRequestIdToResponse(response, 'req-789');
275
+ const cloned = response.clone();
276
+ expect(cloned[requestIdSymbol]).toBe('req-789');
277
+ // recursion: clones of clones inherit too
278
+ const grandchild = cloned.clone();
279
+ expect(grandchild[requestIdSymbol]).toBe('req-789');
280
+ });
273
281
  });
274
282
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/http",
3
- "version": "0.5.2-next.93dd22e.0",
3
+ "version": "0.5.2-next.b54869d.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.5.2-next.93dd22e.0"
14
+ "@outputai/core": "0.5.2-next.b54869d.0"
15
15
  },
16
16
  "license": "Apache-2.0",
17
17
  "publishConfig": {