Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @agentuity/sdk Changelog

## 0.0.152

### Patch Changes

- Fixed issue with otel headers not propagating for outbound services

## 0.0.151

### Patch Changes
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentuity/sdk",
"version": "0.0.151",
"version": "0.0.152",
"description": "The Agentuity SDK for NodeJS and Bun",
"license": "Apache-2.0",
"public": true,
Expand Down Expand Up @@ -97,4 +97,4 @@
"mailparser": "^3.7.4",
"nodemailer": "^7.0.3"
}
}
}
10 changes: 10 additions & 0 deletions src/apis/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { context, propagation } from '@opentelemetry/api';
import { getSDKVersion } from '../router/router';
import { isReadableStream } from '../types';

Expand Down Expand Up @@ -141,6 +142,15 @@ export async function send<K>(
for (const key in request.headers) {
headers[key] = request.headers[key];
}

// inject trace context headers if there's an active context
const currentContext = context.active();
propagation.inject(currentContext, headers, {
set: (carrier, key, value) => {
carrier[key] = value;
},
});
Comment on lines +145 to +152

@coderabbitai coderabbitai Bot Oct 7, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify trace header override behavior.

The trace context injection occurs after custom headers are copied (lines 142-144), which means propagation.inject will overwrite any traceparent or other trace headers passed in request.headers.

However, the test at lines 688-706 in test/apis/api.test.ts expects existing trace headers to be preserved. If there's an active OpenTelemetry context, the propagation.inject call will replace the custom traceparent header, potentially causing the test to fail in scenarios with an active trace context.

Run the following script to verify the test expectations:


🏁 Script executed:

#!/bin/bash
# Check if tests properly validate the interaction between custom traceparent headers and active context
rg -nP "should preserve existing trace headers" test/apis/api.test.ts -A 20

Length of output: 724


🏁 Script executed:

#!/bin/bash
sed -n '130,180p' src/apis/api.ts

Length of output: 1352


🏁 Script executed:

#!/bin/bash
rg -nP "import.*(context|propagation)" src/apis/api.ts

Length of output: 60


Prevent trace header override
The injected setter must only add missing headers—otherwise propagation.inject will overwrite any custom traceparent. Change to:

propagation.inject(currentContext, headers, {
  set: (carrier, key, value) => {
    if (!(key in carrier)) {
      carrier[key] = value;
    }
  },
});

This ensures existing trace headers in request.headers are preserved.

🤖 Prompt for AI Agents
In src/apis/api.ts around lines 145 to 152, the propagation.inject setter
currently overwrites existing headers which can replace custom trace headers;
update the setter so it only assigns the value when the header key is not
already present in the carrier (i.e., check if key is absent before setting) to
preserve any existing trace headers in request.headers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we want it to override

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the test that was mentioned to see if it conflicts with the override behavior
sed -n '688,706p' test/apis/api.test.ts

Length of output: 557


@jhaynie, understood! The current implementation is correct for your use case—when there's an active OpenTelemetry context, it should override any manually passed trace headers.

However, I noticed that the test "should preserve existing trace headers passed in request" (lines 688-706 in test/apis/api.test.ts) might be misleading. It currently passes because there's no active OpenTelemetry context set up in that test, so propagation.inject doesn't inject anything, leaving the custom traceparent intact.

To properly test the override behavior, you might want to either:

  1. Update the test name and documentation to clarify it only preserves headers when there's no active context
  2. Add a separate test that verifies active context does override custom headers (using context.with() to set up an active span)

This will make the test suite more explicit about the intended behavior.


🧬 Code Graph Analysis Results

[src/apis/api.ts] lines 1-14

  • Complete snippet:
import { context, propagation } from '@opentelemetry/api';
import { getSDKVersion } from '../router/router';
import { isReadableStream } from '../types';

// allow the fetch function to be overridden
let apiFetch = globalThis.fetch;

// only used in tests
export function setFetch(f: typeof fetch) {
	apiFetch = f;
}

export function getFetch() {
	return apiFetch;
}

Reason: This defines the override mechanism for fetch, which directly relates to the user’s note “we want it to override.”


✏️ Learnings added
Learnt from: jhaynie
PR: agentuity/sdk-js#197
File: src/apis/api.ts:145-152
Timestamp: 2025-10-07T02:33:29.185Z
Learning: In src/apis/api.ts, OpenTelemetry trace context propagation is intentionally designed to override any manually passed trace headers (like traceparent) when there's an active context. This ensures the active trace context takes precedence.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


// this shouldn't be overridden
headers.Authorization = `Bearer ${apiKey}`;
const init: RequestInit & { duplex?: 'half' } = {
Expand Down
3 changes: 1 addition & 2 deletions src/apis/prompt/generated/_index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export const prompts = {
};
export const prompts = {};

// Export types for compatibility
export const PromptConfig = undefined; // Type-only export, value is not used
Expand Down
2 changes: 1 addition & 1 deletion src/apis/vector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default class VectorAPI implements VectorStorage {
);
if (resp.status === 200) {
if (resp.json?.success) {
const json = resp.json as unknown as { data: { id: string; }[]; };
const json = resp.json as unknown as { data: { id: string }[] };
span.setStatus({ code: SpanStatusCode.OK });
return json.data.map((o) => o.id);
}
Expand Down
84 changes: 84 additions & 0 deletions test/apis/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '../../src/apis/api';
import { createMockFetch } from '../setup';
import { ReadableStream } from 'node:stream/web';
import { context, ROOT_CONTEXT } from '@opentelemetry/api';

describe('API Client', () => {
let originalEnv: NodeJS.ProcessEnv;
Expand Down Expand Up @@ -646,4 +647,87 @@ describe('API Client', () => {
expect(getFetch()).toBe(globalThis.fetch);
});
});

describe('OpenTelemetry trace context propagation', () => {
it('should pass through traceparent header when provided', async () => {
const traceparent =
'00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';

await send({
method: 'GET',
path: '/test',
body: undefined as never,
headers: {
traceparent,
},
});

expect(fetchCalls.length).toBeGreaterThan(0);
const [, options] = fetchCalls[0];
const headers = options?.headers as Record<string, string>;

expect(headers?.traceparent).toBe(traceparent);
});

it('should not inject traceparent header when there is no active context', async () => {
await context.with(ROOT_CONTEXT, async () => {
await send({
method: 'GET',
path: '/test',
body: undefined as never,
});

expect(fetchCalls.length).toBeGreaterThan(0);
const [, options] = fetchCalls[0];
const headers = options?.headers as Record<string, string>;

expect(headers?.traceparent).toBeUndefined();
});
});

it('should preserve existing trace headers passed in request', async () => {
const existingTraceparent =
'00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';

await send({
method: 'POST',
path: '/test',
body: JSON.stringify({ test: 'data' }),
headers: {
traceparent: existingTraceparent,
},
});

expect(fetchCalls.length).toBeGreaterThan(0);
const [, options] = fetchCalls[0];
const headers = options?.headers as Record<string, string>;

expect(headers?.traceparent).toBe(existingTraceparent);
});

it('should preserve custom headers when traceparent is included', async () => {
const traceparent =
'00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';

await send({
method: 'GET',
path: '/test',
body: undefined as never,
headers: {
traceparent,
'X-Custom-Header': 'custom-value',
'X-Another-Header': 'another-value',
},
});

expect(fetchCalls.length).toBeGreaterThan(0);
const [, options] = fetchCalls[0];
const headers = options?.headers as Record<string, string>;

expect(headers?.traceparent).toBe(traceparent);
expect(headers?.['X-Custom-Header']).toBe('custom-value');
expect(headers?.['X-Another-Header']).toBe('another-value');
expect(headers?.Authorization).toBe('Bearer test-api-key');
});
});
});
Loading