Skip to content
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

Add support for GraphQL #749

Open
IGassmann opened this issue Nov 23, 2023 · 3 comments
Open

Add support for GraphQL #749

IGassmann opened this issue Nov 23, 2023 · 3 comments

Comments

@IGassmann
Copy link

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.

@karatekaneen
Copy link
Contributor

karatekaneen commented Dec 1, 2023

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

@IGassmann
Copy link
Author

IGassmann commented Dec 22, 2023

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 :)

@karatekaneen
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

No branches or pull requests

3 participants