Skip to content

Commit fb296f0

Browse files
authored
Improve server side GraphQL support for spring-graphql and Nextflix DGS (#2856)
1 parent f86fb22 commit fb296f0

File tree

77 files changed

+3835
-71
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+3835
-71
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
- Add HTTP response code to Spring WebFlux transactions ([#2870](https://github.com/getsentry/sentry-java/pull/2870))
88
- Add `sampled` to Dynamic Sampling Context ([#2869](https://github.com/getsentry/sentry-java/pull/2869))
9+
- Improve server side GraphQL support for spring-graphql and Nextflix DGS ([#2856](https://github.com/getsentry/sentry-java/pull/2856))
10+
- If you have already been using `SentryDataFetcherExceptionHandler` that still works but has been deprecated. Please use `SentryGenericDataFetcherExceptionHandler` combined with `SentryInstrumentation` instead for better error reporting.
11+
- More exceptions and errors caught and reported to Sentry by also looking at the `ExecutionResult` (more specifically its `errors`)
12+
- More details for Sentry events: query, variables and response (where possible)
13+
- Breadcrumbs for operation (query, mutation, subscription), data fetchers and data loaders (Spring only)
14+
- Better hub propagation by using `GraphQLContext`
915

1016
### Fixes
1117

buildSrc/src/main/java/Config.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,20 @@ object Config {
7373
val jacksonDatabind = "com.fasterxml.jackson.core:jackson-databind"
7474

7575
val springBootStarter = "org.springframework.boot:spring-boot-starter:$springBootVersion"
76+
val springBootStarterGraphql = "org.springframework.boot:spring-boot-starter-graphql:$springBootVersion"
7677
val springBootStarterTest = "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
7778
val springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
79+
val springBootStarterWebsocket = "org.springframework.boot:spring-boot-starter-websocket:$springBootVersion"
7880
val springBootStarterWebflux = "org.springframework.boot:spring-boot-starter-webflux:$springBootVersion"
7981
val springBootStarterAop = "org.springframework.boot:spring-boot-starter-aop:$springBootVersion"
8082
val springBootStarterSecurity = "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
8183
val springBootStarterJdbc = "org.springframework.boot:spring-boot-starter-jdbc:$springBootVersion"
8284

8385
val springBoot3Starter = "org.springframework.boot:spring-boot-starter:$springBoot3Version"
86+
val springBoot3StarterGraphql = "org.springframework.boot:spring-boot-starter-graphql:$springBoot3Version"
8487
val springBoot3StarterTest = "org.springframework.boot:spring-boot-starter-test:$springBoot3Version"
8588
val springBoot3StarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBoot3Version"
89+
val springBoot3StarterWebsocket = "org.springframework.boot:spring-boot-starter-websocket:$springBoot3Version"
8690
val springBoot3StarterWebflux = "org.springframework.boot:spring-boot-starter-webflux:$springBoot3Version"
8791
val springBoot3StarterAop = "org.springframework.boot:spring-boot-starter-aop:$springBoot3Version"
8892
val springBoot3StarterSecurity = "org.springframework.boot:spring-boot-starter-security:$springBoot3Version"

sentry-graphql/api/sentry-graphql.api

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,71 @@ public final class io/sentry/graphql/BuildConfig {
33
public static final field VERSION_NAME Ljava/lang/String;
44
}
55

6+
public final class io/sentry/graphql/ExceptionReporter {
7+
public fun <init> (Z)V
8+
public fun captureThrowable (Ljava/lang/Throwable;Lio/sentry/graphql/ExceptionReporter$ExceptionDetails;Lgraphql/ExecutionResult;)V
9+
}
10+
11+
public final class io/sentry/graphql/ExceptionReporter$ExceptionDetails {
12+
public fun <init> (Lio/sentry/IHub;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Z)V
13+
public fun <init> (Lio/sentry/IHub;Lgraphql/schema/DataFetchingEnvironment;Z)V
14+
public fun getHub ()Lio/sentry/IHub;
15+
public fun getQuery ()Ljava/lang/String;
16+
public fun getVariables ()Ljava/util/Map;
17+
public fun isSubscription ()Z
18+
}
19+
20+
public final class io/sentry/graphql/GraphqlStringUtils {
21+
public fun <init> ()V
22+
public static fun fieldToString (Lgraphql/execution/MergedField;)Ljava/lang/String;
23+
public static fun objectTypeToString (Lgraphql/schema/GraphQLObjectType;)Ljava/lang/String;
24+
public static fun typeToString (Lgraphql/schema/GraphQLOutputType;)Ljava/lang/String;
25+
}
26+
27+
public final class io/sentry/graphql/NoOpSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler {
28+
public static fun getInstance ()Lio/sentry/graphql/NoOpSubscriptionHandler;
29+
public fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IHub;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object;
30+
}
31+
632
public final class io/sentry/graphql/SentryDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler {
733
public fun <init> (Lgraphql/execution/DataFetcherExceptionHandler;)V
834
public fun <init> (Lio/sentry/IHub;Lgraphql/execution/DataFetcherExceptionHandler;)V
935
public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult;
1036
}
1137

38+
public final class io/sentry/graphql/SentryGenericDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler {
39+
public fun <init> (Lgraphql/execution/DataFetcherExceptionHandler;)V
40+
public fun <init> (Lio/sentry/IHub;Lgraphql/execution/DataFetcherExceptionHandler;)V
41+
public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult;
42+
}
43+
44+
public final class io/sentry/graphql/SentryGraphqlExceptionHandler {
45+
public fun <init> (Lgraphql/execution/DataFetcherExceptionHandler;)V
46+
public fun onException (Ljava/lang/Throwable;Lgraphql/schema/DataFetchingEnvironment;Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult;
47+
}
48+
1249
public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/instrumentation/SimpleInstrumentation {
50+
public static final field SENTRY_EXCEPTIONS_CONTEXT_KEY Ljava/lang/String;
51+
public static final field SENTRY_HUB_CONTEXT_KEY Ljava/lang/String;
1352
public fun <init> ()V
1453
public fun <init> (Lio/sentry/IHub;)V
1554
public fun <init> (Lio/sentry/IHub;Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V
1655
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V
56+
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;)V
57+
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Z)V
58+
public fun <init> (Lio/sentry/graphql/SentrySubscriptionHandler;Z)V
59+
public fun beginExecuteOperation (Lgraphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters;)Lgraphql/execution/instrumentation/InstrumentationContext;
1760
public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Lgraphql/execution/instrumentation/InstrumentationContext;
1861
public fun createState ()Lgraphql/execution/instrumentation/InstrumentationState;
1962
public fun instrumentDataFetcher (Lgraphql/schema/DataFetcher;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Lgraphql/schema/DataFetcher;
63+
public fun instrumentExecutionResult (Lgraphql/ExecutionResult;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Ljava/util/concurrent/CompletableFuture;
2064
}
2165

2266
public abstract interface class io/sentry/graphql/SentryInstrumentation$BeforeSpanCallback {
2367
public abstract fun execute (Lio/sentry/ISpan;Lgraphql/schema/DataFetchingEnvironment;Ljava/lang/Object;)Lio/sentry/ISpan;
2468
}
2569

70+
public abstract interface class io/sentry/graphql/SentrySubscriptionHandler {
71+
public abstract fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IHub;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object;
72+
}
73+

sentry-graphql/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ dependencies {
3636
testImplementation(kotlin(Config.kotlinStdLib))
3737
testImplementation(Config.TestLibs.kotlinTestJunit)
3838
testImplementation(Config.TestLibs.mockitoKotlin)
39+
testImplementation(Config.TestLibs.mockitoInline)
3940
testImplementation(Config.TestLibs.mockWebserver)
4041
testImplementation(Config.Libs.okhttp)
42+
testImplementation(Config.Libs.springBootStarterGraphql)
43+
testImplementation("com.netflix.graphql.dgs:graphql-error-types:4.9.2")
4144
testImplementation(Config.Libs.graphQlJava)
4245
}
4346

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package io.sentry.graphql;
2+
3+
import graphql.ExecutionResult;
4+
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
5+
import graphql.language.AstPrinter;
6+
import graphql.schema.DataFetchingEnvironment;
7+
import io.sentry.Hint;
8+
import io.sentry.IHub;
9+
import io.sentry.SentryEvent;
10+
import io.sentry.SentryLevel;
11+
import io.sentry.SentryOptions;
12+
import io.sentry.exception.ExceptionMechanismException;
13+
import io.sentry.protocol.Mechanism;
14+
import io.sentry.protocol.Request;
15+
import io.sentry.protocol.Response;
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
import org.jetbrains.annotations.ApiStatus;
19+
import org.jetbrains.annotations.NotNull;
20+
import org.jetbrains.annotations.Nullable;
21+
22+
@ApiStatus.Internal
23+
public final class ExceptionReporter {
24+
private final boolean captureRequestBodyForNonSubscriptions;
25+
26+
public ExceptionReporter(final boolean captureRequestBodyForNonSubscriptions) {
27+
this.captureRequestBodyForNonSubscriptions = captureRequestBodyForNonSubscriptions;
28+
}
29+
30+
private static final @NotNull String MECHANISM_TYPE = "GraphqlInstrumentation";
31+
32+
public void captureThrowable(
33+
final @NotNull Throwable throwable,
34+
final @NotNull ExceptionDetails exceptionDetails,
35+
final @Nullable ExecutionResult result) {
36+
final @NotNull IHub hub = exceptionDetails.getHub();
37+
final @NotNull Mechanism mechanism = new Mechanism();
38+
mechanism.setType(MECHANISM_TYPE);
39+
mechanism.setHandled(false);
40+
final @NotNull Throwable mechanismException =
41+
new ExceptionMechanismException(mechanism, throwable, Thread.currentThread());
42+
final @NotNull SentryEvent event = new SentryEvent(mechanismException);
43+
event.setLevel(SentryLevel.FATAL);
44+
45+
final @NotNull Hint hint = new Hint();
46+
setRequestDetailsOnEvent(hub, exceptionDetails, event);
47+
48+
if (result != null && isAllowedToAttachBody(hub)) {
49+
final @NotNull Response response = new Response();
50+
final @NotNull Map<String, Object> responseBody = result.toSpecification();
51+
response.setData(responseBody);
52+
event.getContexts().setResponse(response);
53+
}
54+
55+
hub.captureEvent(event, hint);
56+
}
57+
58+
private boolean isAllowedToAttachBody(final @NotNull IHub hub) {
59+
final @NotNull SentryOptions options = hub.getOptions();
60+
return options.isSendDefaultPii()
61+
&& !SentryOptions.RequestSize.NONE.equals(options.getMaxRequestBodySize());
62+
}
63+
64+
private void setRequestDetailsOnEvent(
65+
final @NotNull IHub hub,
66+
final @NotNull ExceptionDetails exceptionDetails,
67+
final @NotNull SentryEvent event) {
68+
hub.configureScope(
69+
(scope) -> {
70+
final @Nullable Request scopeRequest = scope.getRequest();
71+
final @NotNull Request request = scopeRequest == null ? new Request() : scopeRequest;
72+
setDetailsOnRequest(hub, exceptionDetails, request);
73+
event.setRequest(request);
74+
});
75+
}
76+
77+
private void setDetailsOnRequest(
78+
final @NotNull IHub hub,
79+
final @NotNull ExceptionDetails exceptionDetails,
80+
final @NotNull Request request) {
81+
request.setApiTarget("graphql");
82+
83+
if (isAllowedToAttachBody(hub)
84+
&& (exceptionDetails.isSubscription() || captureRequestBodyForNonSubscriptions)) {
85+
final @NotNull Map<String, Object> data = new HashMap<>();
86+
87+
data.put("query", exceptionDetails.getQuery());
88+
89+
final @Nullable Map<String, Object> variables = exceptionDetails.getVariables();
90+
if (variables != null && !variables.isEmpty()) {
91+
data.put("variables", variables);
92+
}
93+
94+
// for Spring HTTP this will be replaced by RequestBodyExtractingEventProcessor
95+
// for non subscription (websocket) errors
96+
request.setData(data);
97+
}
98+
}
99+
100+
public static final class ExceptionDetails {
101+
102+
private final @NotNull IHub hub;
103+
private final @Nullable InstrumentationExecutionParameters instrumentationExecutionParameters;
104+
private final @Nullable DataFetchingEnvironment dataFetchingEnvironment;
105+
106+
private final boolean isSubscription;
107+
108+
public ExceptionDetails(
109+
final @NotNull IHub hub,
110+
final @Nullable InstrumentationExecutionParameters instrumentationExecutionParameters,
111+
final boolean isSubscription) {
112+
this.hub = hub;
113+
this.instrumentationExecutionParameters = instrumentationExecutionParameters;
114+
dataFetchingEnvironment = null;
115+
this.isSubscription = isSubscription;
116+
}
117+
118+
public ExceptionDetails(
119+
final @NotNull IHub hub,
120+
final @Nullable DataFetchingEnvironment dataFetchingEnvironment,
121+
final boolean isSubscription) {
122+
this.hub = hub;
123+
this.dataFetchingEnvironment = dataFetchingEnvironment;
124+
instrumentationExecutionParameters = null;
125+
this.isSubscription = isSubscription;
126+
}
127+
128+
public @Nullable String getQuery() {
129+
if (instrumentationExecutionParameters != null) {
130+
return instrumentationExecutionParameters.getQuery();
131+
}
132+
if (dataFetchingEnvironment != null) {
133+
return AstPrinter.printAst(dataFetchingEnvironment.getDocument());
134+
}
135+
return null;
136+
}
137+
138+
public @Nullable Map<String, Object> getVariables() {
139+
if (instrumentationExecutionParameters != null) {
140+
return instrumentationExecutionParameters.getVariables();
141+
}
142+
if (dataFetchingEnvironment != null) {
143+
return dataFetchingEnvironment.getVariables();
144+
}
145+
return null;
146+
}
147+
148+
public boolean isSubscription() {
149+
return isSubscription;
150+
}
151+
152+
public @NotNull IHub getHub() {
153+
return hub;
154+
}
155+
}
156+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.sentry.graphql;
2+
3+
import graphql.execution.MergedField;
4+
import graphql.schema.GraphQLNamedOutputType;
5+
import graphql.schema.GraphQLObjectType;
6+
import graphql.schema.GraphQLOutputType;
7+
import io.sentry.util.StringUtils;
8+
import org.jetbrains.annotations.ApiStatus;
9+
import org.jetbrains.annotations.NotNull;
10+
import org.jetbrains.annotations.Nullable;
11+
12+
@ApiStatus.Internal
13+
public final class GraphqlStringUtils {
14+
15+
public static @Nullable String fieldToString(final @Nullable MergedField field) {
16+
if (field == null) {
17+
return null;
18+
}
19+
20+
return field.getName();
21+
}
22+
23+
public static @Nullable String typeToString(final @Nullable GraphQLOutputType type) {
24+
if (type == null) {
25+
return null;
26+
}
27+
28+
if (type instanceof GraphQLNamedOutputType) {
29+
final @NotNull GraphQLNamedOutputType namedType = (GraphQLNamedOutputType) type;
30+
return namedType.getName();
31+
}
32+
33+
return StringUtils.toString(type);
34+
}
35+
36+
public static @Nullable String objectTypeToString(final @Nullable GraphQLObjectType type) {
37+
if (type == null) {
38+
return null;
39+
}
40+
41+
return type.getName();
42+
}
43+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.sentry.graphql;
2+
3+
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
4+
import io.sentry.IHub;
5+
import org.jetbrains.annotations.NotNull;
6+
7+
public final class NoOpSubscriptionHandler implements SentrySubscriptionHandler {
8+
9+
private static final @NotNull NoOpSubscriptionHandler instance = new NoOpSubscriptionHandler();
10+
11+
private NoOpSubscriptionHandler() {}
12+
13+
public static @NotNull NoOpSubscriptionHandler getInstance() {
14+
return instance;
15+
}
16+
17+
@Override
18+
public @NotNull Object onSubscriptionResult(
19+
@NotNull Object result,
20+
@NotNull IHub hub,
21+
@NotNull ExceptionReporter exceptionReporter,
22+
@NotNull InstrumentationFieldFetchParameters parameters) {
23+
return result;
24+
}
25+
}

sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@
88
import io.sentry.Hint;
99
import io.sentry.HubAdapter;
1010
import io.sentry.IHub;
11+
import io.sentry.SentryIntegrationPackageStorage;
1112
import io.sentry.util.Objects;
1213
import org.jetbrains.annotations.NotNull;
1314

1415
/**
1516
* Captures exceptions that occur during data fetching, passes them to Sentry and invokes a delegate
1617
* exception handler.
18+
*
19+
* @deprecated please use {@link SentryGenericDataFetcherExceptionHandler} in combination with
20+
* {@link SentryInstrumentation} instead for better error reporting.
1721
*/
22+
@Deprecated
1823
public final class SentryDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
1924
private final @NotNull IHub hub;
2025
private final @NotNull DataFetcherExceptionHandler delegate;
@@ -23,6 +28,7 @@ public SentryDataFetcherExceptionHandler(
2328
final @NotNull IHub hub, final @NotNull DataFetcherExceptionHandler delegate) {
2429
this.hub = Objects.requireNonNull(hub, "hub is required");
2530
this.delegate = Objects.requireNonNull(delegate, "delegate is required");
31+
SentryIntegrationPackageStorage.getInstance().addIntegration("GrahQLLegacyExceptionHandler");
2632
}
2733

2834
public SentryDataFetcherExceptionHandler(final @NotNull DataFetcherExceptionHandler delegate) {

0 commit comments

Comments
 (0)