-
-
Notifications
You must be signed in to change notification settings - Fork 452
Improve server side GraphQL support for spring-graphql and Nextflix DGS #2856
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
Changes from all commits
426da00
10194c8
2ec2bbb
c7ab0eb
65fd0a3
0f2e929
53e1486
3ca6c27
d9ed5ec
86e964a
2864d0c
797e46c
2ce76e0
eada4d3
34e560b
dbd68dc
ff29b47
2e01876
e50321b
ab741eb
8cf8f57
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
package io.sentry.graphql; | ||
adinauer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import graphql.ExecutionResult; | ||
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; | ||
import graphql.language.AstPrinter; | ||
import graphql.schema.DataFetchingEnvironment; | ||
import io.sentry.Hint; | ||
import io.sentry.IHub; | ||
import io.sentry.SentryEvent; | ||
import io.sentry.SentryLevel; | ||
import io.sentry.SentryOptions; | ||
import io.sentry.exception.ExceptionMechanismException; | ||
import io.sentry.protocol.Mechanism; | ||
import io.sentry.protocol.Request; | ||
import io.sentry.protocol.Response; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import org.jetbrains.annotations.ApiStatus; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
@ApiStatus.Internal | ||
public final class ExceptionReporter { | ||
adinauer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private final boolean captureRequestBodyForNonSubscriptions; | ||
|
||
public ExceptionReporter(final boolean captureRequestBodyForNonSubscriptions) { | ||
this.captureRequestBodyForNonSubscriptions = captureRequestBodyForNonSubscriptions; | ||
} | ||
|
||
private static final @NotNull String MECHANISM_TYPE = "GraphqlInstrumentation"; | ||
|
||
public void captureThrowable( | ||
final @NotNull Throwable throwable, | ||
final @NotNull ExceptionDetails exceptionDetails, | ||
final @Nullable ExecutionResult result) { | ||
final @NotNull IHub hub = exceptionDetails.getHub(); | ||
final @NotNull Mechanism mechanism = new Mechanism(); | ||
mechanism.setType(MECHANISM_TYPE); | ||
mechanism.setHandled(false); | ||
final @NotNull Throwable mechanismException = | ||
new ExceptionMechanismException(mechanism, throwable, Thread.currentThread()); | ||
final @NotNull SentryEvent event = new SentryEvent(mechanismException); | ||
event.setLevel(SentryLevel.FATAL); | ||
|
||
final @NotNull Hint hint = new Hint(); | ||
setRequestDetailsOnEvent(hub, exceptionDetails, event); | ||
|
||
if (result != null && isAllowedToAttachBody(hub)) { | ||
final @NotNull Response response = new Response(); | ||
final @NotNull Map<String, Object> responseBody = result.toSpecification(); | ||
response.setData(responseBody); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you set all the information besides the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean statuscode, headers and cookies? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can probably attach some of it for Spring in an event processor but since the response isn't finished yet it might very well be wrong / incomplete. Would you still like me to add it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add it after the request is made? This does not need to be GraphQL-specific since it makes sense for every integration. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't understand. Do you mean after the response is finished? We'd have to delay sending an event to Sentry, probably put the response stuff on the scope and pick it up from there once ready. Sounds brittle and complicated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, after the request is "finished". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we do this in a follow up PR? |
||
event.getContexts().setResponse(response); | ||
} | ||
|
||
hub.captureEvent(event, hint); | ||
} | ||
|
||
private boolean isAllowedToAttachBody(final @NotNull IHub hub) { | ||
final @NotNull SentryOptions options = hub.getOptions(); | ||
return options.isSendDefaultPii() | ||
&& !SentryOptions.RequestSize.NONE.equals(options.getMaxRequestBodySize()); | ||
} | ||
|
||
private void setRequestDetailsOnEvent( | ||
final @NotNull IHub hub, | ||
final @NotNull ExceptionDetails exceptionDetails, | ||
final @NotNull SentryEvent event) { | ||
hub.configureScope( | ||
(scope) -> { | ||
final @Nullable Request scopeRequest = scope.getRequest(); | ||
final @NotNull Request request = scopeRequest == null ? new Request() : scopeRequest; | ||
setDetailsOnRequest(hub, exceptionDetails, request); | ||
event.setRequest(request); | ||
}); | ||
} | ||
|
||
private void setDetailsOnRequest( | ||
final @NotNull IHub hub, | ||
final @NotNull ExceptionDetails exceptionDetails, | ||
final @NotNull Request request) { | ||
request.setApiTarget("graphql"); | ||
|
||
if (isAllowedToAttachBody(hub) | ||
&& (exceptionDetails.isSubscription() || captureRequestBodyForNonSubscriptions)) { | ||
final @NotNull Map<String, Object> data = new HashMap<>(); | ||
|
||
data.put("query", exceptionDetails.getQuery()); | ||
|
||
final @Nullable Map<String, Object> variables = exceptionDetails.getVariables(); | ||
if (variables != null && !variables.isEmpty()) { | ||
data.put("variables", variables); | ||
} | ||
|
||
// for Spring HTTP this will be replaced by RequestBodyExtractingEventProcessor | ||
// for non subscription (websocket) errors | ||
request.setData(data); | ||
} | ||
} | ||
|
||
public static final class ExceptionDetails { | ||
adinauer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private final @NotNull IHub hub; | ||
private final @Nullable InstrumentationExecutionParameters instrumentationExecutionParameters; | ||
private final @Nullable DataFetchingEnvironment dataFetchingEnvironment; | ||
|
||
private final boolean isSubscription; | ||
|
||
public ExceptionDetails( | ||
final @NotNull IHub hub, | ||
final @Nullable InstrumentationExecutionParameters instrumentationExecutionParameters, | ||
final boolean isSubscription) { | ||
this.hub = hub; | ||
this.instrumentationExecutionParameters = instrumentationExecutionParameters; | ||
dataFetchingEnvironment = null; | ||
this.isSubscription = isSubscription; | ||
} | ||
|
||
public ExceptionDetails( | ||
final @NotNull IHub hub, | ||
final @Nullable DataFetchingEnvironment dataFetchingEnvironment, | ||
final boolean isSubscription) { | ||
this.hub = hub; | ||
this.dataFetchingEnvironment = dataFetchingEnvironment; | ||
instrumentationExecutionParameters = null; | ||
this.isSubscription = isSubscription; | ||
} | ||
|
||
public @Nullable String getQuery() { | ||
if (instrumentationExecutionParameters != null) { | ||
return instrumentationExecutionParameters.getQuery(); | ||
} | ||
if (dataFetchingEnvironment != null) { | ||
return AstPrinter.printAst(dataFetchingEnvironment.getDocument()); | ||
} | ||
return null; | ||
} | ||
|
||
public @Nullable Map<String, Object> getVariables() { | ||
if (instrumentationExecutionParameters != null) { | ||
return instrumentationExecutionParameters.getVariables(); | ||
} | ||
if (dataFetchingEnvironment != null) { | ||
return dataFetchingEnvironment.getVariables(); | ||
} | ||
return null; | ||
} | ||
|
||
public boolean isSubscription() { | ||
return isSubscription; | ||
} | ||
|
||
public @NotNull IHub getHub() { | ||
return hub; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package io.sentry.graphql; | ||
|
||
import graphql.execution.MergedField; | ||
import graphql.schema.GraphQLNamedOutputType; | ||
import graphql.schema.GraphQLObjectType; | ||
import graphql.schema.GraphQLOutputType; | ||
import io.sentry.util.StringUtils; | ||
import org.jetbrains.annotations.ApiStatus; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
@ApiStatus.Internal | ||
public final class GraphqlStringUtils { | ||
adinauer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public static @Nullable String fieldToString(final @Nullable MergedField field) { | ||
if (field == null) { | ||
return null; | ||
} | ||
|
||
return field.getName(); | ||
} | ||
|
||
public static @Nullable String typeToString(final @Nullable GraphQLOutputType type) { | ||
if (type == null) { | ||
return null; | ||
} | ||
|
||
if (type instanceof GraphQLNamedOutputType) { | ||
final @NotNull GraphQLNamedOutputType namedType = (GraphQLNamedOutputType) type; | ||
return namedType.getName(); | ||
} | ||
|
||
return StringUtils.toString(type); | ||
} | ||
|
||
public static @Nullable String objectTypeToString(final @Nullable GraphQLObjectType type) { | ||
if (type == null) { | ||
return null; | ||
} | ||
|
||
return type.getName(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package io.sentry.graphql; | ||
|
||
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; | ||
import io.sentry.IHub; | ||
import org.jetbrains.annotations.NotNull; | ||
|
||
public final class NoOpSubscriptionHandler implements SentrySubscriptionHandler { | ||
adinauer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private static final @NotNull NoOpSubscriptionHandler instance = new NoOpSubscriptionHandler(); | ||
|
||
private NoOpSubscriptionHandler() {} | ||
|
||
public static @NotNull NoOpSubscriptionHandler getInstance() { | ||
return instance; | ||
} | ||
|
||
@Override | ||
public @NotNull Object onSubscriptionResult( | ||
@NotNull Object result, | ||
@NotNull IHub hub, | ||
@NotNull ExceptionReporter exceptionReporter, | ||
@NotNull InstrumentationFieldFetchParameters parameters) { | ||
return result; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.