Skip to content

Commit

Permalink
fix: Avoid overwriting existing trace header (#449)
Browse files Browse the repository at this point in the history
  • Loading branch information
limhjgrace authored Sep 13, 2023
1 parent f36a9b5 commit 965ea07
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 3 deletions.
9 changes: 7 additions & 2 deletions src/plugins/event-plugins/FetchPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
resourceToUrlString,
is429,
is4xx,
is5xx
is5xx,
getTraceHeader
} from '../utils/http-utils';
import { HTTP_EVENT_TYPE, XRAY_TRACE_EVENT_TYPE } from '../utils/constant';
import {
Expand Down Expand Up @@ -271,8 +272,12 @@ export class FetchPlugin extends MonkeyPatched<Window, 'fetch'> {
if (!isUrlAllowed(resourceToUrlString(input), this.config)) {
return original.apply(thisArg, argsArray as any);
}
const traceHeader = getTraceHeader((input as Request).headers);

if (this.isTracingEnabled() && this.isSessionRecorded()) {
if (traceHeader.traceId && traceHeader.segmentId) {
httpEvent.trace_id = traceHeader.traceId;
httpEvent.segment_id = traceHeader.segmentId;
} else if (this.isTracingEnabled() && this.isSessionRecorded()) {
trace = this.beginTrace(input, init, argsArray);
httpEvent.trace_id = trace.trace_id;
httpEvent.segment_id = trace.subsegments![0].id;
Expand Down
11 changes: 10 additions & 1 deletion src/plugins/event-plugins/XhrPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,15 @@ export const XHR_PLUGIN_ID = 'xhr';
export class XhrPlugin extends MonkeyPatched<XMLHttpRequest, 'send' | 'open'> {
private config: HttpPluginConfig;
private xhrMap: Map<XMLHttpRequest, XhrDetails>;
private isSyntheticsUA: boolean;

constructor(config?: PartialHttpPluginConfig) {
super(XHR_PLUGIN_ID);
this.config = { ...defaultConfig, ...config };
this.xhrMap = new Map<XMLHttpRequest, XhrDetails>();
this.isSyntheticsUA = navigator.userAgent.includes(
'CloudWatchSynthetics'
);
}

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

private recordTraceEvent(trace: XRayTraceEvent) {
if (this.isTracingEnabled() && this.isSessionRecorded()) {
if (
!this.isSyntheticsUA &&
this.isTracingEnabled() &&
this.isSessionRecorded()
) {
this.context.record(XRAY_TRACE_EVENT_TYPE, trace);
}
}
Expand Down Expand Up @@ -323,6 +331,7 @@ export class XhrPlugin extends MonkeyPatched<XMLHttpRequest, 'send' | 'open'> {
self.initializeTrace(xhrDetails);

if (
!self.isSyntheticsUA &&
self.isTracingEnabled() &&
self.addXRayTraceIdHeader() &&
self.isSessionRecorded()
Expand Down
35 changes: 35 additions & 0 deletions src/plugins/event-plugins/__tests__/FetchPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const URL = 'https://aws.amazon.com';
const TRACE_ID =
'Root=1-0-000000000000000000000000;Parent=0000000000000000;Sampled=1';

const existingTraceId = '1-0-000000000000000000000001';
const existingSegmentId = '0000000000000001';
const existingTraceHeaderValue = `Root=${existingTraceId};Parent=${existingSegmentId};Sampled=1`;

const Headers = function (init?: Record<string, string>) {
const headers = init ? init : {};
this.get = (name: string) => {
Expand Down Expand Up @@ -934,4 +938,35 @@ describe('FetchPlugin tests', () => {
segment_id: expect.anything()
});
});
test('when fetch is called and request has existing trace header then existing trace data is added to the http event', async () => {
// Init
const config: PartialHttpPluginConfig = {
logicalServiceName: 'sample.rum.aws.amazon.com',
urlsToInclude: [/aws\.amazon\.com/],
recordAllRequests: true
};

const plugin: FetchPlugin = new FetchPlugin(config);
plugin.load(xRayOnContext);

const init: RequestInit = {
headers: {
[X_AMZN_TRACE_ID]: existingTraceHeaderValue
}
};

const request: Request = new Request(URL, init);

// Run
await fetch(request);
plugin.disable();

// Assert
expect(record).toHaveBeenCalledTimes(1);
expect(record.mock.calls[0][0]).toEqual(HTTP_EVENT_TYPE);
expect(record.mock.calls[0][1]).toMatchObject({
trace_id: existingTraceId,
segment_id: existingSegmentId
});
});
});
98 changes: 98 additions & 0 deletions src/plugins/event-plugins/__tests__/XhrPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { XRAY_TRACE_EVENT_TYPE, HTTP_EVENT_TYPE } from '../../utils/constant';
import { DEFAULT_CONFIG } from '../../../test-utils/test-utils';
import { MockHeaders } from 'xhr-mock/lib/types';

const actualUserAgent = navigator.userAgent;
const SYNTHETIC_USER_AGENT =
'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';

// Mock getRandomValues -- since it does nothing, the 'random' number will be 0.
jest.mock('../../../utils/random');

Expand Down Expand Up @@ -907,4 +911,98 @@ describe('XhrPlugin tests', () => {
segment_id: '0000000000000000'
});
});

test('when user agent is CW Synthetics then plugin does not record a trace', async () => {
// Init
Object.defineProperty(navigator, 'userAgent', {
get() {
return SYNTHETIC_USER_AGENT;
},
configurable: true
});

const config: PartialHttpPluginConfig = {
urlsToInclude: [/response\.json/]
};

mock.get(/.*/, {
body: JSON.stringify({ message: 'Hello World!' })
});

const plugin: XhrPlugin = new XhrPlugin(config);
plugin.load(xRayOnContext);

// Run
const xhr = new XMLHttpRequest();
xhr.open('GET', './response.json', true);
xhr.send();

// Yield to the event queue so the event listeners can run
await new Promise((resolve) => setTimeout(resolve, 0));

// Reset
plugin.disable();
Object.defineProperty(navigator, 'userAgent', {
get() {
return actualUserAgent;
},
configurable: true
});

// Assert
expect(record).not.toHaveBeenCalled();
});

test('when user agent is CW Synthetics then the plugin records the http request/response', async () => {
// Init
Object.defineProperty(navigator, 'userAgent', {
get() {
return SYNTHETIC_USER_AGENT;
},
configurable: true
});

const config: PartialHttpPluginConfig = {
urlsToInclude: [/response\.json/],
recordAllRequests: true
};

mock.get(/.*/, {
body: JSON.stringify({ message: 'Hello World!' })
});

const plugin: XhrPlugin = new XhrPlugin(config);
plugin.load(xRayOnContext);

// Run
const xhr = new XMLHttpRequest();
xhr.open('GET', './response.json', true);
xhr.send();

// Yield to the event queue so the event listeners can run
await new Promise((resolve) => setTimeout(resolve, 0));

// Reset
plugin.disable();
Object.defineProperty(navigator, 'userAgent', {
get() {
return actualUserAgent;
},
configurable: true
});

// Assert
expect(record).toHaveBeenCalledTimes(1);
expect(record.mock.calls[0][0]).toEqual(HTTP_EVENT_TYPE);
expect(record.mock.calls[0][1]).toMatchObject({
request: {
method: 'GET',
url: './response.json'
},
response: {
status: 200,
statusText: 'OK'
}
});
});
});
53 changes: 53 additions & 0 deletions src/plugins/utils/__tests__/http-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { X_AMZN_TRACE_ID, getTraceHeader } from '../http-utils';

const Request = function (input: RequestInfo, init?: RequestInit) {
if (typeof input === 'string') {
this.url = input;
this.method = 'GET';
this.headers = new Headers();
} else {
this.url = input.url;
this.method = input.method ? input.method : 'GET';
this.headers = input.headers ? input.headers : new Headers();
}
if (init) {
this.method = init.method ? init.method : this.method;
if (
this.headers &&
typeof (init.headers as Headers).get === 'function'
) {
this.headers = init.headers;
} else if (this.headers) {
this.headers = new Headers(init.headers as Record<string, string>);
}
}
};

describe('http-utils', () => {
test('when request header contains trace header then return traceId and segmentId', async () => {
const existingTraceId = '1-0-000000000000000000000001';
const existingSegmentId = '0000000000000001';
const existingTraceHeaderValue = `Root=${existingTraceId};Parent=${existingSegmentId};Sampled=1`;

const init: RequestInit = {
headers: {
[X_AMZN_TRACE_ID]: existingTraceHeaderValue
}
};
const request: Request = new Request('https://aws.amazon.com', init);

const traceHeader = getTraceHeader(request.headers);

expect(traceHeader.traceId).toEqual(existingTraceId);
expect(traceHeader.segmentId).toEqual(existingSegmentId);
});

test('when request header does not contain trace header then returned traceId and segmentId are undefined', async () => {
const request: Request = new Request('https://aws.amazon.com');

const traceHeader = getTraceHeader(request.headers);

expect(traceHeader.traceId).toEqual(undefined);
expect(traceHeader.segmentId).toEqual(undefined);
});
});
17 changes: 17 additions & 0 deletions src/plugins/utils/http-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export type HttpPluginConfig = {
addXRayTraceIdHeader: boolean;
};

export type TraceHeader = {
traceId?: string;
segmentId?: string;
};

export const defaultConfig: HttpPluginConfig = {
logicalServiceName: 'rum.aws.amazon.com',
urlsToInclude: [/.*/],
Expand Down Expand Up @@ -181,6 +186,18 @@ export const getAmznTraceIdHeaderValue = (
return 'Root=' + traceId + ';Parent=' + segmentId + ';Sampled=1';
};

export const getTraceHeader = (headers: Headers) => {
const traceHeader: TraceHeader = {};

if (headers) {
const headerComponents = headers.get(X_AMZN_TRACE_ID)?.split(';');
if (headerComponents?.length === 3) {
traceHeader.traceId = headerComponents[0].split('Root=')[1];
traceHeader.segmentId = headerComponents[1].split('Parent=')[1];
}
}
return traceHeader;
};
/**
* Extracts an URL string from the fetch resource parameter.
*/
Expand Down

0 comments on commit 965ea07

Please sign in to comment.