Skip to content

Commit

Permalink
fix: don't trace synthetic activity or overwrite existing trace header
Browse files Browse the repository at this point in the history
  • Loading branch information
limhjgrace committed Sep 12, 2023
1 parent f36a9b5 commit bd8bbc6
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 9 deletions.
29 changes: 25 additions & 4 deletions src/plugins/event-plugins/FetchPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
resourceToUrlString,
is429,
is4xx,
is5xx
is5xx,
X_AMZN_TRACE_ID,
isSyntheticsUA
} from '../utils/http-utils';
import { HTTP_EVENT_TYPE, XRAY_TRACE_EVENT_TYPE } from '../utils/constant';
import {
Expand Down Expand Up @@ -85,11 +87,26 @@ export class FetchPlugin extends MonkeyPatched<Window, 'fetch'> {
init: RequestInit | undefined,
argsArray: IArguments
): XRayTraceEvent => {
let traceId;
let segmentId;

const headers = (input as Request).headers;
if (headers) {
const headerComponents = headers.get(X_AMZN_TRACE_ID)?.split(';');
if (headerComponents?.length === 3) {
traceId = headerComponents[0].split('Root=')[1];
segmentId = headerComponents[1].split('Parent=')[1];
}
}

const startTime = epochTime();
const http: Http = createXRayTraceEventHttp(input, init, true);
const xRayTraceEvent: XRayTraceEvent = createXRayTraceEvent(
this.config.logicalServiceName,
startTime
startTime,
undefined,
traceId,
segmentId
);
const subsegment: Subsegment = createXRaySubsegment(
requestInfoToHostname(input),
Expand All @@ -98,7 +115,7 @@ export class FetchPlugin extends MonkeyPatched<Window, 'fetch'> {
);
xRayTraceEvent.subsegments!.push(subsegment);

if (this.config.addXRayTraceIdHeader) {
if (this.config.addXRayTraceIdHeader && traceId === undefined) {
this.addXRayTraceIdHeader(input, init, argsArray, xRayTraceEvent);
}

Expand Down Expand Up @@ -272,7 +289,11 @@ export class FetchPlugin extends MonkeyPatched<Window, 'fetch'> {
return original.apply(thisArg, argsArray as any);
}

if (this.isTracingEnabled() && this.isSessionRecorded()) {
if (
!isSyntheticsUA() &&
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
10 changes: 8 additions & 2 deletions src/plugins/event-plugins/XhrPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
requestInfoToHostname,
is429,
is4xx,
is5xx
is5xx,
isSyntheticsUA
} from '../utils/http-utils';
import { XhrError } from '../../errors/XhrError';
import { HTTP_EVENT_TYPE, XRAY_TRACE_EVENT_TYPE } from '../utils/constant';
Expand Down Expand Up @@ -280,7 +281,11 @@ export class XhrPlugin extends MonkeyPatched<XMLHttpRequest, 'send' | 'open'> {
}

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

if (
!isSyntheticsUA() &&
self.isTracingEnabled() &&
self.addXRayTraceIdHeader() &&
self.isSessionRecorded()
Expand Down
166 changes: 166 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,14 @@ const URL = 'https://aws.amazon.com';
const TRACE_ID =
'Root=1-0-000000000000000000000000;Parent=0000000000000000;Sampled=1';

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';

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 +942,162 @@ describe('FetchPlugin tests', () => {
segment_id: expect.anything()
});
});

test('when user agent is CW Synthetics then the plugin does not record a trace', async () => {
Object.defineProperty(navigator, 'userAgent', {
get() {
return SYNTHETIC_USER_AGENT;
},
configurable: true
});
const config: PartialHttpPluginConfig = {
logicalServiceName: 'sample.rum.aws.amazon.com',
urlsToInclude: [/aws\.amazon\.com/]
};

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

// Run
await fetch(URL);

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

// Assert
expect(mockFetch).toHaveBeenCalledTimes(1);
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: [/aws\.amazon\.com/],
recordAllRequests: true
};

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

// Run
await fetch(URL);

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

// Assert
expect(mockFetch).toHaveBeenCalledTimes(1);
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: URL
},
response: {
status: 200,
statusText: 'OK'
}
});
});
test('when fetch is called and request has existing trace header then the plugin records a trace with existing trace data', async () => {
const config: PartialHttpPluginConfig = {
logicalServiceName: 'sample.rum.aws.amazon.com',
addXRayTraceIdHeader: 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(mockFetch).toHaveBeenCalledTimes(1);
expect(record.mock.calls[0][0]).toEqual(XRAY_TRACE_EVENT_TYPE);
expect(record.mock.calls[0][1]).toMatchObject({
name: 'sample.rum.aws.amazon.com',
id: existingSegmentId,
start_time: 0,
trace_id: existingTraceId,
end_time: 0,
subsegments: [
{
id: '0000000000000000',
name: 'aws.amazon.com',
start_time: 0,
end_time: 0,
namespace: 'remote',
http: {
request: {
method: 'GET',
url: URL,
traced: true
},
response: { status: 200, content_length: 125 }
}
}
]
});
});
test('when fetch is called and request has existing trace header then existing trace data is added to the http event', async () => {
// Init
console.log(navigator.userAgent);
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(2);
expect(record.mock.calls[1][0]).toEqual(HTTP_EVENT_TYPE);
expect(record.mock.calls[1][1]).toMatchObject({
trace_id: existingTraceId,
segment_id: '0000000000000000'
});
});
});
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'
}
});
});
});
Loading

0 comments on commit bd8bbc6

Please sign in to comment.