-
-
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
base: develop
Are you sure you want to change the base?
feat(browser): Add graphqlClientIntegration
#13783
Conversation
Added support for graphql query with `xhr` with tests. Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
@@ -357,6 +357,8 @@ export function xhrCallback( | |||
return undefined; | |||
} | |||
|
|||
const requestBody = JSON.parse(sentryXhrData.body as string); |
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 is dangerous, any such operations we have to try-catch as bodies may not be JSON!
@@ -374,6 +376,7 @@ export function xhrCallback( | |||
'server.address': host, | |||
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', | |||
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', | |||
body: requestBody, |
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.
h: We should not do this - this will attach the request bodies to all spans, always, which is a) large, b) potentially PII sensitive 😬
Instead of doing this in on('spanStart')
, I think we'll need a hook that provides the request or body in some way. I am thinking of a new hook like:
client.on('outgoingRequestSpanStart', (span: Span, { body }: { body: unknown }) => {
// ...
});
and emit this hook in this file after the span was started 🤔
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.
Noted. I implemented that hook and attached the req payload only to graphql spans.
Added test for fetch graphql. Created new utility functions and added tests. Updated `instrumentFetch` to collect fetch request payload. Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
const handlerData: HandlerDataFetch = { | ||
args, | ||
fetchData: { | ||
method, | ||
url, | ||
body, |
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.
IMO collecting fetch req payload shouldn't be a problem as long as only the graphql requests are sampled. Because xhr instrumentation already collects the payload.
Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
|
…r graphql requests Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
const requestBody = JSON.parse(payload); | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
const isGraphQLRequest = !!requestBody['query']; |
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 may not be the most accurate and safest way to detect a graphql request?
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.
If we only move this into the graphql integration, instead of runing this for every request, we can IMHO safely make this check, because stuff already ran through the endpoints
check, so we can assume it should be fine!
… has started Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
Just wanted to inform about the failing tests:
|
dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts
Outdated
Show resolved
Hide resolved
|
||
function _getGraphQLOperation(requestBody: unknown): string { | ||
// Standard graphql request shape: https://graphql.org/learn/serving-over-http/#post-request | ||
const graphqlBody = requestBody as GraphQLRequestPayload; |
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.
if we do not know this is a body, we should at least try-catch all of this. But if we (as mentioned in comments above) use getBodyString
, we can simply pass this in types as string, as we know it was converted before (or is undefined, in which case, nothing to do)
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 finally found the time to review this in depth. Overall, I really like it, I left some comments with requests for changes. Mostly it is about moving stuff around, and avoiding any graphql-specific code in the "general" places. Also, please note that if you rebase this in develop
, there are/will be a bunch of conflicts - this is mostly because the utils
package is deprecated and all that code was moved to core
.
noted, i'll implement the feedback. thanks for the comprehensive review!
will move on with the conflicts if the current implementation checks out |
…ific - Updated `outgoingRequestBreadcrumbStart` hook name to `beforeOutgoingRequestBreadcrumb`. - Updated standard graphql request payload structure Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
@@ -0,0 +1,121 @@ | |||
import { SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils'; | |||
import { getBodyString } from '@sentry-internal/replay'; |
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.
@mydea should this util be in the replay package?
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.
if we use/need it here, I would move it to @sentry-internal/browser-utils
, and use it from there both in replay and here!
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.
otherwise, this will lead to circular dependency issues :)
- Renamed `outgoingRequestSpanStart` hook to `beforeOutgoingRequestSpan`. - Followed Otel semantic for GraphQL Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
- Added guard for missing `httpMethod`. - Refactored getting body based on xhr or fetch logic into a function. - Added Otel semantic in breadcrumb data. Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
|
||
if (!data.graphql && graphqlBody) { | ||
const operationInfo = _getGraphQLOperation(graphqlBody as GraphQLRequestPayload); | ||
data['graphql.document'] = (graphqlBody as GraphQLRequestPayload).query; |
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.
currently, these assertions are needed b/c the util getGraphQLRequestPayload
doesn't have access to the GraphQLRequestPayload
type.
|
||
const isHttpClientSpan = spanOp === 'http.client'; | ||
|
||
if (isHttpClientSpan) { |
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.
l: I would invert this and just early-return, makes this a bit easier to read IMHO :) so:
if (!isHttpClientSpan) {
return;
}
// rest of logic here...
*/ | ||
on( | ||
hook: 'beforeOutgoingRequestSpan', | ||
callback: (span: Span, handlerData: HandlerDataXhr | HandlerDataFetch) => void, |
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.
m: Let's instead type this like this:
callback: (span: Span, handlerData: HandlerDataXhr | HandlerDataFetch) => void, | |
callback: (span: Span, handlerData: FetchBreadcrumbHint | XhrBreadcrumbHint) => void, |
So give it the (typed) hint instead. this should be a bit more "stable" then the handler data, and the relevant handler data should be part of the hint instead?
*/ | ||
on( | ||
hook: 'beforeOutgoingRequestBreadcrumb', | ||
callback: (breadcrumb: Breadcrumb, handlerData: HandlerDataXhr | HandlerDataFetch) => void, |
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.
same here:
callback: (breadcrumb: Breadcrumb, handlerData: HandlerDataXhr | HandlerDataFetch) => void, | |
callback: (breadcrumb: Breadcrumb, handlerData: FetchBreadcrumbHint | XhrBreadcrumbHint) => void, |
} | ||
|
||
// The body prop attached to HandlerDataFetch for the span should be removed. | ||
if (isFetch && data.body) { |
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.
do we need to unset this? what happens if we do not do that?
@@ -58,6 +59,15 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat | |||
startTimestamp: timestampInSeconds() * 1000, | |||
}; | |||
|
|||
const body = parseFetchPayload(args); |
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.
The main work that IMHO is left is to remove this code here, and to instead do this in the integration itself. Through the hint you should have access to the args, so you should be able to get the payload in there directly (in a hook) instead of doing this generically for all requests.
Then, you can inline the getGraphQLRequestPayload
code too into the integration itself, which should make typing etc. stuff much easier. Ideally, this file is not touched at all, and the only thing that is added outside of the integration is the hooks configuration - everything else (more or less) is encapsulated in the integration :)
I looked over it again, apart from some rebase stuff that is sadly going to have to happen 😬 (e.g. the client type is now gone and just part of core/client now, This: https://github.com/getsentry/sentry-javascript/pull/13783/files#r1913443495 is the main thing left to do IMHO - if you need more help/pointers please let us know! |
Resolves #13399
Todo:
Supportfetch
spans and testsSupport shorthand graphql queries (i.e., without a name)Moved to laterEnhance breadcrumb datayarn lint
) & (yarn test
).