sentry-go icon indicating copy to clipboard operation
sentry-go copied to clipboard

Add support for GraphQL

Open IGassmann opened this issue 2 years ago • 5 comments

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 avatar Nov 23 '23 10:11 IGassmann

@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)
})

karatekaneen avatar Dec 01 '23 11:12 karatekaneen

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 avatar Dec 22 '23 10:12 IGassmann

@IGassmann Nice! I will definitely change our implementation to something more like your solution

karatekaneen avatar Dec 27 '23 15:12 karatekaneen