-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(browser): Add graphqlClientIntegration
#13783
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
Merged
lforst
merged 78 commits into
getsentry:develop
from
Zen-cronic:feat/graphqlClientIntegration
Feb 11, 2025
Merged
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 17e89b1
feat(browser): Add support for fetch graphql request
Zen-cronic be96da7
test(browser): Remove skip test
Zen-cronic b985d6a
fix(browser): Attach request payload to fetch instrumentation only fo…
Zen-cronic f7a021b
fix(browser): Emit the `outgoingRequestSpanStart` hook after the span…
Zen-cronic 285e619
feat(browser): Update breadcrumbs with graphql request data
Zen-cronic 0095a88
fix(browser): Change breadcrumb hook signature to not be graphql-spec…
Zen-cronic 47618d8
fix(browser): Change span hook signature to not be graphql-specific
Zen-cronic 581374b
fix(browser): Add more requested fixes
Zen-cronic 6ef6953
fix(browser): Refactor to reduce type assertions
Zen-cronic 1cbe786
chore(browser): Fix lint errors
Zen-cronic 4f64feb
fix(browser): Refactor span handler to use hint
Zen-cronic 7517a93
fix(browser): Refactor breadcrumb handlers to use hint
Zen-cronic 8dcf72b
test(browser): Add tests for `getRequestPayloadXhrOrFetch`
Zen-cronic 7fdec36
refactor(browser): Remove type assertions
Zen-cronic ae04bda
fix(browser): Remove unnecessary `FetchInput` type
Zen-cronic 630d678
chore(browser): Remove deleted import
Zen-cronic 719d727
fix(browser): Resolve rebase conflicts
Zen-cronic 4c20f14
fix(core): Revert resolved rebase conflict
Zen-cronic 4ab14d3
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic ff49b03
fix: Lint
Zen-cronic 8ea71ba
refactor(browser-utils): Move `getBodyString`
Zen-cronic f778e13
refactor(core): Move `hasProp`
Zen-cronic eccfa79
chore: Merge import statements
Zen-cronic e4581b7
refactor(browser-utils): Move `FetchHint` and `XhrHint`
Zen-cronic 521f61f
chore(e2e): Replace deprecated imports
Zen-cronic 7a75d8a
feat(browser): Add `graphqlClientIntegration`
Zen-cronic b611943
feat(browser): Add support for fetch graphql request
Zen-cronic 1be1082
test(browser): Remove skip test
Zen-cronic 29c1dc1
fix(browser): Attach request payload to fetch instrumentation only fo…
Zen-cronic 0a5663c
fix(browser): Emit the `outgoingRequestSpanStart` hook after the span…
Zen-cronic f5320b2
feat(browser): Update breadcrumbs with graphql request data
Zen-cronic 26a7873
fix(browser): Change breadcrumb hook signature to not be graphql-spec…
Zen-cronic f0c88e8
fix(browser): Change span hook signature to not be graphql-specific
Zen-cronic 4e78e20
fix(browser): Add more requested fixes
Zen-cronic 4cb51b4
fix(browser): Refactor to reduce type assertions
Zen-cronic 95dcbe8
chore(browser): Fix lint errors
Zen-cronic efbfe1f
fix(browser): Refactor span handler to use hint
Zen-cronic 787b18c
fix(browser): Refactor breadcrumb handlers to use hint
Zen-cronic 93a4853
test(browser): Add tests for `getRequestPayloadXhrOrFetch`
Zen-cronic 859a1ee
refactor(browser): Remove type assertions
Zen-cronic ad6bc30
fix(browser): Remove unnecessary `FetchInput` type
Zen-cronic f4c151a
chore(browser): Remove deleted import
Zen-cronic 8d5550e
fix(browser): Resolve rebase conflicts
Zen-cronic 3e3322d
fix(core): Revert resolved rebase conflict
Zen-cronic 8ec59ce
fix: Lint
Zen-cronic 59c8fcd
refactor(browser-utils): Move `getBodyString`
Zen-cronic ee8b1d7
refactor(core): Move `hasProp`
Zen-cronic 6d0d3c5
chore: Merge import statements
Zen-cronic db865bf
refactor(browser-utils): Move `FetchHint` and `XhrHint`
Zen-cronic 2e8d2ae
chore(e2e): Replace deprecated imports
Zen-cronic 07af617
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic 1405707
Merge remote-tracking branch 'refs/remotes/origin/feat/graphqlClientI…
Zen-cronic 129a774
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic d3ba78f
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic 4d06a2f
chore(browser-utils): Remove jest
Zen-cronic 67e2971
fix(browser): Change parsing the fetch req payload to allow more type…
Zen-cronic 200d810
ref(browser-utils): Use undefined assertion
Zen-cronic 514ef47
ref(replay): Extend ReplayLogger from core Logger
Zen-cronic a3a36e3
ref(browser-utils): Revert `getBodyString` tests to original case
Zen-cronic 22c2c92
fix(core): Make `endTimestamp` of `FetchBreadcrumbHint` optional
Zen-cronic e7876d7
feat(browser): Add support for queries with operation name
Zen-cronic 694da93
feat(browser): Add support for queries without operation name
Zen-cronic 998a3fe
Merge branch 'feat/graphqlClientIntegration' of https://github.com/Ze…
Zen-cronic d8b0a83
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic 68f7d1a
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic 1136d87
feat(svelte)!: Disable component update tracking by default (#15265)
Lms24 9c2ed9e
ref(sveltekit): Clean up sub-request check (#15251)
Lms24 f490c91
feat(core): Add `inheritOrSampleWith` helper to `traceSampler` (#15277)
3a03766
feat(user feedback): Adds toolbar for cropping and annotating (#15282)
c298lee 41dc493
fix(nuxt): Detect Azure Function runtime for flushing with timeout (#…
s1gr1d c8f69fe
Add graphqlclient CDN bundle & fix bundle tests
mydea 7150923
Merge branch 'develop' into feat/graphqlClientIntegration
Zen-cronic 286f0d7
fix(browser-utils): Use passed in logger
Zen-cronic 681fe9e
Merge branch 'develop' into feat/graphqlClientIntegration
Zen-cronic a0f7aeb
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic 141c67e
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic 054ac5e
Merge branch 'getsentry:develop' into feat/graphqlClientIntegration
Zen-cronic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
17 changes: 17 additions & 0 deletions
17
dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/subject.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); |
103 changes: 103 additions & 0 deletions
103
dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}); | ||
}); |
16 changes: 16 additions & 0 deletions
16
dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); |
15 changes: 15 additions & 0 deletions
15
dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/subject.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
102 changes: 102 additions & 0 deletions
102
dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
return [undefined, 'BODY_PARSE_ERROR']; | ||
} | ||
|
||
DEBUG_BUILD && logger.info('Skipping network body because of body type', body); | ||
Zen-cronic marked this conversation as resolved.
Outdated
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; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.