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

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from

Conversation

Zen-cronic
Copy link
Contributor

@Zen-cronic Zen-cronic commented Sep 25, 2024

Resolves #13399

Todo:

  • Support fetch spans and tests
  • Support shorthand graphql queries (i.e., without a name) Moved to later
  • Enhance breadcrumb data

  • If you've added code that should be tested, please add tests.
  • Ensure your code lints and the test suite passes (yarn lint) & (yarn test).

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);
Copy link
Member

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,
Copy link
Member

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 🤔

Copy link
Contributor Author

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,
Copy link
Contributor Author

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.

@Zen-cronic Zen-cronic requested a review from mydea September 26, 2024 15:05
Signed-off-by: Kaung Zin Hein <kaungzinhein113@gmail.com>
@Zen-cronic
Copy link
Contributor Author

Zen-cronic commented Sep 26, 2024

  • Todo: fix failing fetch tests by removing body from non-graphql requests

…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'];
Copy link
Contributor Author

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?

Copy link
Member

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>
@Zen-cronic Zen-cronic requested a review from mydea September 30, 2024 13:02
@Zen-cronic
Copy link
Contributor Author

Just wanted to inform about the failing tests:


function _getGraphQLOperation(requestBody: unknown): string {
// Standard graphql request shape: https://graphql.org/learn/serving-over-http/#post-request
const graphqlBody = requestBody as GraphQLRequestPayload;
Copy link
Member

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)

Copy link
Member

@mydea mydea left a 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.

@Zen-cronic
Copy link
Contributor Author

Zen-cronic commented Dec 20, 2024

noted, i'll implement the feedback. thanks for the comprehensive review!

  • update span hook
  • update breadcrumb hook
  • resolve rebase conflicts

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>
@Zen-cronic Zen-cronic requested a review from a team as a code owner January 7, 2025 15:53
@@ -0,0 +1,121 @@
import { SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils';
import { getBodyString } from '@sentry-internal/replay';
Copy link
Member

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?

Copy link
Member

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!

Copy link
Member

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;
Copy link
Contributor Author

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.

@Zen-cronic Zen-cronic requested a review from mydea January 7, 2025 18:58

const isHttpClientSpan = spanOp === 'http.client';

if (isHttpClientSpan) {
Copy link
Member

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,
Copy link
Member

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:

Suggested change
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here:

Suggested change
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) {
Copy link
Member

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);
Copy link
Member

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 :)

@mydea
Copy link
Member

mydea commented Jan 13, 2025

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, @sentry/types is gone and types should just be imported from @sentry/core now, ...), overall I want to stress that this is a really cool PR!

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add optional graphqlClientIntegration to @sentry/browser
4 participants