Skip to content

Commit 965ea07

Browse files
authored
fix: Avoid overwriting existing trace header (#449)
1 parent f36a9b5 commit 965ea07

File tree

6 files changed

+220
-3
lines changed

6 files changed

+220
-3
lines changed

src/plugins/event-plugins/FetchPlugin.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
resourceToUrlString,
2020
is429,
2121
is4xx,
22-
is5xx
22+
is5xx,
23+
getTraceHeader
2324
} from '../utils/http-utils';
2425
import { HTTP_EVENT_TYPE, XRAY_TRACE_EVENT_TYPE } from '../utils/constant';
2526
import {
@@ -271,8 +272,12 @@ export class FetchPlugin extends MonkeyPatched<Window, 'fetch'> {
271272
if (!isUrlAllowed(resourceToUrlString(input), this.config)) {
272273
return original.apply(thisArg, argsArray as any);
273274
}
275+
const traceHeader = getTraceHeader((input as Request).headers);
274276

275-
if (this.isTracingEnabled() && this.isSessionRecorded()) {
277+
if (traceHeader.traceId && traceHeader.segmentId) {
278+
httpEvent.trace_id = traceHeader.traceId;
279+
httpEvent.segment_id = traceHeader.segmentId;
280+
} else if (this.isTracingEnabled() && this.isSessionRecorded()) {
276281
trace = this.beginTrace(input, init, argsArray);
277282
httpEvent.trace_id = trace.trace_id;
278283
httpEvent.segment_id = trace.subsegments![0].id;

src/plugins/event-plugins/XhrPlugin.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,15 @@ export const XHR_PLUGIN_ID = 'xhr';
9696
export class XhrPlugin extends MonkeyPatched<XMLHttpRequest, 'send' | 'open'> {
9797
private config: HttpPluginConfig;
9898
private xhrMap: Map<XMLHttpRequest, XhrDetails>;
99+
private isSyntheticsUA: boolean;
99100

100101
constructor(config?: PartialHttpPluginConfig) {
101102
super(XHR_PLUGIN_ID);
102103
this.config = { ...defaultConfig, ...config };
103104
this.xhrMap = new Map<XMLHttpRequest, XhrDetails>();
105+
this.isSyntheticsUA = navigator.userAgent.includes(
106+
'CloudWatchSynthetics'
107+
);
104108
}
105109

106110
protected onload(): void {
@@ -280,7 +284,11 @@ export class XhrPlugin extends MonkeyPatched<XMLHttpRequest, 'send' | 'open'> {
280284
}
281285

282286
private recordTraceEvent(trace: XRayTraceEvent) {
283-
if (this.isTracingEnabled() && this.isSessionRecorded()) {
287+
if (
288+
!this.isSyntheticsUA &&
289+
this.isTracingEnabled() &&
290+
this.isSessionRecorded()
291+
) {
284292
this.context.record(XRAY_TRACE_EVENT_TYPE, trace);
285293
}
286294
}
@@ -323,6 +331,7 @@ export class XhrPlugin extends MonkeyPatched<XMLHttpRequest, 'send' | 'open'> {
323331
self.initializeTrace(xhrDetails);
324332

325333
if (
334+
!self.isSyntheticsUA &&
326335
self.isTracingEnabled() &&
327336
self.addXRayTraceIdHeader() &&
328337
self.isSessionRecorded()

src/plugins/event-plugins/__tests__/FetchPlugin.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const URL = 'https://aws.amazon.com';
3232
const TRACE_ID =
3333
'Root=1-0-000000000000000000000000;Parent=0000000000000000;Sampled=1';
3434

35+
const existingTraceId = '1-0-000000000000000000000001';
36+
const existingSegmentId = '0000000000000001';
37+
const existingTraceHeaderValue = `Root=${existingTraceId};Parent=${existingSegmentId};Sampled=1`;
38+
3539
const Headers = function (init?: Record<string, string>) {
3640
const headers = init ? init : {};
3741
this.get = (name: string) => {
@@ -934,4 +938,35 @@ describe('FetchPlugin tests', () => {
934938
segment_id: expect.anything()
935939
});
936940
});
941+
test('when fetch is called and request has existing trace header then existing trace data is added to the http event', async () => {
942+
// Init
943+
const config: PartialHttpPluginConfig = {
944+
logicalServiceName: 'sample.rum.aws.amazon.com',
945+
urlsToInclude: [/aws\.amazon\.com/],
946+
recordAllRequests: true
947+
};
948+
949+
const plugin: FetchPlugin = new FetchPlugin(config);
950+
plugin.load(xRayOnContext);
951+
952+
const init: RequestInit = {
953+
headers: {
954+
[X_AMZN_TRACE_ID]: existingTraceHeaderValue
955+
}
956+
};
957+
958+
const request: Request = new Request(URL, init);
959+
960+
// Run
961+
await fetch(request);
962+
plugin.disable();
963+
964+
// Assert
965+
expect(record).toHaveBeenCalledTimes(1);
966+
expect(record.mock.calls[0][0]).toEqual(HTTP_EVENT_TYPE);
967+
expect(record.mock.calls[0][1]).toMatchObject({
968+
trace_id: existingTraceId,
969+
segment_id: existingSegmentId
970+
});
971+
});
937972
});

src/plugins/event-plugins/__tests__/XhrPlugin.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import { XRAY_TRACE_EVENT_TYPE, HTTP_EVENT_TYPE } from '../../utils/constant';
1414
import { DEFAULT_CONFIG } from '../../../test-utils/test-utils';
1515
import { MockHeaders } from 'xhr-mock/lib/types';
1616

17+
const actualUserAgent = navigator.userAgent;
18+
const SYNTHETIC_USER_AGENT =
19+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11 CloudWatchSynthetics/arn:aws:synthetics:us-west-2:0000000000000:canary:test-canary-name';
20+
1721
// Mock getRandomValues -- since it does nothing, the 'random' number will be 0.
1822
jest.mock('../../../utils/random');
1923

@@ -907,4 +911,98 @@ describe('XhrPlugin tests', () => {
907911
segment_id: '0000000000000000'
908912
});
909913
});
914+
915+
test('when user agent is CW Synthetics then plugin does not record a trace', async () => {
916+
// Init
917+
Object.defineProperty(navigator, 'userAgent', {
918+
get() {
919+
return SYNTHETIC_USER_AGENT;
920+
},
921+
configurable: true
922+
});
923+
924+
const config: PartialHttpPluginConfig = {
925+
urlsToInclude: [/response\.json/]
926+
};
927+
928+
mock.get(/.*/, {
929+
body: JSON.stringify({ message: 'Hello World!' })
930+
});
931+
932+
const plugin: XhrPlugin = new XhrPlugin(config);
933+
plugin.load(xRayOnContext);
934+
935+
// Run
936+
const xhr = new XMLHttpRequest();
937+
xhr.open('GET', './response.json', true);
938+
xhr.send();
939+
940+
// Yield to the event queue so the event listeners can run
941+
await new Promise((resolve) => setTimeout(resolve, 0));
942+
943+
// Reset
944+
plugin.disable();
945+
Object.defineProperty(navigator, 'userAgent', {
946+
get() {
947+
return actualUserAgent;
948+
},
949+
configurable: true
950+
});
951+
952+
// Assert
953+
expect(record).not.toHaveBeenCalled();
954+
});
955+
956+
test('when user agent is CW Synthetics then the plugin records the http request/response', async () => {
957+
// Init
958+
Object.defineProperty(navigator, 'userAgent', {
959+
get() {
960+
return SYNTHETIC_USER_AGENT;
961+
},
962+
configurable: true
963+
});
964+
965+
const config: PartialHttpPluginConfig = {
966+
urlsToInclude: [/response\.json/],
967+
recordAllRequests: true
968+
};
969+
970+
mock.get(/.*/, {
971+
body: JSON.stringify({ message: 'Hello World!' })
972+
});
973+
974+
const plugin: XhrPlugin = new XhrPlugin(config);
975+
plugin.load(xRayOnContext);
976+
977+
// Run
978+
const xhr = new XMLHttpRequest();
979+
xhr.open('GET', './response.json', true);
980+
xhr.send();
981+
982+
// Yield to the event queue so the event listeners can run
983+
await new Promise((resolve) => setTimeout(resolve, 0));
984+
985+
// Reset
986+
plugin.disable();
987+
Object.defineProperty(navigator, 'userAgent', {
988+
get() {
989+
return actualUserAgent;
990+
},
991+
configurable: true
992+
});
993+
994+
// Assert
995+
expect(record).toHaveBeenCalledTimes(1);
996+
expect(record.mock.calls[0][0]).toEqual(HTTP_EVENT_TYPE);
997+
expect(record.mock.calls[0][1]).toMatchObject({
998+
request: {
999+
method: 'GET',
1000+
url: './response.json'
1001+
},
1002+
response: {
1003+
status: 200,
1004+
statusText: 'OK'
1005+
}
1006+
});
1007+
});
9101008
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { X_AMZN_TRACE_ID, getTraceHeader } from '../http-utils';
2+
3+
const Request = function (input: RequestInfo, init?: RequestInit) {
4+
if (typeof input === 'string') {
5+
this.url = input;
6+
this.method = 'GET';
7+
this.headers = new Headers();
8+
} else {
9+
this.url = input.url;
10+
this.method = input.method ? input.method : 'GET';
11+
this.headers = input.headers ? input.headers : new Headers();
12+
}
13+
if (init) {
14+
this.method = init.method ? init.method : this.method;
15+
if (
16+
this.headers &&
17+
typeof (init.headers as Headers).get === 'function'
18+
) {
19+
this.headers = init.headers;
20+
} else if (this.headers) {
21+
this.headers = new Headers(init.headers as Record<string, string>);
22+
}
23+
}
24+
};
25+
26+
describe('http-utils', () => {
27+
test('when request header contains trace header then return traceId and segmentId', async () => {
28+
const existingTraceId = '1-0-000000000000000000000001';
29+
const existingSegmentId = '0000000000000001';
30+
const existingTraceHeaderValue = `Root=${existingTraceId};Parent=${existingSegmentId};Sampled=1`;
31+
32+
const init: RequestInit = {
33+
headers: {
34+
[X_AMZN_TRACE_ID]: existingTraceHeaderValue
35+
}
36+
};
37+
const request: Request = new Request('https://aws.amazon.com', init);
38+
39+
const traceHeader = getTraceHeader(request.headers);
40+
41+
expect(traceHeader.traceId).toEqual(existingTraceId);
42+
expect(traceHeader.segmentId).toEqual(existingSegmentId);
43+
});
44+
45+
test('when request header does not contain trace header then returned traceId and segmentId are undefined', async () => {
46+
const request: Request = new Request('https://aws.amazon.com');
47+
48+
const traceHeader = getTraceHeader(request.headers);
49+
50+
expect(traceHeader.traceId).toEqual(undefined);
51+
expect(traceHeader.segmentId).toEqual(undefined);
52+
});
53+
});

src/plugins/utils/http-utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export type HttpPluginConfig = {
3939
addXRayTraceIdHeader: boolean;
4040
};
4141

42+
export type TraceHeader = {
43+
traceId?: string;
44+
segmentId?: string;
45+
};
46+
4247
export const defaultConfig: HttpPluginConfig = {
4348
logicalServiceName: 'rum.aws.amazon.com',
4449
urlsToInclude: [/.*/],
@@ -181,6 +186,18 @@ export const getAmznTraceIdHeaderValue = (
181186
return 'Root=' + traceId + ';Parent=' + segmentId + ';Sampled=1';
182187
};
183188

189+
export const getTraceHeader = (headers: Headers) => {
190+
const traceHeader: TraceHeader = {};
191+
192+
if (headers) {
193+
const headerComponents = headers.get(X_AMZN_TRACE_ID)?.split(';');
194+
if (headerComponents?.length === 3) {
195+
traceHeader.traceId = headerComponents[0].split('Root=')[1];
196+
traceHeader.segmentId = headerComponents[1].split('Parent=')[1];
197+
}
198+
}
199+
return traceHeader;
200+
};
184201
/**
185202
* Extracts an URL string from the fetch resource parameter.
186203
*/

0 commit comments

Comments
 (0)