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

Support for DIO HTTP library #673

Closed
ueman opened this issue Dec 20, 2021 · 12 comments · Fixed by #688
Closed

Support for DIO HTTP library #673

ueman opened this issue Dec 20, 2021 · 12 comments · Fixed by #688

Comments

@ueman
Copy link
Collaborator

ueman commented Dec 20, 2021

dio is a popular HTTP library and I would like to request support for it, similar to the already existing support for the SentryHttpClient.

They have support for so-called interceptors, which should make it straight forward and also pretty similar to the SentryHttpClient.

@kuhnroyal
Copy link
Contributor

kuhnroyal commented Dec 20, 2021

I use DIO as well, can share what I am currently using.

/// A [Dio] interceptor that adds [Breadcrumb]s and
/// transaction spans to existing transactions for
/// performance monitoring.
class HttpBreadcrumbInterceptor extends Interceptor {
  final _request = HttpRequestBreadcrumbInterceptor();
  final _success = HttpSuccessBreadcrumbInterceptor();
  final _error = HttpErrorBreadcrumbInterceptor();

  @override
  Future<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    final currentSpan = Sentry.getSpan();
    final span = currentSpan?.startChild(
      'http.client',
      description: '${options.method} ${options.uri}',
    );

    if (span != null) {
      final traceHeader = span.toSentryTrace();
      options.headers[traceHeader.name] = traceHeader.value;
      options.extra['sentry-span'] = span;
    }
    return _request.onRequest(options, handler);
  }

  @override
  Future<void> onResponse(
    Response<dynamic> response,
    ResponseInterceptorHandler handler,
  ) async {
    final span = response.requestOptions.extra['sentry-span'] as ISentrySpan?;
    span?.status = SpanStatus.fromHttpStatusCode(response.statusCode!);
    await _success.onResponse(response, handler);
    await span?.finish();
  }

  @override
  Future<void> onError(
    DioError err,
    ErrorInterceptorHandler handler,
  ) async {
    final span = err.requestOptions.extra['sentry-span'] as ISentrySpan?;
    await _error.onError(err, handler);
    span?.throwable = err;
    span?.status = const SpanStatus.internalError();
    await span?.finish();
  }
}

class HttpRequestBreadcrumbInterceptor extends Interceptor {
  @override
  Future<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    Sentry.addBreadcrumb(
      HttpBreadcrumb.onRequest(
        HttpBreadcrumbData(
          url: options.uri.toString(),
          method: options.method,
        ),
      ),
    );
    handler.next(options);
  }
}

class HttpSuccessBreadcrumbInterceptor extends Interceptor {
  @override
  Future<void> onResponse(
    Response<dynamic> response,
    ResponseInterceptorHandler handler,
  ) async {
    Sentry.addBreadcrumb(
      HttpBreadcrumb.onResponse(
        HttpBreadcrumbData(
          url: response.requestOptions.uri.toString(),
          statusCode: response.statusCode,
          method: response.requestOptions.method,
          reason: response.statusMessage,
        ),
      ),
    );
    handler.next(response);
  }
}

class HttpErrorBreadcrumbInterceptor extends Interceptor {
  @override
  Future<void> onError(
    DioError error,
    ErrorInterceptorHandler handler,
  ) async {
    await addHttpErrorBreadcrumb(error);
    handler.next(error);
  }

  static Future<void> addHttpErrorBreadcrumb(DioError error) async {
    String? body;
    if (error.response?.data != null) {
      try {
        body = jsonEncode(error.response!.data);
      } catch (_) {
        // Ignore and call toString()
        body = 'JSON encoding failed: ${error.response!.data.toString()}';
      }
    }
    Sentry.addBreadcrumb(
      HttpBreadcrumb.onResponse(
        HttpBreadcrumbData(
          url: error.requestOptions.uri.toString(),
          statusCode: error.response?.statusCode,
          method: error.requestOptions.method,
          reason: error.response?.statusMessage,
          data: {
            'type': error.type.toString(),
            'err': error.message,
            if (body != null) 'body': body,
          },
        ),
      ),
    );
  }
}

class HttpBreadcrumb {
  const HttpBreadcrumb._();

  static Breadcrumb _httpBreadcrumb(
    String message, {
    DateTime? timestamp,
    required String category,
    required Map<String, String?> data,
  }) =>
      Breadcrumb(
        message: message,
        timestamp: timestamp ?? DateTime.now().toUtc(),
        type: 'http',
        category: category,
        data: data,
      );

  static Breadcrumb onRequest(HttpBreadcrumbData request) => _httpBreadcrumb(
        '${request.method}: ${request.url}',
        category: 'Request',
        data: request.toMap(),
      );

  static Breadcrumb onResponse(HttpBreadcrumbData response) => _httpBreadcrumb(
        '${response.statusCode} ${response.method}: ${response.url}',
        category: 'Response: ${response.statusCode}',
        data: response.toMap(),
      );
}

@ueman
Copy link
Collaborator Author

ueman commented Dec 20, 2021

@kuhnroyal Do you mind also sharing the source for HttpBreadcrumb?

@kuhnroyal
Copy link
Contributor

Added it above, it is very basic and the breadcrumbs are from the old Sentry client way back.
But could probably use the same breadcrumb implementation that is in the HttpClient adapter.
The span handling I just copied from there recently, could probably be a separate interceptor.

@ueman
Copy link
Collaborator Author

ueman commented Dec 20, 2021

Awesome, thanks!

@marandaneto
Copy link
Contributor

thanks @kuhnroyal

@ueman
Copy link
Collaborator Author

ueman commented Dec 23, 2021

After looking into it some more, I believe we can use dios HttpClientAdapter and even mostly copy the existing SentryHttpClient code, because it's basically the same.

The failing request client is a little bit more work to migrate, but the code below already works just as expected.

var dio = Dio();
  dio.httpClientAdapter = SentryHttpClient(recordBreadcrumbs: true, networkTracing: true);
import 'dart:typed_data';

import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:sentry/sentry.dart';

class BreadcrumbClient extends HttpClientAdapter {
  BreadcrumbClient({HttpClientAdapter? client, Hub? hub})
      : _hub = hub ?? HubAdapter(),
        _client = client ?? DefaultHttpClientAdapter();

  final HttpClientAdapter _client;
  final Hub _hub;

  @override
  Future<ResponseBody> fetch(
    RequestOptions options,
    Stream<Uint8List>? requestStream,
    Future? cancelFuture,
  ) async {
    // See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/

    var requestHadException = false;
    int? statusCode;
    String? reason;
    int? responseBodySize;

    final stopwatch = Stopwatch();
    stopwatch.start();

    try {
      final response =
          await _client.fetch(options, requestStream, cancelFuture);

      statusCode = response.statusCode;
      reason = response.statusMessage;

      return response;
    } catch (_) {
      requestHadException = true;
      rethrow;
    } finally {
      stopwatch.stop();

      final breadcrumb = Breadcrumb.http(
        level: requestHadException ? SentryLevel.error : SentryLevel.info,
        url: options.uri,
        method: options.method,
        statusCode: statusCode,
        reason: reason,
        requestDuration: stopwatch.elapsed,
        responseBodySize: responseBodySize,
      );

      _hub.addBreadcrumb(breadcrumb);
    }
  }

  @override
  void close({bool force = false}) => _client.close(force: force);
}


import 'dart:typed_data';

import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

/// A [http](https://pub.dev/packages/http)-package compatible HTTP client
/// which adds support to Sentry Performance feature.
/// https://develop.sentry.dev/sdk/performance
class TracingClient extends HttpClientAdapter {
  TracingClient({HttpClientAdapter? client, Hub? hub})
      : _hub = hub ?? HubAdapter(),
        _client = client ?? DefaultHttpClientAdapter();

  final HttpClientAdapter _client;
  final Hub _hub;

  @override
  Future<ResponseBody> fetch(
    RequestOptions options,
    Stream<Uint8List>? requestStream,
    Future? cancelFuture,
  ) async {
    // see https://develop.sentry.dev/sdk/performance/#header-sentry-trace
    final currentSpan = _hub.getSpan();
    final span = currentSpan?.startChild(
      'http.client',
      description: '${options.method} ${options.uri}',
    );

    ResponseBody? response;
    try {
      if (span != null) {
        final traceHeader = span.toSentryTrace();
        options.headers[traceHeader.name] = traceHeader.value;
      }

      // TODO: tracingOrigins support

      response = await _client.fetch(options, requestStream, cancelFuture);
      span?.status = SpanStatus.fromHttpStatusCode(response.statusCode ?? -1);
    } catch (exception) {
      span?.throwable = exception;
      span?.status = const SpanStatus.internalError();

      rethrow;
    } finally {
      await span?.finish();
    }
    return response;
  }

  @override
  void close({bool force = false}) => _client.close(force: force);
}

class SentryHttpClient extends HttpClientAdapter {
  SentryHttpClient({
    HttpClientAdapter? client,
    Hub? hub,
    bool recordBreadcrumbs = true,
    bool networkTracing = false,
  }) {
    _hub = hub ?? HubAdapter();

    var innerClient = client ?? DefaultHttpClientAdapter();

    if (networkTracing) {
      innerClient = TracingClient(client: innerClient, hub: _hub);
    }

    // The ordering here matters.
    // We don't want to include the breadcrumbs for the current request
    // when capturing it as a failed request.
    // However it still should be added for following events.
    if (recordBreadcrumbs) {
      innerClient = BreadcrumbClient(client: innerClient, hub: _hub);
    }

    _client = innerClient;
  }

  late HttpClientAdapter _client;
  late Hub _hub;

  @override
  Future<ResponseBody> fetch(
    RequestOptions options,
    Stream<Uint8List>? requestStream,
    Future? cancelFuture,
  ) =>
      _client.fetch(options, requestStream, cancelFuture);

  @override
  void close({bool force = false}) => _client.close(force: force);
}

@kuhnroyal
Copy link
Contributor

That works but Dio generally promotes the use of interceptors.

@ueman
Copy link
Collaborator Author

ueman commented Dec 23, 2021

Yeah, I agree. However, this approach reduces the amount of maintenance, because it's so similar to the already existing SentryHttpClient. Using the adapter instead of an interceptor is probably also easier to use, because the user doesn't need to pay attention to the order of interceptors. The adapter is already most likely the inner most thing. This is especially important because there are things like the CacheInterceptor, which could bypass reads from the network, in which case I wouldn't want to create a span.

@ueman ueman mentioned this issue Jan 2, 2022
5 tasks
@ueman
Copy link
Collaborator Author

ueman commented Jan 2, 2022

@kuhnroyal are you one of the new maintainers of dio? Anyway, I've put up #688 for review, maybe you want to take a look at it too.

@kuhnroyal
Copy link
Contributor

Yea, I'll check it out tomorrow.

@kuhnroyal
Copy link
Contributor

Looks fine. Integrated it and will test tomorrow.
Aside from dio, I noticed today that there is no Sentry client for the pure Dart HttpClient from the SDK, not the one from the http package.

@ueman
Copy link
Collaborator Author

ueman commented Jan 5, 2022

I noticed today that there is no Sentry client for the pure Dart HttpClient from the SDK, not the one from the http package.

Related: #539

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants