Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(browser): Add graphqlClientIntegration #13783

Merged
merged 78 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 73 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
b88b4f9
feat(browser): Add `graphqlClientIntegration`
Zen-cronic Sep 25, 2024
17e89b1
feat(browser): Add support for fetch graphql request
Zen-cronic Sep 26, 2024
be96da7
test(browser): Remove skip test
Zen-cronic Sep 26, 2024
b985d6a
fix(browser): Attach request payload to fetch instrumentation only fo…
Zen-cronic Sep 27, 2024
f7a021b
fix(browser): Emit the `outgoingRequestSpanStart` hook after the span…
Zen-cronic Sep 27, 2024
285e619
feat(browser): Update breadcrumbs with graphql request data
Zen-cronic Sep 29, 2024
0095a88
fix(browser): Change breadcrumb hook signature to not be graphql-spec…
Zen-cronic Jan 7, 2025
47618d8
fix(browser): Change span hook signature to not be graphql-specific
Zen-cronic Jan 7, 2025
581374b
fix(browser): Add more requested fixes
Zen-cronic Jan 7, 2025
6ef6953
fix(browser): Refactor to reduce type assertions
Zen-cronic Jan 7, 2025
1cbe786
chore(browser): Fix lint errors
Zen-cronic Jan 7, 2025
4f64feb
fix(browser): Refactor span handler to use hint
Zen-cronic Jan 19, 2025
7517a93
fix(browser): Refactor breadcrumb handlers to use hint
Zen-cronic Jan 19, 2025
8dcf72b
test(browser): Add tests for `getRequestPayloadXhrOrFetch`
Zen-cronic Jan 19, 2025
7fdec36
refactor(browser): Remove type assertions
Zen-cronic Jan 19, 2025
ae04bda
fix(browser): Remove unnecessary `FetchInput` type
Zen-cronic Jan 19, 2025
630d678
chore(browser): Remove deleted import
Zen-cronic Jan 23, 2025
719d727
fix(browser): Resolve rebase conflicts
Zen-cronic Jan 23, 2025
4c20f14
fix(core): Revert resolved rebase conflict
Zen-cronic Jan 23, 2025
4ab14d3
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic Jan 23, 2025
ff49b03
fix: Lint
Zen-cronic Jan 23, 2025
8ea71ba
refactor(browser-utils): Move `getBodyString`
Zen-cronic Jan 23, 2025
f778e13
refactor(core): Move `hasProp`
Zen-cronic Jan 23, 2025
eccfa79
chore: Merge import statements
Zen-cronic Jan 23, 2025
e4581b7
refactor(browser-utils): Move `FetchHint` and `XhrHint`
Zen-cronic Jan 23, 2025
521f61f
chore(e2e): Replace deprecated imports
Zen-cronic Jan 23, 2025
7a75d8a
feat(browser): Add `graphqlClientIntegration`
Zen-cronic Sep 25, 2024
b611943
feat(browser): Add support for fetch graphql request
Zen-cronic Sep 26, 2024
1be1082
test(browser): Remove skip test
Zen-cronic Sep 26, 2024
29c1dc1
fix(browser): Attach request payload to fetch instrumentation only fo…
Zen-cronic Sep 27, 2024
0a5663c
fix(browser): Emit the `outgoingRequestSpanStart` hook after the span…
Zen-cronic Sep 27, 2024
f5320b2
feat(browser): Update breadcrumbs with graphql request data
Zen-cronic Sep 29, 2024
26a7873
fix(browser): Change breadcrumb hook signature to not be graphql-spec…
Zen-cronic Jan 7, 2025
f0c88e8
fix(browser): Change span hook signature to not be graphql-specific
Zen-cronic Jan 7, 2025
4e78e20
fix(browser): Add more requested fixes
Zen-cronic Jan 7, 2025
4cb51b4
fix(browser): Refactor to reduce type assertions
Zen-cronic Jan 7, 2025
95dcbe8
chore(browser): Fix lint errors
Zen-cronic Jan 7, 2025
efbfe1f
fix(browser): Refactor span handler to use hint
Zen-cronic Jan 19, 2025
787b18c
fix(browser): Refactor breadcrumb handlers to use hint
Zen-cronic Jan 19, 2025
93a4853
test(browser): Add tests for `getRequestPayloadXhrOrFetch`
Zen-cronic Jan 19, 2025
859a1ee
refactor(browser): Remove type assertions
Zen-cronic Jan 19, 2025
ad6bc30
fix(browser): Remove unnecessary `FetchInput` type
Zen-cronic Jan 19, 2025
f4c151a
chore(browser): Remove deleted import
Zen-cronic Jan 23, 2025
8d5550e
fix(browser): Resolve rebase conflicts
Zen-cronic Jan 23, 2025
3e3322d
fix(core): Revert resolved rebase conflict
Zen-cronic Jan 23, 2025
8ec59ce
fix: Lint
Zen-cronic Jan 23, 2025
59c8fcd
refactor(browser-utils): Move `getBodyString`
Zen-cronic Jan 23, 2025
ee8b1d7
refactor(core): Move `hasProp`
Zen-cronic Jan 23, 2025
6d0d3c5
chore: Merge import statements
Zen-cronic Jan 23, 2025
db865bf
refactor(browser-utils): Move `FetchHint` and `XhrHint`
Zen-cronic Jan 23, 2025
2e8d2ae
chore(e2e): Replace deprecated imports
Zen-cronic Jan 23, 2025
07af617
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic Jan 24, 2025
1405707
Merge remote-tracking branch 'refs/remotes/origin/feat/graphqlClientI…
Zen-cronic Jan 24, 2025
129a774
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic Jan 29, 2025
d3ba78f
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic Jan 29, 2025
4d06a2f
chore(browser-utils): Remove jest
Zen-cronic Jan 29, 2025
67e2971
fix(browser): Change parsing the fetch req payload to allow more type…
Zen-cronic Jan 29, 2025
200d810
ref(browser-utils): Use undefined assertion
Zen-cronic Jan 29, 2025
514ef47
ref(replay): Extend ReplayLogger from core Logger
Zen-cronic Jan 30, 2025
a3a36e3
ref(browser-utils): Revert `getBodyString` tests to original case
Zen-cronic Jan 30, 2025
22c2c92
fix(core): Make `endTimestamp` of `FetchBreadcrumbHint` optional
Zen-cronic Jan 31, 2025
e7876d7
feat(browser): Add support for queries with operation name
Zen-cronic Jan 31, 2025
694da93
feat(browser): Add support for queries without operation name
Zen-cronic Jan 31, 2025
998a3fe
Merge branch 'feat/graphqlClientIntegration' of https://github.com/Ze…
Zen-cronic Jan 31, 2025
d8b0a83
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic Jan 31, 2025
68f7d1a
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic Feb 4, 2025
1136d87
feat(svelte)!: Disable component update tracking by default (#15265)
Lms24 Feb 4, 2025
9c2ed9e
ref(sveltekit): Clean up sub-request check (#15251)
Lms24 Feb 4, 2025
f490c91
feat(core): Add `inheritOrSampleWith` helper to `traceSampler` (#15277)
lforst Feb 4, 2025
3a03766
feat(user feedback): Adds toolbar for cropping and annotating (#15282)
c298lee Feb 4, 2025
41dc493
fix(nuxt): Detect Azure Function runtime for flushing with timeout (#…
s1gr1d Feb 5, 2025
c8f69fe
Add graphqlclient CDN bundle & fix bundle tests
mydea Feb 5, 2025
7150923
Merge branch 'develop' into feat/graphqlClientIntegration
Zen-cronic Feb 5, 2025
286f0d7
fix(browser-utils): Use passed in logger
Zen-cronic Feb 6, 2025
681fe9e
Merge branch 'develop' into feat/graphqlClientIntegration
Zen-cronic Feb 6, 2025
a0f7aeb
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic Feb 7, 2025
141c67e
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic Feb 10, 2025
054ac5e
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic Feb 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const query = `query Test{
people {
name
pet
}
}`;

const requestBody = JSON.stringify({ query });

fetch('http://sentry-test.io/foo', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: requestBody,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';

import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';

// Duplicate from subject.js
const query = `query Test{
people {
name
pet
}
}`;
const queryPayload = JSON.stringify({ query });

sentryTest('should update spans for GraphQL fetch requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
return;
}

const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', route => {
return route.fulfill({
status: 200,
body: JSON.stringify({
people: [
{ name: 'Amy', pet: 'dog' },
{ name: 'Jay', pet: 'cat' },
],
}),
headers: {
'Content-Type': 'application/json',
},
});
});

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client');

expect(requestSpans).toHaveLength(1);

expect(requestSpans![0]).toMatchObject({
description: 'POST http://sentry-test.io/foo (query Test)',
parent_span_id: eventData.contexts?.trace?.span_id,
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: eventData.contexts?.trace?.trace_id,
status: 'ok',
data: expect.objectContaining({
type: 'fetch',
'http.method': 'POST',
'http.url': 'http://sentry-test.io/foo',
url: 'http://sentry-test.io/foo',
'server.address': 'sentry-test.io',
'sentry.op': 'http.client',
'sentry.origin': 'auto.http.browser',
'graphql.document': queryPayload,
}),
});
});

sentryTest('should update breadcrumbs for GraphQL fetch requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
return;
}

const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', route => {
return route.fulfill({
status: 200,
body: JSON.stringify({
people: [
{ name: 'Amy', pet: 'dog' },
{ name: 'Jay', pet: 'cat' },
],
}),
headers: {
'Content-Type': 'application/json',
},
});
});

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData?.breadcrumbs?.length).toBe(1);

expect(eventData!.breadcrumbs![0]).toEqual({
timestamp: expect.any(Number),
category: 'fetch',
type: 'http',
data: {
method: 'POST',
status_code: 200,
url: 'http://sentry-test.io/foo',
__span: expect.any(String),
'graphql.document': query,
'graphql.operation': 'query Test',
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Sentry from '@sentry/browser';
// Must import this like this to ensure the test transformation for CDN bundles works
import { graphqlClientIntegration } from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [
Sentry.browserTracingIntegration(),
graphqlClientIntegration({
endpoints: ['http://sentry-test.io/foo'],
}),
],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const xhr = new XMLHttpRequest();

xhr.open('POST', 'http://sentry-test.io/foo');
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('Content-Type', 'application/json');

const query = `query Test{
people {
name
pet
}
}`;

const requestBody = JSON.stringify({ query });
xhr.send(requestBody);
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';

import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';

// Duplicate from subject.js
const query = `query Test{
people {
name
pet
}
}`;
const queryPayload = JSON.stringify({ query });

sentryTest('should update spans for GraphQL XHR requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
return;
}

const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', route => {
return route.fulfill({
status: 200,
body: JSON.stringify({
people: [
{ name: 'Amy', pet: 'dog' },
{ name: 'Jay', pet: 'cat' },
],
}),
headers: {
'Content-Type': 'application/json',
},
});
});

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client');

expect(requestSpans).toHaveLength(1);

expect(requestSpans![0]).toMatchObject({
description: 'POST http://sentry-test.io/foo (query Test)',
parent_span_id: eventData.contexts?.trace?.span_id,
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: eventData.contexts?.trace?.trace_id,
status: 'ok',
data: {
type: 'xhr',
'http.method': 'POST',
'http.url': 'http://sentry-test.io/foo',
url: 'http://sentry-test.io/foo',
'server.address': 'sentry-test.io',
'sentry.op': 'http.client',
'sentry.origin': 'auto.http.browser',
'graphql.document': queryPayload,
},
});
});

sentryTest('should update breadcrumbs for GraphQL XHR requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
return;
}

const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/foo', route => {
return route.fulfill({
status: 200,
body: JSON.stringify({
people: [
{ name: 'Amy', pet: 'dog' },
{ name: 'Jay', pet: 'cat' },
],
}),
headers: {
'Content-Type': 'application/json',
},
});
});

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData?.breadcrumbs?.length).toBe(1);

expect(eventData!.breadcrumbs![0]).toEqual({
timestamp: expect.any(Number),
category: 'xhr',
type: 'http',
data: {
method: 'POST',
status_code: 200,
url: 'http://sentry-test.io/foo',
'graphql.document': query,
'graphql.operation': 'query Test',
},
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record<string, string> = {
reportingObserverIntegration: 'reportingobserver',
feedbackIntegration: 'feedback',
moduleMetadataIntegration: 'modulemetadata',
graphqlClientIntegration: 'graphqlclient',
// technically, this is not an integration, but let's add it anyway for simplicity
makeMultiplexedTransport: 'multiplexedtransport',
};
Expand Down
1 change: 0 additions & 1 deletion packages/browser-utils/jest.config.js

This file was deleted.

6 changes: 3 additions & 3 deletions packages/browser-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@
"clean": "rimraf build coverage sentry-internal-browser-utils-*.tgz",
"fix": "eslint . --format stylish --fix",
"lint": "eslint . --format stylish",
"test:unit": "jest",
"test": "jest",
"test:watch": "jest --watch",
"test:unit": "vitest run",
"test": "vitest run",
"test:watch": "vitest --watch",
"yalc:publish": "yalc publish --push --sig"
},
"volta": {
Expand Down
4 changes: 4 additions & 0 deletions packages/browser-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ export { addHistoryInstrumentationHandler } from './instrument/history';
export { fetch, setTimeout, clearCachedImplementation, getNativeImplementation } from './getNativeImplementation';

export { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY } from './instrument/xhr';

export { getBodyString, getFetchRequestArgBody, serializeFormData } from './networkUtils';

export type { FetchHint, NetworkMetaWarning, XhrHint } from './types';
57 changes: 57 additions & 0 deletions packages/browser-utils/src/networkUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { logger } from '@sentry/core';
import type { Logger } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import type { NetworkMetaWarning } from './types';

/**
* Serializes FormData.
*
* This is a bit simplified, but gives us a decent estimate.
* This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13'.
*
*/
export function serializeFormData(formData: FormData): string {
// @ts-expect-error passing FormData to URLSearchParams actually works
return new URLSearchParams(formData).toString();
}

/** Get the string representation of a body. */
export function getBodyString(body: unknown, _logger: Logger = logger): [string | undefined, NetworkMetaWarning?] {
try {
if (typeof body === 'string') {
return [body];
}

if (body instanceof URLSearchParams) {
return [body.toString()];
}

if (body instanceof FormData) {
return [serializeFormData(body)];
}

if (!body) {
return [undefined];
}
} catch (error) {
DEBUG_BUILD && logger.error(error, 'Failed to serialize body', body);
Zen-cronic marked this conversation as resolved.
Show resolved Hide resolved
return [undefined, 'BODY_PARSE_ERROR'];
}

DEBUG_BUILD && logger.info('Skipping network body because of body type', body);
Zen-cronic marked this conversation as resolved.
Show resolved Hide resolved

return [undefined, 'UNPARSEABLE_BODY_TYPE'];
}

/**
* Parses the fetch arguments to extract the request payload.
*
* We only support getting the body from the fetch options.
*/
export function getFetchRequestArgBody(fetchArgs: unknown[] = []): RequestInit['body'] | undefined {
if (fetchArgs.length !== 2 || typeof fetchArgs[1] !== 'object') {
return undefined;
}

return (fetchArgs[1] as RequestInit).body;
}
25 changes: 25 additions & 0 deletions packages/browser-utils/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import type {
FetchBreadcrumbHint,
HandlerDataFetch,
SentryWrappedXMLHttpRequest,
XhrBreadcrumbHint,
} from '@sentry/core';
import { GLOBAL_OBJ } from '@sentry/core';

export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ &
// document is not available in all browser environments (webworkers). We make it optional so you have to explicitly check for it
Omit<Window, 'document'> &
Partial<Pick<Window, 'document'>>;

export type NetworkMetaWarning =
| 'MAYBE_JSON_TRUNCATED'
| 'TEXT_TRUNCATED'
| 'URL_SKIPPED'
| 'BODY_PARSE_ERROR'
| 'BODY_PARSE_TIMEOUT'
| 'UNPARSEABLE_BODY_TYPE';

type RequestBody = null | Blob | BufferSource | FormData | URLSearchParams | string;

export type XhrHint = XhrBreadcrumbHint & {
xhr: XMLHttpRequest & SentryWrappedXMLHttpRequest;
input?: RequestBody;
};
export type FetchHint = FetchBreadcrumbHint & {
input: HandlerDataFetch['args'];
response: Response;
};
Loading
Loading