-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Open
Zen-cronic
wants to merge
62
commits into
getsentry:develop
Choose a base branch
from
Zen-cronic:feat/graphqlClientIntegration
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+897
−173
Open
Changes from 54 commits
Commits
Show all changes
62 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 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 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, | ||
}); |
95 changes: 95 additions & 0 deletions
95
dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts
This file contains 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,95 @@ | ||
import { expect } from '@playwright/test'; | ||
import type { Event } from '@sentry/core'; | ||
|
||
import { sentryTest } from '../../../../utils/fixtures'; | ||
import { getFirstSentryEnvelopeRequest } 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 }) => { | ||
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 }) => { | ||
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', | ||
}, | ||
}); | ||
}); |
14 changes: 14 additions & 0 deletions
14
dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js
This file contains 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,14 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
|
||
window.Sentry = Sentry; | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
integrations: [ | ||
Sentry.browserTracingIntegration(), | ||
Sentry.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 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); |
94 changes: 94 additions & 0 deletions
94
dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts
This file contains 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,94 @@ | ||
import { expect } from '@playwright/test'; | ||
import type { Event } from '@sentry/core'; | ||
|
||
import { sentryTest } from '../../../../utils/fixtures'; | ||
import { getFirstSentryEnvelopeRequest } 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 }) => { | ||
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 }) => { | ||
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 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 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 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,72 @@ | ||
import type { ConsoleLevel, Logger } from '@sentry/core'; | ||
import { DEBUG_BUILD } from './debug-build'; | ||
import type { NetworkMetaWarning } from './types'; | ||
|
||
type ReplayConsoleLevels = Extract<ConsoleLevel, 'info' | 'warn' | 'error' | 'log'>; | ||
type LoggerMethod = (...args: unknown[]) => void; | ||
type LoggerConsoleMethods = Record<ReplayConsoleLevels, LoggerMethod>; | ||
|
||
interface LoggerConfig { | ||
captureExceptions: boolean; | ||
traceInternals: boolean; | ||
} | ||
|
||
// Duplicate from replay-internal | ||
interface ReplayLogger extends LoggerConsoleMethods { | ||
/** | ||
* Calls `logger.info` but saves breadcrumb in the next tick due to race | ||
* conditions before replay is initialized. | ||
*/ | ||
infoTick: LoggerMethod; | ||
/** | ||
* Captures exceptions (`Error`) if "capture internal exceptions" is enabled | ||
*/ | ||
exception: LoggerMethod; | ||
/** | ||
* Configures the logger with additional debugging behavior | ||
*/ | ||
setConfig(config: Partial<LoggerConfig>): void; | ||
} | ||
|
||
function _serializeFormData(formData: FormData): string { | ||
// 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' | ||
// @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 | ReplayLogger, | ||
): [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) { | ||
// RelayLogger | ||
if (DEBUG_BUILD && logger && 'exception' in logger) { | ||
logger.exception(error, 'Failed to serialize body', body); | ||
} else if (DEBUG_BUILD && logger) { | ||
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); | ||
|
||
return [undefined, 'UNPARSEABLE_BODY_TYPE']; | ||
} |
This file contains 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this, imo, is not the optimal approach - it's only duped here b/c replay-internal needs to use the
exception
method ingetBodyString
to set the PREFIX asReplay
:sentry-javascript/packages/replay-internal/src/coreHandlers/util/networkUtils.ts
Line 82 in ad5418d
sentry-javascript/packages/replay-internal/src/util/logger.ts
Lines 70 to 75 in ad5418d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, let's get rid of this here and just use the regular
logger.log()
instead! Make the type for the argumentlogger?: Logger
which should also match for replay logger, I suppose, as that extends the core logger?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would go even one step further and make this:
so default this to the regular logger, then we do not need to guard this in the code!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that makes the most sense. But apparently,
ReplayLogger
has always been extended fromLoggerConsoleMethods
instead of the coreLogger
:sentry-javascript/packages/replay-internal/src/util/logger.ts
Lines 5 to 17 in 9ce4ad1
It's a decision i'm not aware of (it could've inherited from
Logger
since it's written), so i'm not sure about the implication of switching toLogger
as the base class.I tried the switch locally - both the unit tests (in
replay-internal
) and the integration/e2e tests pass, so it seems to not have caused any issue.If this switch is considered OK, then I'll proceed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be fine! making this just accept
Logger
seems right to me and should be OK for replay too :)