Add support for GraphQL
Summary
Sentry recently released improved GraphQL support. However, there isn't yet support for Go's GraphQL libraries.
It would be great to see support for Go's most popular GraphQL libraries, such as gqlgen, in the same way support was added for other languages like Python's Strawberry integration.
Motivation
This would provide a better error-reporting experience for GraphQL APIs written in Go.
@IGassmann If you are using gqlgen today and want to get started quickly while this is being implemented you can use this package that I found yesterday and add a small error handler. Solved almost all of my needs.
gqlHandler.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub()
}
hub.CaptureException(err)
return graphql.DefaultErrorPresenter(ctx, err)
})
For those interested, here is how I instrumented gqlgen with Sentry:
gqlHandler.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
oc := graphql.GetOperationContext(ctx)
operationType := string(oc.Operation.Operation)
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub().Clone()
ctx = sentry.SetHubOnContext(ctx, hub)
}
// See https://docs.sentry.io/platforms/go/guides/http/enriching-events/scopes/
hub.ConfigureScope(func(scope *sentry.Scope) {
scope.SetContext("graphql", map[string]interface{}{
"document": oc.RawQuery,
"variables": oc.Variables,
})
scope.SetTag("graphql.operation.name", oc.OperationName)
scope.SetTag("graphql.operation.type", operationType)
user, err := authn.User(ctx)
if err == nil && user != nil {
scope.SetUser(sentry.User{
ID: user.ID.String(),
Email: user.Email,
})
}
})
// See https://docs.sentry.io/platforms/go/guides/http/performance/instrumentation/custom-instrumentation/
span := sentry.StartSpan(ctx, fmt.Sprintf("graphql.%s", operationType))
span.Description = fmt.Sprintf("%s %s", operationType, oc.OperationName)
// Before the operation
handler := next(ctx)
return func(ctx context.Context) *graphql.Response {
response := handler(ctx)
// After the operation
span.Finish()
return response
}
})
gqlHandler.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
fc := graphql.GetFieldContext(ctx)
// Skip fields that don't have a resolver.
if !fc.IsResolver {
return next(ctx)
}
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub().Clone()
ctx = sentry.SetHubOnContext(ctx, hub)
}
// See https://docs.sentry.io/platforms/go/guides/http/performance/instrumentation/custom-instrumentation/
fieldPath := fmt.Sprintf("%s.%s", fc.Object, fc.Field.Name)
span := sentry.StartSpan(ctx, "graphql.resolve")
span.Description = fmt.Sprintf("resolving %s", fieldPath)
span.SetData("graphql.field_name", fc.Field.Name)
span.SetData("graphql.field_path", fieldPath)
span.SetData("graphql.path", fc.Path().String())
res, err = next(ctx)
span.Finish()
return res, err
})
gqlHandler.SetRecoverFunc(func(ctx context.Context, err interface{}) error {
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub().Clone()
}
hub.RecoverWithContext(ctx, err)
// Return a generic error to the user
return &gqlerror.Error{
Path: graphql.GetPath(ctx),
Message: "Internal system error.",
Extensions: map[string]interface{}{
"code": "INTERNAL_SYSTEM_ERROR",
},
}
})
gqlHandler.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {
var gqlErr *gqlerror.Error
if errors.As(err, &gqlErr) {
if errCode, ok := gqlErr.Extensions["code"].(string); ok {
// We don't want to log GraphQL built-in validation and parsing errors
if errCode == errcode.ValidationFailed || errCode == errcode.ParseFailed {
return gqlErr
}
// We already log internal system errors in the recover function
if errCode == "INTERNAL_SYSTEM_ERROR" {
return gqlErr
}
}
}
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub().Clone()
ctx = sentry.SetHubOnContext(ctx, hub)
}
hub.CaptureException(err)
return graphql.DefaultErrorPresenter(ctx, err)
})
I inspired myself by Sentry's Strawberry integration, but I, unfortunately, wasn't able to get syntax highlighting working like on the Sentry blog post.
Any improvement suggestions are welcomed :)
@IGassmann Nice! I will definitely change our implementation to something more like your solution