Skip to content

Commit e475eb4

Browse files
authored
feat: inject W3C traceparent header for request correlation (#362)
Generates a unique traceparent header (W3C Trace Context) on every outgoing SDK API request. The traceId maps to App Insights operation_Id, enabling end-to-end correlation between CF Worker logs and backend App Insights. - Added TRACEPARENT and UIPATH_TRACEPARENT_ID constants to headers.ts - Both traceparent and x-uipath-traceparent-id headers sent with same value - traceId and spanId generated independently per W3C spec - Header format: 00-{traceId:32hex}-{spanId:16hex}-01
1 parent 2361b75 commit e475eb4

3 files changed

Lines changed: 102 additions & 2 deletions

File tree

src/core/http/api-client.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { RequestSpec } from '../../models/common/request-spec';
44
import { TokenManager } from '../auth/token-manager';
55
import { errorResponseParser } from '../errors/parser';
66
import { ErrorFactory } from '../errors/error-factory';
7-
import { CONTENT_TYPES, RESPONSE_TYPES } from '../../utils/constants/headers';
7+
import { CONTENT_TYPES, RESPONSE_TYPES, TRACEPARENT, UIPATH_TRACEPARENT_ID } from '../../utils/constants/headers';
88

99
export interface ApiClientConfig {
1010
headers?: Record<string, string>;
@@ -64,8 +64,15 @@ export class ApiClient {
6464
if (isFormData) {
6565
delete defaultHeaders['Content-Type'];
6666
}
67-
const headers = {
67+
68+
const traceId = crypto.randomUUID().replace(/-/g, '');
69+
const spanId = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
70+
const traceparentValue = `00-${traceId}-${spanId}-01`;
71+
72+
const headers: Record<string, string> = {
6873
...defaultHeaders,
74+
[TRACEPARENT]: traceparentValue,
75+
[UIPATH_TRACEPARENT_ID]: traceparentValue,
6976
...options.headers
7077
};
7178

src/utils/constants/headers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const CORRELATION_ID = 'X-UIPATH-Correlation-Id';
77
export const JOB_KEY = 'X-UIPATH-JobKey';
88
export const FOLDER_ID = 'X-UIPATH-OrganizationUnitId';
99
export const INSTANCE_ID = 'X-UIPATH-InstanceId';
10+
export const TRACEPARENT = 'traceparent';
11+
export const UIPATH_TRACEPARENT_ID = 'x-uipath-traceparent-id';
1012

1113
/**
1214
* Content type constants for HTTP requests/responses
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { ApiClient } from '../../../../src/core/http/api-client';
3+
import { TEST_CONSTANTS } from '../../../utils/constants/common';
4+
5+
const TRACEPARENT_REGEX = /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/;
6+
7+
const mockTokenManager = {
8+
getValidToken: vi.fn().mockResolvedValue(TEST_CONSTANTS.DEFAULT_ACCESS_TOKEN),
9+
};
10+
11+
const mockConfig = {
12+
baseUrl: TEST_CONSTANTS.BASE_URL,
13+
orgName: TEST_CONSTANTS.ORGANIZATION_ID,
14+
tenantName: TEST_CONSTANTS.TENANT_ID,
15+
};
16+
17+
const mockExecutionContext = {};
18+
19+
let capturedHeaders: Record<string, string> = {};
20+
21+
beforeEach(() => {
22+
capturedHeaders = {};
23+
global.fetch = vi.fn().mockImplementation((_url: string, options: any) => {
24+
capturedHeaders = { ...options.headers };
25+
return Promise.resolve({
26+
ok: true,
27+
status: 200,
28+
text: () => Promise.resolve(JSON.stringify({ result: 'ok' })),
29+
});
30+
});
31+
});
32+
33+
function createClient(clientConfig = {}) {
34+
return new ApiClient(
35+
mockConfig as any,
36+
mockExecutionContext as any,
37+
mockTokenManager as any,
38+
clientConfig,
39+
);
40+
}
41+
42+
describe('ApiClient traceparent', () => {
43+
it('injects a traceparent header with correct W3C format', async () => {
44+
const client = createClient();
45+
await client.get('/test');
46+
47+
expect(capturedHeaders['traceparent']).toBeDefined();
48+
expect(capturedHeaders['traceparent']).toMatch(TRACEPARENT_REGEX);
49+
});
50+
51+
it('injects x-uipath-traceparent-id with same value as traceparent', async () => {
52+
const client = createClient();
53+
await client.get('/test');
54+
55+
expect(capturedHeaders['x-uipath-traceparent-id']).toBeDefined();
56+
expect(capturedHeaders['x-uipath-traceparent-id']).toBe(capturedHeaders['traceparent']);
57+
});
58+
59+
it('generates different traceId and spanId values', async () => {
60+
const client = createClient();
61+
await client.get('/test');
62+
63+
const parts = capturedHeaders['traceparent'].split('-');
64+
const traceId = parts[1];
65+
const spanId = parts[2];
66+
67+
expect(traceId).not.toBe(spanId.padEnd(32, '0'));
68+
expect(traceId.slice(0, 16)).not.toBe(spanId);
69+
});
70+
71+
it('generates unique traceparent per request', async () => {
72+
const client = createClient();
73+
74+
await client.get('/test1');
75+
const first = capturedHeaders['traceparent'];
76+
77+
await client.get('/test2');
78+
const second = capturedHeaders['traceparent'];
79+
80+
expect(first).not.toBe(second);
81+
});
82+
83+
it('allows caller to override traceparent via options.headers', async () => {
84+
const client = createClient();
85+
const custom = '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01';
86+
87+
await client.get('/test', { headers: { traceparent: custom } });
88+
89+
expect(capturedHeaders['traceparent']).toBe(custom);
90+
});
91+
});

0 commit comments

Comments
 (0)