Skip to content

Commit 2702851

Browse files
authored
Merge 3ca6c27 into fe10f05
2 parents fe10f05 + 3ca6c27 commit 2702851

File tree

72 files changed

+3397
-91
lines changed

Some content is hidden

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

72 files changed

+3397
-91
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
### Features
66

77
- Add TraceOrigin to Transactions and Spans ([#2803](https://github.com/getsentry/sentry-java/pull/2803))
8+
- Improve server side GraphQL support for spring-graphql and Nextflix DGS ([#2856](https://github.com/getsentry/sentry-java/pull/2856))
9+
- More exceptions and errors caught and reported to Sentry by also looking at the `ExecutionResult` (more specifically its `errors`)
10+
- More details for Sentry events: query, variables and response (where possible)
11+
- Breadcrumbs for operation (query, mutation, subscription), data fetchers and data loaders (Spring only)
12+
- Better hub propagation by using `GraphQLContext`
813

914
### Fixes
1015

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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,66 @@ 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/SentryGraphqlExceptionHandler {
39+
public fun <init> (Lgraphql/execution/DataFetcherExceptionHandler;)V
40+
public fun onException (Ljava/lang/Throwable;Lgraphql/schema/DataFetchingEnvironment;Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult;
41+
}
42+
1243
public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/instrumentation/SimpleInstrumentation {
44+
public static final field SENTRY_EXCEPTIONS_CONTEXT_KEY Ljava/lang/String;
45+
public static final field SENTRY_HUB_CONTEXT_KEY Ljava/lang/String;
1346
public fun <init> ()V
1447
public fun <init> (Lio/sentry/IHub;)V
1548
public fun <init> (Lio/sentry/IHub;Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V
1649
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V
50+
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;)V
51+
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Z)V
52+
public fun <init> (Lio/sentry/graphql/SentrySubscriptionHandler;Z)V
53+
public fun beginExecuteOperation (Lgraphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters;)Lgraphql/execution/instrumentation/InstrumentationContext;
1754
public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Lgraphql/execution/instrumentation/InstrumentationContext;
55+
public fun beginSubscribedFieldEvent (Lgraphql/execution/instrumentation/parameters/InstrumentationFieldParameters;)Lgraphql/execution/instrumentation/InstrumentationContext;
1856
public fun createState ()Lgraphql/execution/instrumentation/InstrumentationState;
1957
public fun instrumentDataFetcher (Lgraphql/schema/DataFetcher;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Lgraphql/schema/DataFetcher;
58+
public fun instrumentExecutionResult (Lgraphql/ExecutionResult;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Ljava/util/concurrent/CompletableFuture;
2059
}
2160

2261
public abstract interface class io/sentry/graphql/SentryInstrumentation$BeforeSpanCallback {
2362
public abstract fun execute (Lio/sentry/ISpan;Lgraphql/schema/DataFetchingEnvironment;Ljava/lang/Object;)Lio/sentry/ISpan;
2463
}
2564

65+
public abstract interface class io/sentry/graphql/SentrySubscriptionHandler {
66+
public abstract fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IHub;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object;
67+
}
68+

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

3-
import static io.sentry.TypeCheckHint.GRAPHQL_HANDLER_PARAMETERS;
4-
53
import graphql.execution.DataFetcherExceptionHandler;
64
import graphql.execution.DataFetcherExceptionHandlerParameters;
75
import graphql.execution.DataFetcherExceptionHandlerResult;
8-
import io.sentry.Hint;
9-
import io.sentry.HubAdapter;
106
import io.sentry.IHub;
11-
import io.sentry.util.Objects;
127
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
139

1410
/**
1511
* Captures exceptions that occur during data fetching, passes them to Sentry and invokes a delegate
1612
* exception handler.
1713
*/
1814
public final class SentryDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
19-
private final @NotNull IHub hub;
20-
private final @NotNull DataFetcherExceptionHandler delegate;
15+
private final @NotNull SentryGraphqlExceptionHandler handler;
2116

2217
public SentryDataFetcherExceptionHandler(
23-
final @NotNull IHub hub, final @NotNull DataFetcherExceptionHandler delegate) {
24-
this.hub = Objects.requireNonNull(hub, "hub is required");
25-
this.delegate = Objects.requireNonNull(delegate, "delegate is required");
18+
final @Nullable IHub hub, final @NotNull DataFetcherExceptionHandler delegate) {
19+
this.handler = new SentryGraphqlExceptionHandler(delegate);
2620
}
2721

2822
public SentryDataFetcherExceptionHandler(final @NotNull DataFetcherExceptionHandler delegate) {
29-
this(HubAdapter.getInstance(), delegate);
23+
this(null, delegate);
3024
}
3125

3226
@Override
3327
@SuppressWarnings("deprecation")
34-
public DataFetcherExceptionHandlerResult onException(
28+
public @Nullable DataFetcherExceptionHandlerResult onException(
3529
final @NotNull DataFetcherExceptionHandlerParameters handlerParameters) {
36-
final Hint hint = new Hint();
37-
hint.set(GRAPHQL_HANDLER_PARAMETERS, handlerParameters);
38-
39-
hub.captureException(handlerParameters.getException(), hint);
40-
return delegate.onException(handlerParameters);
30+
return handler.onException(
31+
handlerParameters.getException(),
32+
handlerParameters.getDataFetchingEnvironment(),
33+
handlerParameters);
4134
}
4235
}

0 commit comments

Comments
 (0)