Skip to content

Commit 20b6516

Browse files
committed
fix(browser): Attach request payload to fetch instrumentation only for graphql requests
Signed-off-by: Kaung Zin Hein <[email protected]>
1 parent 77c9fbc commit 20b6516

File tree

4 files changed

+75
-23
lines changed

4 files changed

+75
-23
lines changed

packages/browser/src/integrations/graphqlClient.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,17 @@ const _graphqlClientIntegration = ((options: GraphQLClientOptions) => {
3434
const httpUrl = spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL] || spanAttributes['http.url'];
3535

3636
const { endpoints } = options;
37-
3837
const isTracedGraphqlEndpoint = endpoints.includes(httpUrl);
3938

4039
if (isTracedGraphqlEndpoint) {
4140
const httpMethod = spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] || spanAttributes['http.method'];
4241
const graphqlBody = spanAttributes['body'];
4342

4443
// Standard graphql request shape: https://graphql.org/learn/serving-over-http/#post-request
44+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
4545
const graphqlQuery = graphqlBody && (graphqlBody['query'] as string);
46+
47+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
4648
const graphqlOperationName = graphqlBody && (graphqlBody['operationName'] as string);
4749

4850
const { operationName = graphqlOperationName, operationType } = parseGraphQLQuery(graphqlQuery);

packages/utils/src/graphql.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@ export function parseGraphQLQuery(query: string): GraphQLOperation {
2727
/**
2828
* Extract the payload of a request ONLY if it's GraphQL.
2929
* @param payload - A valid JSON string
30+
* @returns A POJO or undefined
3031
*/
3132
export function getGraphQLRequestPayload(payload: string): any | undefined {
3233
let graphqlBody = undefined;
3334
try {
3435
const requestBody = JSON.parse(payload);
36+
37+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
3538
const isGraphQLRequest = !!requestBody['query'];
39+
3640
if (isGraphQLRequest) {
3741
graphqlBody = requestBody;
3842
}

packages/utils/src/instrument/fetch.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import type { HandlerDataFetch } from '@sentry/types';
33

4+
import { getGraphQLRequestPayload } from '../graphql';
45
import { isError } from '../is';
56
import { addNonEnumerableProperty, fill } from '../object';
67
import { supportsNativeFetch } from '../supports';
@@ -48,17 +49,25 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat
4849

4950
fill(GLOBAL_OBJ, 'fetch', function (originalFetch: () => void): () => void {
5051
return function (...args: any[]): void {
51-
const { method, url, body } = parseFetchArgs(args);
52+
const { method, url } = parseFetchArgs(args);
5253
const handlerData: HandlerDataFetch = {
5354
args,
5455
fetchData: {
5556
method,
5657
url,
57-
body,
5858
},
5959
startTimestamp: timestampInSeconds() * 1000,
6060
};
6161

62+
const body = parseFetchPayload(args);
63+
64+
if (body) {
65+
const graphqlRequest = getGraphQLRequestPayload(body);
66+
if (graphqlRequest) {
67+
handlerData.fetchData.body = body;
68+
}
69+
}
70+
6271
// if there is no callback, fetch is instrumented directly
6372
if (!onFetchResolved) {
6473
triggerHandlers('fetch', {
@@ -192,12 +201,12 @@ function getUrlFromResource(resource: FetchResource): string {
192201
}
193202

194203
/**
195-
* Parses the fetch arguments to find the used Http method, the url, and the payload of the request.
204+
* Parses the fetch arguments to find the used Http method and the url of the request.
196205
* Exported for tests only.
197206
*/
198-
export function parseFetchArgs(fetchArgs: unknown[]): { method: string; url: string; body: string | null } {
207+
export function parseFetchArgs(fetchArgs: unknown[]): { method: string; url: string } {
199208
if (fetchArgs.length === 0) {
200-
return { method: 'GET', url: '', body: null };
209+
return { method: 'GET', url: '' };
201210
}
202211

203212
if (fetchArgs.length === 2) {
@@ -206,14 +215,26 @@ export function parseFetchArgs(fetchArgs: unknown[]): { method: string; url: str
206215
return {
207216
url: getUrlFromResource(url),
208217
method: hasProp(options, 'method') ? String(options.method).toUpperCase() : 'GET',
209-
body: hasProp(options, 'body') ? String(options.body) : null,
210218
};
211219
}
212220

213221
const arg = fetchArgs[0];
214222
return {
215223
url: getUrlFromResource(arg as FetchResource),
216224
method: hasProp(arg, 'method') ? String(arg.method).toUpperCase() : 'GET',
217-
body: hasProp(arg, 'body') ? String(arg.body) : null,
218225
};
219226
}
227+
228+
/**
229+
* Parses the fetch arguments to extract the request payload.
230+
* Exported for tests only.
231+
*/
232+
export function parseFetchPayload(fetchArgs: unknown[]): string | undefined {
233+
if (fetchArgs.length === 2) {
234+
const options = fetchArgs[1];
235+
return hasProp(options, 'body') ? String(options.body) : undefined;
236+
}
237+
238+
const arg = fetchArgs[0];
239+
return hasProp(arg, 'body') ? String(arg.body) : undefined;
240+
}

packages/utils/test/instrument/fetch.test.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,59 @@
1-
import { parseFetchArgs } from '../../src/instrument/fetch';
1+
import { parseFetchArgs, parseFetchPayload } from '../../src/instrument/fetch';
22

33
describe('instrument > parseFetchArgs', () => {
4-
const data = { name: 'Test' };
5-
64
it.each([
7-
['string URL only', ['http://example.com'], { method: 'GET', url: 'http://example.com', body: null }],
8-
['URL object only', [new URL('http://example.com')], { method: 'GET', url: 'http://example.com/', body: null }],
9-
['Request URL only', [{ url: 'http://example.com' }], { method: 'GET', url: 'http://example.com', body: null }],
5+
['string URL only', ['http://example.com'], { method: 'GET', url: 'http://example.com' }],
6+
['URL object only', [new URL('http://example.com')], { method: 'GET', url: 'http://example.com/' }],
7+
['Request URL only', [{ url: 'http://example.com' }], { method: 'GET', url: 'http://example.com' }],
108
[
119
'Request URL & method only',
1210
[{ url: 'http://example.com', method: 'post' }],
13-
{ method: 'POST', url: 'http://example.com', body: null },
11+
{ method: 'POST', url: 'http://example.com' },
1412
],
13+
['string URL & options', ['http://example.com', { method: 'post' }], { method: 'POST', url: 'http://example.com' }],
14+
[
15+
'URL object & options',
16+
[new URL('http://example.com'), { method: 'post' }],
17+
{ method: 'POST', url: 'http://example.com/' },
18+
],
19+
[
20+
'Request URL & options',
21+
[{ url: 'http://example.com' }, { method: 'post' }],
22+
{ method: 'POST', url: 'http://example.com' },
23+
],
24+
])('%s', (_name, args, expected) => {
25+
const actual = parseFetchArgs(args as unknown[]);
26+
27+
expect(actual).toEqual(expected);
28+
});
29+
});
30+
31+
describe('instrument > parseFetchPayload', () => {
32+
const data = [1, 2, 3];
33+
const jsonData = '{"data":[1,2,3]}';
34+
35+
it.each([
36+
['string URL only', ['http://example.com'], undefined],
37+
['URL object only', [new URL('http://example.com')], undefined],
38+
['Request URL only', [{ url: 'http://example.com' }], undefined],
1539
[
16-
'string URL & options',
17-
['http://example.com', { method: 'post', body: JSON.stringify(data) }],
18-
{ method: 'POST', url: 'http://example.com', body: '{"name":"Test"}' },
40+
'Request URL & method only',
41+
[{ url: 'http://example.com', method: 'post', body: JSON.stringify({ data }) }],
42+
jsonData,
1943
],
44+
['string URL & options', ['http://example.com', { method: 'post', body: JSON.stringify({ data }) }], jsonData],
2045
[
2146
'URL object & options',
22-
[new URL('http://example.com'), { method: 'post', body: JSON.stringify(data) }],
23-
{ method: 'POST', url: 'http://example.com/', body: '{"name":"Test"}' },
47+
[new URL('http://example.com'), { method: 'post', body: JSON.stringify({ data }) }],
48+
jsonData,
2449
],
2550
[
2651
'Request URL & options',
27-
[{ url: 'http://example.com' }, { method: 'post', body: JSON.stringify(data) }],
28-
{ method: 'POST', url: 'http://example.com', body: '{"name":"Test"}' },
52+
[{ url: 'http://example.com' }, { method: 'post', body: JSON.stringify({ data }) }],
53+
jsonData,
2954
],
3055
])('%s', (_name, args, expected) => {
31-
const actual = parseFetchArgs(args as unknown[]);
56+
const actual = parseFetchPayload(args as unknown[]);
3257

3358
expect(actual).toEqual(expected);
3459
});

0 commit comments

Comments
 (0)