GraphQL Integration

Learn more about the Sentry GraphQL (sentry_link) integration for the Flutter SDK.

The sentry_link integration adds Sentry instrumentation to GraphQL clients built on the gql ecosystem.

It helps you capture:

  • Link exceptions (transport/client failures such as network and parsing issues)
  • GraphQL response errors (entries in response.errors)
  • Tracing spans for queries, mutations, and subscriptions
  • Breadcrumbs for successful GraphQL operations (operation name and duration)
  • Request context (operation name, query, variables, and response data) attached to captured events

sentry_link works with the gql ecosystem and is commonly used with:

Other clients built on gql packages generally work too.

Add sentry_link and your GraphQL client dependency:

pubspec.yaml
Copied
dependencies:
  sentry: ^9.24.0
  sentry_link: ^9.24.0
  graphql: ^5.1.3

After you initialize Sentry in your app, add SentryGql.link() to your GraphQL client:

Copied
import 'package:graphql/client.dart';
import 'package:sentry/sentry.dart';
import 'package:sentry_link/sentry_link.dart';

final link = Link.from([
  SentryGql.link(
    shouldStartTransaction: false,
    graphQlErrorsMarkTransactionAsFailed: false,
  ),
  // Add any middleware links (for example, AuthLink) here.
  HttpLink(
    'https://your-graphql-endpoint.com/graphql',
    httpClient: SentryHttpClient(),
    serializer: SentryRequestSerializer(),
    parser: SentryResponseParser(),
  ),
]);

final client = GraphQLClient(
  cache: GraphQLCache(),
  link: link,
);

For better error context and grouping, add the recommended init options from Advanced Configuration.

ParameterTypeDefaultDescription
shouldStartTransactionboolrequiredSet to true to start a transaction per GraphQL operation when no active span/transaction exists.
graphQlErrorsMarkTransactionAsFailedboolrequiredSet to true to mark GraphQL spans/transactions as unknownError when response.errors exists.
enableBreadcrumbsbooltrueRecords breadcrumbs for successful GraphQL operations.
reportExceptionsbooltrueCaptures LinkException failures as Sentry events.
reportExceptionsAsBreadcrumbsboolfalseRecords LinkException failures as breadcrumbs instead of events.
reportGraphQlErrorsbooltrueCaptures GraphQL response errors as Sentry events.
reportGraphQlErrorsAsBreadcrumbsboolfalseRecords GraphQL response errors as breadcrumbs instead of events.

sentry_link reports two different failure layers:

  1. Link exceptions (reportExceptions*): transport/client-side failures from the link chain, such as ServerException, NetworkException, and parser/serialization failures.
  2. GraphQL response errors (reportGraphQlErrors*): application-layer errors returned in response.errors (for example, resolver, validation, or authorization errors).

When you want GraphQL transactions and spans, enable tracing in your SDK initialization and start transactions in SentryGql.link():

Copied
import 'package:graphql/client.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_link/sentry_link.dart';

await SentryFlutter.init((options) {
  options.dsn = '___PUBLIC_DSN___';
  options.tracesSampleRate = 1.0;
});

final link = Link.from([
  SentryGql.link(
    shouldStartTransaction: true,
    graphQlErrorsMarkTransactionAsFailed: true,
  ),
  HttpLink(
    'https://your-graphql-endpoint.com/graphql',
    httpClient: SentryHttpClient(),
    serializer: SentryRequestSerializer(),
    parser: SentryResponseParser(),
  ),
]);

final client = GraphQLClient(cache: GraphQLCache(), link: link);

Tracing behavior:

  • Span descriptions follow GraphQL: "{operationName}" {type}, for example GraphQL: "LoadPosts" query.
  • Span operations are http.graphql.query, http.graphql.mutation, and http.graphql.subscription.
  • If no active transaction exists and shouldStartTransaction is true, the SDK creates one automatically.
  • Span status is set as follows:
    • Success → SpanStatus.ok()
    • response.errors present and graphQlErrorsMarkTransactionAsFailed is trueSpanStatus.unknownError()
    • response.errors present and graphQlErrorsMarkTransactionAsFailed is falseSpanStatus.ok()
    • A thrown LinkExceptionSpanStatus.unknownError() (regardless of flag values)
  • SentryRequestSerializer and SentryResponseParser add child spans with operation serialize.http.client for request serialization and response parsing.

For better context and issue grouping, we recommend this initialization setup:

Copied
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_link/sentry_link.dart';

await SentryFlutter.init((options) {
  // ... your existing Sentry options

  // Filter duplicate HTTP breadcrumbs for GraphQL requests.
  options.beforeBreadcrumb = graphQlFilter();

  // Preserve nested LinkException causes for better error context.
  options.addGqlExtractors();

  // Improve stack trace grouping by excluding sentry_link internals.
  options.addSentryLinkInAppExcludes();
});

If you use Dio, replace HttpLink with DioLink and use sentry_dio:

Copied
import 'package:dio/dio.dart';
import 'package:gql_link/gql_link.dart';
import 'package:sentry_dio/sentry_dio.dart';
import 'package:sentry_link/sentry_link.dart';
import 'package:gql_dio_link/gql_dio_link.dart';

final link = Link.from([
  SentryGql.link(
    shouldStartTransaction: true,
    graphQlErrorsMarkTransactionAsFailed: true,
  ),
  DioLink(
    'https://your-graphql-endpoint.com/graphql',
    client: Dio()..addSentry(),
    serializer: SentryRequestSerializer(),
    parser: SentryResponseParser(),
  ),
]);

You can either disable HTTP breadcrumbs globally or filter only GraphQL HTTP duplicates. For targeted filtering, set beforeBreadcrumb with your own graphQlFilter() callback:

Copied
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_link/sentry_link.dart';

await SentryFlutter.init((options) {
  // ... your existing Sentry options
  options.beforeBreadcrumb = graphQlFilter((breadcrumb, hint) {
    // Add your custom filtering or mutation logic here.
    return breadcrumb;
  });
});

LinkException instances can contain nested causes. Add GraphQL extractors in your SDK initialization options callback so Sentry preserves that exception chain:

Copied
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_link/sentry_link.dart';

await SentryFlutter.init((options) {
  // ... your existing Sentry options
  options.addGqlExtractors();
});

To keep sentry_link internals out of in-app stack frames, add:

Copied
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_link/sentry_link.dart';

await SentryFlutter.init((options) {
  // ... your existing Sentry options
  options.addSentryLinkInAppExcludes();
});
Advanced HttpLink response-decoder tracing (optional)

This pattern adds a custom span around HttpLink response decoding. Use it when you need deeper visibility into JSON decoding time.

Copied
import 'dart:async';
import 'dart:convert';

import 'package:graphql/client.dart';
import 'package:http/http.dart' as http;
import 'package:sentry/sentry.dart';
import 'package:sentry_link/sentry_link.dart';

final link = Link.from([
  SentryGql.link(
    shouldStartTransaction: true,
    graphQlErrorsMarkTransactionAsFailed: true,
  ),
  HttpLink(
    'https://your-graphql-endpoint.com/graphql',
    httpClient: SentryHttpClient(),
    serializer: SentryRequestSerializer(),
    parser: SentryResponseParser(),
    httpResponseDecoder: sentryResponseDecoder,
  ),
]);

Map<String, dynamic>? sentryResponseDecoder(
  http.Response response, {
  Hub? hub,
}) {
  final currentHub = hub ?? HubAdapter();
  final span = currentHub.getSpan()?.startChild(
        'serialize.http.client',
        description: 'http response deserialization',
      );
  Map<String, dynamic>? result;
  try {
    result = _defaultHttpResponseDecoder(response);
    span?.status = const SpanStatus.ok();
  } catch (error) {
    span?.status = const SpanStatus.unknownError();
    span?.throwable = error;
    rethrow;
  } finally {
    unawaited(span?.finish());
  }
  return result;
}

Map<String, dynamic>? _defaultHttpResponseDecoder(http.Response response) {
  return json.decode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>?;
}
Additional extractors for graphql package exceptions (optional)

If you use graphql and want even deeper exception-cause chains, you can add custom extractors:

Copied
import 'package:graphql/graphql.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_link/sentry_link.dart';

await SentryFlutter.init((options) {
  // ... your existing Sentry options
  options.addExceptionCauseExtractor(UnknownExceptionExtractor());
  options.addExceptionCauseExtractor(NetworkExceptionExtractor());
  options.addExceptionCauseExtractor(CacheMissExceptionExtractor());
  options.addExceptionCauseExtractor(OperationExceptionExtractor());
  options.addExceptionCauseExtractor(CacheMisconfigurationExceptionExtractor());
  options.addExceptionCauseExtractor(MismatchedDataStructureExceptionExtractor());
  options.addExceptionCauseExtractor(UnexpectedResponseStructureExceptionExtractor());
});

class UnknownExceptionExtractor extends LinkExceptionExtractor<UnknownException> {}

class NetworkExceptionExtractor extends LinkExceptionExtractor<NetworkException> {}

class CacheMissExceptionExtractor extends LinkExceptionExtractor<CacheMissException> {}

class CacheMisconfigurationExceptionExtractor
    extends LinkExceptionExtractor<CacheMisconfigurationException> {}

class MismatchedDataStructureExceptionExtractor
    extends LinkExceptionExtractor<MismatchedDataStructureException> {}

class UnexpectedResponseStructureExceptionExtractor
    extends LinkExceptionExtractor<UnexpectedResponseStructureException> {}

class OperationExceptionExtractor extends ExceptionCauseExtractor<OperationException> {
  
  ExceptionCause? cause(OperationException error) {
    return ExceptionCause(error.linkException, error.originalStackTrace);
  }
}

Run a query with an intentional schema mistake (for example, misspelling a field):

Copied
import 'package:sentry/sentry.dart';
import 'package:sentry_link/sentry_link.dart';
import 'package:graphql/client.dart';

final link = Link.from([
  SentryGql.link(
    shouldStartTransaction: false,
    graphQlErrorsMarkTransactionAsFailed: false,
  ),
  HttpLink(
    'https://your-graphql-endpoint.com/graphql',
    httpClient: SentryHttpClient(),
    serializer: SentryRequestSerializer(),
    parser: SentryResponseParser(),
  ),
]);

final client = GraphQLClient(cache: GraphQLCache(), link: link);

final result = await client.query(
  QueryOptions(
    operationName: 'LoadPosts',
    document: gql(r'''
      query LoadPosts($id: ID!) {
        post(id: $id) {
          id
          titl
          body
        }
      }
    '''),
    variables: {'id': 50},
  ),
);

Open your project in sentry.io:

  • In Issues, confirm a GraphQL event includes operation name, query, variables, and response details.

If you enabled tracing in Enable Tracing, also verify:

  • In Performance, confirm a GraphQL: "LoadPosts" query transaction/span with GraphQL and serialization/parsing timing data.
Was this helpful?
Help improve this content
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").