Skip to content

Commit f131f7d

Browse files
committed
Track graphql payload
1 parent 5a2b777 commit f131f7d

9 files changed

Lines changed: 159 additions & 8 deletions

File tree

packages/core/src/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ import {
3434
import {
3535
DATADOG_GRAPH_QL_OPERATION_NAME_HEADER,
3636
DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER,
37-
DATADOG_GRAPH_QL_VARIABLES_HEADER
37+
DATADOG_GRAPH_QL_VARIABLES_HEADER,
38+
DATADOG_GRAPH_QL_PAYLOAD_HEADER
3839
} from './rum/instrumentation/resourceTracking/graphql/graphqlHeaders';
3940
import type { FirstPartyHost } from './rum/types';
4041
import { ErrorSource, PropagatorType, RumActionType } from './rum/types';
@@ -74,6 +75,7 @@ export {
7475
DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER,
7576
DATADOG_GRAPH_QL_OPERATION_NAME_HEADER,
7677
DATADOG_GRAPH_QL_VARIABLES_HEADER,
78+
DATADOG_GRAPH_QL_PAYLOAD_HEADER,
7779
TracingIdType,
7880
TracingIdFormat,
7981
DatadogTracingIdentifier,
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { DATADOG_CUSTOM_HEADER_PREFIX } from '../headers';
2-
31
/*
42
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
53
* This product includes software developed at Datadog (https://www.datadoghq.com/).
64
* Copyright 2016-Present Datadog, Inc.
75
*/
6+
import { DATADOG_CUSTOM_HEADER_PREFIX } from '../headers';
7+
88
export const DATADOG_GRAPH_QL_OPERATION_NAME_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-name`;
99
export const DATADOG_GRAPH_QL_VARIABLES_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-variables`;
1010
export const DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-type`;
11+
export const DATADOG_GRAPH_QL_PAYLOAD_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-payload`;

packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ const formatResourceStopContext = (
7474
if (graphqlAttributes.variables) {
7575
attributes['_dd.graphql.variables'] = graphqlAttributes.variables;
7676
}
77+
78+
if (graphqlAttributes.payload) {
79+
attributes['_dd.graphql.payload'] = graphqlAttributes.payload;
80+
}
7781
}
7882

7983
return attributes;

packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getTracingAttributes } from '../../distributedTracing/distributedTracin
1515
import {
1616
DATADOG_GRAPH_QL_OPERATION_NAME_HEADER,
1717
DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER,
18+
DATADOG_GRAPH_QL_PAYLOAD_HEADER,
1819
DATADOG_GRAPH_QL_VARIABLES_HEADER
1920
} from '../../graphql/graphqlHeaders';
2021
import { DATADOG_BAGGAGE_HEADER, isDatadogCustomHeader } from '../../headers';
@@ -37,6 +38,7 @@ interface DdRumXhrContext {
3738
operationType?: string;
3839
operationName?: string;
3940
variables?: string;
41+
payload?: string;
4042
};
4143
method: string;
4244
url: string;
@@ -228,6 +230,7 @@ const proxySetRequestHeader = (providers: XHRProxyProviders): void => {
228230
value: string
229231
) {
230232
const key = header.toLowerCase();
233+
console.log('HeaderKey: ', key);
231234
if (isDatadogCustomHeader(key)) {
232235
switch (key) {
233236
case DATADOG_GRAPH_QL_OPERATION_NAME_HEADER:
@@ -239,6 +242,9 @@ const proxySetRequestHeader = (providers: XHRProxyProviders): void => {
239242
case DATADOG_GRAPH_QL_VARIABLES_HEADER:
240243
this._datadog_xhr.graphql.variables = value;
241244
break;
245+
case DATADOG_GRAPH_QL_PAYLOAD_HEADER:
246+
this._datadog_xhr.graphql.payload = value;
247+
break;
242248
case DATADOG_BAGGAGE_HEADER:
243249
// Apply Baggage Header only if pre-processed by Datadog
244250
return originalXhrSetRequestHeader.apply(this, [

packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RumResource.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ export type DdRumResourceGraphqlAttributes = {
3232
operationType?: string;
3333
operationName?: string;
3434
variables?: string;
35+
payload?: string;
3536
};

packages/react-native-apollo-client/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@
3737
"lint": "eslint .",
3838
"prepare": "rm -rf lib && yarn bob build"
3939
},
40+
"dependencies": {
41+
"graphql": "^16.8.0"
42+
},
4043
"devDependencies": {
4144
"@apollo/client": "^3.8.3",
4245
"@testing-library/react-native": "7.0.2",
43-
"graphql": "^16.8.0",
4446
"react-native-builder-bob": "0.26.0"
4547
},
4648
"peerDependencies": {

packages/react-native-apollo-client/src/DatadogLink.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,32 @@ import { ApolloLink } from '@apollo/client';
88
import {
99
DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER,
1010
DATADOG_GRAPH_QL_OPERATION_NAME_HEADER,
11-
DATADOG_GRAPH_QL_VARIABLES_HEADER
11+
DATADOG_GRAPH_QL_VARIABLES_HEADER,
12+
DATADOG_GRAPH_QL_PAYLOAD_HEADER
1213
} from '@datadog/mobile-react-native';
1314

14-
import { getOperationName, getVariables, getOperationType } from './helpers';
15+
import {
16+
getOperationName,
17+
getVariables,
18+
getOperationType,
19+
getPayload
20+
} from './helpers';
21+
22+
export type DatadogLinkOptions = {
23+
trackPayload: boolean;
24+
};
1525

1626
export class DatadogLink extends ApolloLink {
17-
constructor() {
27+
private trackPayload = false;
28+
29+
constructor(options: DatadogLinkOptions = { trackPayload: false }) {
1830
super((operation, forward) => {
1931
const operationName = getOperationName(operation);
2032
const formattedVariables = getVariables(operation);
2133
const operationType = getOperationType(operation);
34+
const payload = getPayload(operation, this.trackPayload);
35+
36+
console.log('Payload: ', payload);
2237

2338
operation.setContext(({ headers = {} }) => {
2439
const newHeaders: Record<string, string | null> = {
@@ -34,6 +49,7 @@ export class DatadogLink extends ApolloLink {
3449
newHeaders[
3550
DATADOG_GRAPH_QL_VARIABLES_HEADER
3651
] = formattedVariables;
52+
newHeaders[DATADOG_GRAPH_QL_PAYLOAD_HEADER] = payload;
3753

3854
return {
3955
headers: newHeaders
@@ -42,5 +58,7 @@ export class DatadogLink extends ApolloLink {
4258

4359
return forward(operation);
4460
});
61+
62+
this.trackPayload = options.trackPayload;
4563
}
4664
}

packages/react-native-apollo-client/src/__tests__/helpers.test.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
* Copyright 2016-Present Datadog, Inc.
55
*/
66

7-
import { getOperationName, getVariables, getOperationType } from '../helpers';
7+
import {
8+
getOperationName,
9+
getVariables,
10+
getOperationType,
11+
getPayload
12+
} from '../helpers';
813

914
import {
1015
createCatOperation,
@@ -62,4 +67,75 @@ describe('helpers', () => {
6267
expect(getOperationType({ query: { definitions: [] } })).toBeNull();
6368
});
6469
});
70+
71+
describe('getPayload', () => {
72+
it('returns null when trackPayload is false', () => {
73+
expect(getPayload(getCountryOperation, false)).toBeNull();
74+
});
75+
76+
it('returns null when trackPayload is not provided (defaults to false)', () => {
77+
expect(getPayload(getCountryOperation)).toBeNull();
78+
});
79+
80+
it('returns the query string when trackPayload is true', () => {
81+
const payload = getPayload(getCountryOperation, true);
82+
expect(payload).toBeTruthy();
83+
expect(payload).toContain('query CountryDetails');
84+
expect(payload).toContain('country');
85+
});
86+
87+
it('returns the query string for mutations', () => {
88+
const payload = getPayload(createCatOperation, true);
89+
expect(payload).toBeTruthy();
90+
expect(payload).toContain('mutation CreateCat');
91+
});
92+
93+
it('trims whitespace from the query string', () => {
94+
const payload = getPayload(getCountryOperation, true);
95+
expect(payload).toBeTruthy();
96+
// Check that there's no leading/trailing whitespace
97+
expect(payload).toBe(payload?.trim());
98+
});
99+
100+
it('truncates query strings longer than 32 KiB', () => {
101+
// Create a mock operation with a very long query
102+
// Generate a query with a lot of fields to exceed 32 KiB
103+
const fields = 'a'.repeat(1000);
104+
const mockOperation: any = {
105+
query: {
106+
kind: 'Document',
107+
definitions: [
108+
{
109+
kind: 'OperationDefinition',
110+
operation: 'query',
111+
selectionSet: {
112+
kind: 'SelectionSet',
113+
selections: Array.from(
114+
{ length: 100 },
115+
(_, i) => ({
116+
kind: 'Field',
117+
name: {
118+
kind: 'Name',
119+
value: `field${i}_${fields}`
120+
}
121+
})
122+
)
123+
}
124+
}
125+
]
126+
}
127+
};
128+
129+
const payload = getPayload(mockOperation, true);
130+
expect(payload).toBeTruthy();
131+
expect(payload?.length).toBe(32 * 1024 + 3); // 32 KiB + '...'
132+
expect(payload?.endsWith('...')).toBe(true);
133+
});
134+
135+
it('does not crash if the operation is malformed', () => {
136+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
137+
// @ts-ignore
138+
expect(getPayload({}, true)).toBeNull();
139+
});
140+
});
65141
});

packages/react-native-apollo-client/src/helpers.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import { version } from '@apollo/client/package.json';
1212
import type { Operation } from '@apollo/client';
1313
import { DdSdk } from '@datadog/mobile-react-native';
1414
import type { DefinitionNode, OperationDefinitionNode } from 'graphql';
15+
import { print } from 'graphql';
1516

1617
import { ErrorCode, errorMessages } from './types';
1718

1819
const apolloVersion = `[Apollo v${version}]`;
1920

21+
const GRAPHQL_PAYLOAD_LIMIT = 32 * 1024;
22+
2023
export const getVariables = (operation: Operation): string | null => {
2124
if (operation.variables) {
2225
try {
@@ -70,6 +73,32 @@ export const getOperationType = (
7073
}
7174
};
7275

76+
export const getPayload = (
77+
operation: Operation,
78+
trackPayload: boolean = false
79+
): string | null => {
80+
if (!trackPayload) {
81+
return null;
82+
}
83+
84+
try {
85+
const queryString = print(operation.query);
86+
const trimmedQuery = queryString.trim();
87+
88+
return safeTruncate(trimmedQuery, GRAPHQL_PAYLOAD_LIMIT, '...');
89+
} catch (e) {
90+
DdSdk?.telemetryError(
91+
_getErrorMessage(
92+
ErrorCode.GQL_VARIABLE_RETRIEVAL_ERROR,
93+
apolloVersion
94+
),
95+
_getErrorStack(e),
96+
ErrorCode.GQL_VARIABLE_RETRIEVAL_ERROR
97+
);
98+
return null;
99+
}
100+
};
101+
73102
const _getErrorMessage = (code: ErrorCode, details: string) =>
74103
`${errorMessages[code]} - ${details}`;
75104

@@ -82,3 +111,15 @@ const _getErrorStack = (error: unknown): string => {
82111
? error.stack ?? 'No stack trace available'
83112
: `Non-Error thrown: ${error}`;
84113
};
114+
115+
const safeTruncate = (candidate: string, length: number, suffix = '') => {
116+
const lastChar = candidate.charCodeAt(length - 1);
117+
const isLastCharSurrogatePair = lastChar >= 0xd800 && lastChar <= 0xdbff;
118+
const correctedLength = isLastCharSurrogatePair ? length + 1 : length;
119+
120+
if (candidate.length <= correctedLength) {
121+
return candidate;
122+
}
123+
124+
return `${candidate.slice(0, correctedLength)}${suffix}`;
125+
};

0 commit comments

Comments
 (0)