From bd8bbc6d5eaac603572b54b5171db2e6573332c5 Mon Sep 17 00:00:00 2001 From: Grace Lim Date: Mon, 11 Sep 2023 20:00:15 -0700 Subject: [PATCH] fix: don't trace synthetic activity or overwrite existing trace header --- src/plugins/event-plugins/FetchPlugin.ts | 29 ++- src/plugins/event-plugins/XhrPlugin.ts | 10 +- .../__tests__/FetchPlugin.test.ts | 166 ++++++++++++++++++ .../event-plugins/__tests__/XhrPlugin.test.ts | 98 +++++++++++ src/plugins/utils/http-utils.ts | 12 +- 5 files changed, 306 insertions(+), 9 deletions(-) diff --git a/src/plugins/event-plugins/FetchPlugin.ts b/src/plugins/event-plugins/FetchPlugin.ts index 0f4d9071..8429240c 100644 --- a/src/plugins/event-plugins/FetchPlugin.ts +++ b/src/plugins/event-plugins/FetchPlugin.ts @@ -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 { @@ -85,11 +87,26 @@ export class FetchPlugin extends MonkeyPatched { 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), @@ -98,7 +115,7 @@ export class FetchPlugin extends MonkeyPatched { ); xRayTraceEvent.subsegments!.push(subsegment); - if (this.config.addXRayTraceIdHeader) { + if (this.config.addXRayTraceIdHeader && traceId === undefined) { this.addXRayTraceIdHeader(input, init, argsArray, xRayTraceEvent); } @@ -272,7 +289,11 @@ export class FetchPlugin extends MonkeyPatched { 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; diff --git a/src/plugins/event-plugins/XhrPlugin.ts b/src/plugins/event-plugins/XhrPlugin.ts index 6a9c7ad4..63a3e06c 100644 --- a/src/plugins/event-plugins/XhrPlugin.ts +++ b/src/plugins/event-plugins/XhrPlugin.ts @@ -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'; @@ -280,7 +281,11 @@ export class XhrPlugin extends MonkeyPatched { } private recordTraceEvent(trace: XRayTraceEvent) { - if (this.isTracingEnabled() && this.isSessionRecorded()) { + if ( + !isSyntheticsUA() && + this.isTracingEnabled() && + this.isSessionRecorded() + ) { this.context.record(XRAY_TRACE_EVENT_TYPE, trace); } } @@ -323,6 +328,7 @@ export class XhrPlugin extends MonkeyPatched { self.initializeTrace(xhrDetails); if ( + !isSyntheticsUA() && self.isTracingEnabled() && self.addXRayTraceIdHeader() && self.isSessionRecorded() diff --git a/src/plugins/event-plugins/__tests__/FetchPlugin.test.ts b/src/plugins/event-plugins/__tests__/FetchPlugin.test.ts index c91b8247..21e43c65 100644 --- a/src/plugins/event-plugins/__tests__/FetchPlugin.test.ts +++ b/src/plugins/event-plugins/__tests__/FetchPlugin.test.ts @@ -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) { const headers = init ? init : {}; this.get = (name: string) => { @@ -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' + }); + }); }); diff --git a/src/plugins/event-plugins/__tests__/XhrPlugin.test.ts b/src/plugins/event-plugins/__tests__/XhrPlugin.test.ts index 4dfe99df..ab9d2c6e 100644 --- a/src/plugins/event-plugins/__tests__/XhrPlugin.test.ts +++ b/src/plugins/event-plugins/__tests__/XhrPlugin.test.ts @@ -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'); @@ -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' + } + }); + }); }); diff --git a/src/plugins/utils/http-utils.ts b/src/plugins/utils/http-utils.ts index 5efa008d..30ebc59e 100644 --- a/src/plugins/utils/http-utils.ts +++ b/src/plugins/utils/http-utils.ts @@ -97,15 +97,17 @@ export const createXRayTraceEventHttp = ( export const createXRayTraceEvent = ( name: string, startTime: number, - http?: Http + http?: Http, + traceId?: string, + segmentId?: string ): XRayTraceEvent => { const traceEvent: XRayTraceEvent = { version: '1.0.0', name, origin: 'AWS::RUM::AppMonitor', - id: generateSegmentId(), + id: segmentId ? segmentId : generateSegmentId(), start_time: startTime, - trace_id: generateTraceId(), + trace_id: traceId ? traceId : generateTraceId(), end_time: undefined, subsegments: [], in_progress: false @@ -181,6 +183,10 @@ export const getAmznTraceIdHeaderValue = ( return 'Root=' + traceId + ';Parent=' + segmentId + ';Sampled=1'; }; +export const isSyntheticsUA = () => { + return navigator.userAgent.includes('CloudWatchSynthetics'); +}; + /** * Extracts an URL string from the fetch resource parameter. */