diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts index 261d7534d..da6e094cf 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts @@ -33,15 +33,29 @@ export class ResourceReporter { } const formatResourceStartContext = ( - tracingAttributes: RUMResource['tracingAttributes'] -): Record | undefined => { - return tracingAttributes.samplingPriorityHeader === '0' - ? undefined - : { - '_dd.span_id': tracingAttributes.spanId.toString(10), - '_dd.trace_id': tracingAttributes.traceId.toString(10), - '_dd.rule_psr': tracingAttributes.rulePsr - }; + tracingAttributes: RUMResource['tracingAttributes'], + graphqlAttributes: RUMResource['graphqlAttributes'] +): Record => { + const attributes: Record = {}; + if (tracingAttributes.samplingPriorityHeader !== '0') { + attributes['_dd.span_id'] = tracingAttributes.spanId.toString(10); + attributes['_dd.trace_id'] = tracingAttributes.traceId.toString(10); + attributes['_dd.rule_psr'] = tracingAttributes.rulePsr; + } + + if (graphqlAttributes?.operationType) { + attributes['_dd.graphql.operation_type'] = + graphqlAttributes.operationType; + if (graphqlAttributes.operationName) { + attributes['_dd.graphql.operation_name'] = + graphqlAttributes.operationName; + } + if (graphqlAttributes.variables) { + attributes['_dd.graphql.variables'] = graphqlAttributes.variables; + } + } + + return attributes; }; const formatResourceStopContext = ( @@ -64,7 +78,10 @@ const reportResource = async (resource: RUMResource) => { resource.key, resource.request.method, resource.request.url, - formatResourceStartContext(resource.tracingAttributes), + formatResourceStartContext( + resource.tracingAttributes, + resource.graphqlAttributes + ), resource.timings.startTime ); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts index 2b42c0d6d..ff776509e 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts @@ -28,6 +28,11 @@ interface DdRumXhr extends XMLHttpRequest { } interface DdRumXhrContext { + graphql: { + operationType?: string; + operationName?: string; + variables?: string; + }; method: string; url: string; reported: boolean; @@ -99,6 +104,7 @@ const proxyOpen = ( url, reported: false, timer: new Timer(), + graphql: {}, tracingAttributes: getTracingAttributes({ hostname, firstPartyHostsRegexMap, @@ -177,6 +183,7 @@ const reportXhr = async ( url: context.url, kind: 'xhr' }, + graphqlAttributes: context.graphql, tracingAttributes: context.tracingAttributes, response: { statusCode: xhrProxy.status, @@ -204,15 +211,15 @@ const proxySetRequestHeader = (providers: XHRProxyProviders): void => { ) { if (isDatadogCustomHeader(header)) { if (header === DATADOG_GRAPH_QL_OPERATION_NAME_HEADER) { - // TODO: add information to request + this._datadog_xhr.graphql.operationName = value; return; } if (header === DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER) { - // TODO: add information to request + this._datadog_xhr.graphql.operationType = value; return; } if (header === DATADOG_GRAPH_QL_VARIABLES_HEADER) { - // TODO: add information to request + this._datadog_xhr.graphql.variables = value; return; } } diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index 00a1804cb..69d0043d7 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -25,6 +25,11 @@ import { ORIGIN_HEADER_KEY } from '../../../distributedTracing/distributedTracingHeaders'; import { firstPartyHostsRegexMapBuilder } from '../../../distributedTracing/firstPartyHosts'; +import { + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + DATADOG_GRAPH_QL_VARIABLES_HEADER +} from '../../../graphql/graphqlHeaders'; import { ResourceReporter } from '../DatadogRumResource/ResourceReporter'; import { XHRProxy } from '../XHRProxy'; import { @@ -77,7 +82,7 @@ afterEach(() => { DdRum.unregisterResourceEventMapper(); }); -describe('XHRPr', () => { +describe('XHRProxy', () => { describe('resource interception', () => { it('intercepts XHR request when startTracking() + XHR.open() + XHR.send()', async () => { // GIVEN @@ -1045,4 +1050,126 @@ describe('XHRPr', () => { expect(size).toEqual(-1); }); }); + + describe('setRequestHeader', () => { + it('sets graphql attributes and drops corresponding headers', async () => { + // GIVEN + const method = 'POST'; + const url = 'https://api.example.com/graphql'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([]) + }); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + 'query' + ); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + 'cats' + ); + xhr.setRequestHeader(DATADOG_GRAPH_QL_VARIABLES_HEADER, '{}'); + xhr.send(); + xhr.abort(); + xhr.complete(0, undefined); + await flushPromises(); + + // THEN + const attributes = DdNativeRum.startResource.mock.calls[0][3]; + expect(attributes['_dd.graphql.operation_type']).toEqual('query'); + expect(attributes['_dd.graphql.operation_name']).toEqual('cats'); + expect(attributes['_dd.graphql.variables']).toEqual('{}'); + + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_NAME_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] + ).not.toBeDefined(); + }); + + it('sets graphql attributes and drops corresponding headers when operation name and variables are missing', async () => { + // GIVEN + const method = 'POST'; + const url = 'https://api.example.com/graphql'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([]) + }); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + 'query' + ); + xhr.send(); + xhr.abort(); + xhr.complete(0, undefined); + await flushPromises(); + + // THEN + const attributes = DdNativeRum.startResource.mock.calls[0][3]; + expect(attributes['_dd.graphql.operation_type']).toEqual('query'); + expect(attributes['_dd.graphql.operation_name']).not.toBeDefined(); + expect(attributes['_dd.graphql.variables']).not.toBeDefined(); + + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_NAME_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] + ).not.toBeDefined(); + }); + + it('does not set graphql attributes but drops corresponding headers when operation type is missing', async () => { + // GIVEN + const method = 'POST'; + const url = 'https://api.example.com/graphql'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([]) + }); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + 'cats' + ); + xhr.setRequestHeader(DATADOG_GRAPH_QL_VARIABLES_HEADER, '{}'); + xhr.send(); + xhr.abort(); + xhr.complete(0, undefined); + await flushPromises(); + + // THEN + const attributes = DdNativeRum.startResource.mock.calls[0][3]; + expect(attributes['_dd.graphql.operation_type']).not.toBeDefined(); + expect(attributes['_dd.graphql.operation_name']).not.toBeDefined(); + expect(attributes['_dd.graphql.variables']).not.toBeDefined(); + + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_NAME_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] + ).not.toBeDefined(); + }); + }); }); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RumResource.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RumResource.ts index 27d1cde13..c3a9939c3 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RumResource.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RumResource.ts @@ -15,6 +15,7 @@ export interface RUMResource { kind: ResourceKind; }; tracingAttributes: DdRumResourceTracingAttributes; + graphqlAttributes?: DdRumResourceGraphqlAttributes; response: { statusCode: number; size: number; @@ -26,3 +27,9 @@ export interface RUMResource { }; resourceContext?: XMLHttpRequest; } + +export type DdRumResourceGraphqlAttributes = { + operationType?: string; + operationName?: string; + variables?: string; +};