diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c3d98f3f..2424bad130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ * Bump Sentry Android SDK to [5.2.0](https://github.com/getsentry/sentry-dart/pull/594) (#594) - [changelog](https://github.com/getsentry/sentry-java/blob/5.2.0/CHANGELOG.md) - [diff](https://github.com/getsentry/sentry-java/compare/5.1.2...5.2.0) -* enrich Dart context with isolate name (#600) +* Feat: Enrich Dart context with isolate name (#600) +* Feat: Sentry Performance for HTTP client (#603) # 6.1.0-alpha.1 diff --git a/dart/lib/src/http_client/breadcrumb_client.dart b/dart/lib/src/http_client/breadcrumb_client.dart index 4459773d83..f7d02a97e2 100644 --- a/dart/lib/src/http_client/breadcrumb_client.dart +++ b/dart/lib/src/http_client/breadcrumb_client.dart @@ -91,8 +91,5 @@ class BreadcrumbClient extends BaseClient { } @override - void close() { - // See https://github.com/getsentry/sentry-dart/pull/226#discussion_r536984785 - _client.close(); - } + void close() => _client.close(); } diff --git a/dart/lib/src/http_client/failed_request_client.dart b/dart/lib/src/http_client/failed_request_client.dart index 649f9f4ea4..d4966a859f 100644 --- a/dart/lib/src/http_client/failed_request_client.dart +++ b/dart/lib/src/http_client/failed_request_client.dart @@ -143,10 +143,7 @@ class FailedRequestClient extends BaseClient { } @override - void close() { - // See https://github.com/getsentry/sentry-dart/pull/226#discussion_r536984785 - _client.close(); - } + void close() => _client.close(); // See https://develop.sentry.dev/sdk/event-payloads/request/ Future _captureEvent({ diff --git a/dart/lib/src/http_client/sentry_http_client.dart b/dart/lib/src/http_client/sentry_http_client.dart index 6c18f3a233..c767dc1999 100644 --- a/dart/lib/src/http_client/sentry_http_client.dart +++ b/dart/lib/src/http_client/sentry_http_client.dart @@ -1,4 +1,5 @@ import 'package:http/http.dart'; +import 'tracing_client.dart'; import '../hub.dart'; import '../hub_adapter.dart'; import '../protocol.dart'; @@ -7,9 +8,9 @@ import 'failed_request_client.dart'; /// A [http](https://pub.dev/packages/http)-package compatible HTTP client. /// -/// It can record requests as breadcrumbs. This is on by default. +/// It records requests as breadcrumbs. This is on by default. /// -/// It can also capture requests which throw an exception. This is off by +/// It captures requests which throws an exception. This is off by /// default, set [captureFailedRequests] to `true` to enable it. This can be for /// example for the following reasons: /// - In an browser environment this can be requests which fail because of CORS. @@ -32,6 +33,10 @@ import 'failed_request_client.dart'; /// ); /// ``` /// +/// It starts and finishes a Span if there's a transaction bound to the Scope +/// through the [TracingClient] client, it's disabled by default. +/// Set [networkTracing] to `true` to enable it. +/// /// Remarks: If this client is used as a wrapper, a call to close also closes /// the given client. /// @@ -79,6 +84,7 @@ class SentryHttpClient extends BaseClient { List failedRequestStatusCodes = const [], bool captureFailedRequests = false, bool sendDefaultPii = false, + bool networkTracing = false, }) { _hub = hub ?? HubAdapter(); @@ -93,6 +99,10 @@ class SentryHttpClient extends BaseClient { client: innerClient, ); + 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. @@ -110,6 +120,7 @@ class SentryHttpClient extends BaseClient { @override Future send(BaseRequest request) => _client.send(request); + // See https://github.com/getsentry/sentry-dart/pull/226#discussion_r536984785 @override void close() => _client.close(); } diff --git a/dart/lib/src/http_client/tracing_client.dart b/dart/lib/src/http_client/tracing_client.dart new file mode 100644 index 0000000000..8f35277ad9 --- /dev/null +++ b/dart/lib/src/http_client/tracing_client.dart @@ -0,0 +1,50 @@ +import 'package:http/http.dart'; +import '../hub.dart'; +import '../hub_adapter.dart'; +import '../protocol.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 BaseClient { + TracingClient({Client? client, Hub? hub}) + : _hub = hub ?? HubAdapter(), + _client = client ?? Client(); + + final Client _client; + final Hub _hub; + + @override + Future send(BaseRequest request) async { + // see https://develop.sentry.dev/sdk/performance/#header-sentry-trace + final currentSpan = _hub.getSpan(); + final span = currentSpan?.startChild( + 'http.client', + description: '${request.method} ${request.url}', + ); + + StreamedResponse? response; + try { + if (span != null) { + final traceHeader = span.toSentryTrace(); + request.headers[traceHeader.name] = traceHeader.value; + } + + // TODO: tracingOrigins support + + response = await _client.send(request); + span?.status = SpanStatus.fromHttpStatusCode(response.statusCode); + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + return response; + } + + @override + void close() => _client.close(); +} diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index d42f09f64d..e533ef3501 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -400,6 +400,11 @@ class Hub { SentryLevel.warning, "Instance is disabled and this 'getSpan' call is a no-op.", ); + } else if (!_options.isTracingEnabled()) { + _options.logger( + SentryLevel.info, + "Tracing is disabled and this 'getSpan' returns null.", + ); } else { final item = _peek(); @@ -418,6 +423,11 @@ class Hub { SentryLevel.warning, "Instance is disabled and this 'captureTransaction' call is a no-op.", ); + } else if (!_options.isTracingEnabled()) { + _options.logger( + SentryLevel.info, + "Tracing is disabled and this 'captureTransaction' call is a no-op.", + ); } else if (!transaction.finished) { _options.logger( SentryLevel.warning, diff --git a/dart/lib/src/invalid_sentry_trace_header_exception.dart b/dart/lib/src/invalid_sentry_trace_header_exception.dart new file mode 100644 index 0000000000..4b953fabfa --- /dev/null +++ b/dart/lib/src/invalid_sentry_trace_header_exception.dart @@ -0,0 +1,7 @@ +class InvalidSentryTraceHeaderException implements Exception { + final String _message; + InvalidSentryTraceHeaderException(this._message); + + @override + String toString() => 'Exception: $_message'; +} diff --git a/dart/lib/src/noop_sentry_span.dart b/dart/lib/src/noop_sentry_span.dart index 3f3caa426c..d1a98c4d87 100644 --- a/dart/lib/src/noop_sentry_span.dart +++ b/dart/lib/src/noop_sentry_span.dart @@ -13,6 +13,12 @@ class NoOpSentrySpan extends ISentrySpan { operation: 'noop', ); + static final _header = SentryTraceHeader( + SentryId.empty(), + SpanId.empty(), + sampled: false, + ); + static final _timestamp = getUtcDateTime(); factory NoOpSentrySpan() { @@ -64,4 +70,7 @@ class NoOpSentrySpan extends ISentrySpan { @override bool? get sampled => null; + + @override + SentryTraceHeader toSentryTrace() => _header; } diff --git a/dart/lib/src/protocol.dart b/dart/lib/src/protocol.dart index b5004d4d36..eae944558c 100644 --- a/dart/lib/src/protocol.dart +++ b/dart/lib/src/protocol.dart @@ -32,3 +32,4 @@ export 'protocol/span_id.dart'; export 'protocol/sentry_transaction.dart'; export 'protocol/sentry_trace_context.dart'; export 'protocol/sentry_span.dart'; +export 'protocol/sentry_trace_header.dart'; diff --git a/dart/lib/src/protocol/sentry_span.dart b/dart/lib/src/protocol/sentry_span.dart index 0b0b09a8b9..8d60f6bdd4 100644 --- a/dart/lib/src/protocol/sentry_span.dart +++ b/dart/lib/src/protocol/sentry_span.dart @@ -1,5 +1,5 @@ import '../hub.dart'; -import 'span_status.dart'; +import '../protocol.dart'; import '../sentry_tracer.dart'; import '../tracing.dart'; @@ -121,4 +121,11 @@ class SentrySpan extends ISentrySpan { Map get tags => _tags; Map get data => _data; + + @override + SentryTraceHeader toSentryTrace() => SentryTraceHeader( + _context.traceId, + _context.spanId, + sampled: sampled, + ); } diff --git a/dart/lib/src/protocol/sentry_trace_header.dart b/dart/lib/src/protocol/sentry_trace_header.dart new file mode 100644 index 0000000000..0e1e11fe00 --- /dev/null +++ b/dart/lib/src/protocol/sentry_trace_header.dart @@ -0,0 +1,51 @@ +import 'package:meta/meta.dart'; + +import '../invalid_sentry_trace_header_exception.dart'; +import '../protocol.dart'; + +/// Represents HTTP header "sentry-trace". +@immutable +class SentryTraceHeader { + static const _traceHeader = 'sentry-trace'; + + final SentryId traceId; + final SpanId spanId; + final bool? sampled; + + String get name => _traceHeader; + + String get value { + if (sampled != null) { + final sampled = this.sampled! ? '1' : '0'; + return '$traceId-$spanId-$sampled'; + } else { + return '$traceId-$spanId'; + } + } + + SentryTraceHeader( + this.traceId, + this.spanId, { + bool? sampled, + }) : sampled = sampled; + + factory SentryTraceHeader.fromTraceHeader(String header) { + final parts = header.split('-'); + bool? sampled; + + if (parts.length < 2) { + throw InvalidSentryTraceHeaderException('Header: $header is invalid.'); + } else if (parts.length == 3) { + sampled = '1' == parts[2]; + } + + final traceId = SentryId.fromId(parts[0]); + final spanId = SpanId.fromId(parts[1]); + + return SentryTraceHeader( + traceId, + spanId, + sampled: sampled, + ); + } +} diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 543deb6a13..4ded2d49ce 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -251,8 +251,6 @@ class Sentry { bindToScope: bindToScope, ); - // missing traceHeaders - /// Gets the current active transaction or span. static ISentrySpan? getSpan() => _hub.getSpan(); diff --git a/dart/lib/src/sentry_span_interface.dart b/dart/lib/src/sentry_span_interface.dart index df92ad9056..4942fb736f 100644 --- a/dart/lib/src/sentry_span_interface.dart +++ b/dart/lib/src/sentry_span_interface.dart @@ -1,6 +1,6 @@ import 'package:meta/meta.dart'; -import 'protocol/span_status.dart'; +import 'protocol.dart'; import 'tracing.dart'; /// Represents performance monitoring Span. @@ -42,7 +42,6 @@ abstract class ISentrySpan { /// Returns the star timestamp DateTime get startTimestamp; - // missing toTraceHeader /// Returns true if span is finished bool get finished; @@ -55,4 +54,7 @@ abstract class ISentrySpan { @internal bool? get sampled; + + /// Returns the trace information that could be sent as a sentry-trace header. + SentryTraceHeader toSentryTrace(); } diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index 29aad8d2dd..964f2097a7 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -132,4 +132,7 @@ class SentryTracer extends ISentrySpan { @override bool? get sampled => _rootSpan.sampled; + + @override + SentryTraceHeader toSentryTrace() => _rootSpan.toSentryTrace(); } diff --git a/dart/lib/src/sentry_transaction_context.dart b/dart/lib/src/sentry_transaction_context.dart index f9f2f9117f..b9d1961143 100644 --- a/dart/lib/src/sentry_transaction_context.dart +++ b/dart/lib/src/sentry_transaction_context.dart @@ -26,6 +26,20 @@ class SentryTransactionContext extends SentrySpanContext { parentSpanId: parentSpanId, ); + factory SentryTransactionContext.fromSentryTrace( + String name, + String operation, + SentryTraceHeader traceHeader, + ) { + return SentryTransactionContext( + name, + operation, + traceId: traceHeader.traceId, + parentSpanId: traceHeader.spanId, + parentSampled: traceHeader.sampled, + ); + } + SentryTransactionContext copyWith({ String? name, String? operation, diff --git a/dart/lib/src/tracing.dart b/dart/lib/src/tracing.dart index 0bba524710..362463076d 100644 --- a/dart/lib/src/tracing.dart +++ b/dart/lib/src/tracing.dart @@ -3,3 +3,4 @@ export 'sentry_sampling_context.dart'; export 'sentry_span_context.dart'; export 'sentry_span_interface.dart'; export 'noop_sentry_span.dart'; +export 'invalid_sentry_trace_header_exception.dart'; diff --git a/dart/test/http_client/sentry_http_client_test.dart b/dart/test/http_client/sentry_http_client_test.dart index da8042241b..390cc98561 100644 --- a/dart/test/http_client/sentry_http_client_test.dart +++ b/dart/test/http_client/sentry_http_client_test.dart @@ -73,6 +73,32 @@ void main() { expect(mockHub.captureExceptionCalls.length, 0); verify(mockClient.close()); }); + + test('no captured span if tracing disabled', () async { + final sut = fixture.getSut( + client: fixture.getClient(statusCode: 200, reason: 'OK'), + recordBreadcrumbs: false, + networkTracing: false, + ); + + final response = await sut.get(requestUri); + expect(response.statusCode, 200); + + expect(fixture.hub.getSpanCalls, 0); + }); + + test('captured span if tracing enabled', () async { + final sut = fixture.getSut( + client: fixture.getClient(statusCode: 200, reason: 'OK'), + recordBreadcrumbs: false, + networkTracing: true, + ); + + final response = await sut.get(requestUri); + expect(response.statusCode, 200); + + expect(fixture.hub.getSpanCalls, 1); + }); }); } @@ -94,6 +120,7 @@ class Fixture { MaxRequestBodySize maxRequestBodySize = MaxRequestBodySize.never, List badStatusCodes = const [], bool recordBreadcrumbs = true, + bool networkTracing = false, }) { final mc = client ?? getClient(); return SentryHttpClient( @@ -103,6 +130,7 @@ class Fixture { failedRequestStatusCodes: badStatusCodes, maxRequestBodySize: maxRequestBodySize, recordBreadcrumbs: recordBreadcrumbs, + networkTracing: networkTracing, ); } diff --git a/dart/test/http_client/tracing_client_test.dart b/dart/test/http_client/tracing_client_test.dart new file mode 100644 index 0000000000..526e3ab411 --- /dev/null +++ b/dart/test/http_client/tracing_client_test.dart @@ -0,0 +1,162 @@ +import 'package:http/http.dart'; +import 'package:http/testing.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/http_client/tracing_client.dart'; +import 'package:sentry/src/sentry_tracer.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../mocks/mock_transport.dart'; + +final requestUri = Uri.parse('https://example.com?foo=bar'); + +void main() { + group(TracingClient, () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('captured span if successful request', () async { + final sut = fixture.getSut( + client: fixture.getClient(statusCode: 200, reason: 'OK'), + ); + final tr = fixture._hub.startTransaction( + 'name', + 'op', + bindToScope: true, + ); + + await sut.get(requestUri); + + await tr.finish(); + + final tracer = (tr as SentryTracer); + final span = tracer.children.first; + + expect(span.status, SpanStatus.ok()); + expect(span.context.operation, 'http.client'); + expect(span.context.description, 'GET https://example.com?foo=bar'); + }); + + test('finish span if errored request', () async { + final sut = fixture.getSut( + client: createThrowingClient(), + ); + final tr = fixture._hub.startTransaction( + 'name', + 'op', + bindToScope: true, + ); + + try { + await sut.get(requestUri); + } catch (_) { + // ignore + } + + await tr.finish(); + + final tracer = (tr as SentryTracer); + final span = tracer.children.first; + + expect(span.finished, isTrue); + }); + + test('associate exception to span if errored request', () async { + final sut = fixture.getSut( + client: createThrowingClient(), + ); + final tr = fixture._hub.startTransaction( + 'name', + 'op', + bindToScope: true, + ); + + dynamic exception; + try { + await sut.get(requestUri); + } catch (error) { + exception = error; + } + + await tr.finish(); + + final tracer = (tr as SentryTracer); + final span = tracer.children.first; + + expect(span.status, SpanStatus.internalError()); + expect(span.throwable, exception); + }); + + test('captured span adds sentry-trace header to the request', () async { + final sut = fixture.getSut( + client: fixture.getClient(statusCode: 200, reason: 'OK'), + ); + final tr = fixture._hub.startTransaction( + 'name', + 'op', + bindToScope: true, + ); + + final response = await sut.get(requestUri); + + await tr.finish(); + + final tracer = (tr as SentryTracer); + final span = tracer.children.first; + + expect(response.request!.headers['sentry-trace'], + '${span.toSentryTrace().value}'); + }); + + test('do not throw if no span bound to the scope', () async { + final sut = fixture.getSut( + client: fixture.getClient(statusCode: 200, reason: 'OK'), + ); + + await sut.get(requestUri); + }); + }); +} + +MockClient createThrowingClient() { + return MockClient( + (request) async { + expect(request.url, requestUri); + throw TestException(); + }, + ); +} + +class CloseableMockClient extends Mock implements BaseClient {} + +class Fixture { + final _options = SentryOptions(dsn: fakeDsn); + late Hub _hub; + final transport = MockTransport(); + Fixture() { + _options.transport = transport; + _options.tracesSampleRate = 1.0; + _hub = Hub(_options); + } + + TracingClient getSut({MockClient? client}) { + final mc = client ?? getClient(); + return TracingClient( + client: mc, + hub: _hub, + ); + } + + MockClient getClient({int statusCode = 200, String? reason}) { + return MockClient((request) async { + expect(request.url, requestUri); + return Response('', statusCode, reasonPhrase: reason); + }); + } +} + +class TestException implements Exception {} diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index ad56f55104..32bf3ab1d9 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -258,6 +258,18 @@ void main() { expect(hub.getSpan(), isNull); }); + test('get span does not return span if tracing is disabled', () async { + final hub = fixture.getSut(tracesSampleRate: null); + + hub.startTransaction( + 'name', + 'op', + description: 'desc', + ); + + expect(hub.getSpan(), isNull); + }); + test('transaction isnt captured if not sampled', () async { final hub = fixture.getSut(sampled: false); @@ -267,6 +279,15 @@ void main() { expect(id, SentryId.empty()); }); + test('transaction isnt captured if tracing is disabled', () async { + final hub = fixture.getSut(tracesSampleRate: null); + + var tr = SentryTransaction(fixture.tracer); + final id = await hub.captureTransaction(tr); + + expect(id, SentryId.empty()); + }); + test('transaction is captured', () async { final hub = fixture.getSut(); diff --git a/dart/test/mocks/mock_hub.dart b/dart/test/mocks/mock_hub.dart index 6562544e76..b0e672abbd 100644 --- a/dart/test/mocks/mock_hub.dart +++ b/dart/test/mocks/mock_hub.dart @@ -14,6 +14,7 @@ class MockHub implements Hub { int closeCalls = 0; bool _isEnabled = true; int spanContextCals = 0; + int getSpanCalls = 0; /// Useful for tests. void reset() { @@ -26,6 +27,7 @@ class MockHub implements Hub { _isEnabled = true; spanContextCals = 0; captureTransactionCalls = []; + getSpanCalls = 0; } @override @@ -141,6 +143,7 @@ class MockHub implements Hub { @override ISentrySpan? getSpan() { + getSpanCalls++; return null; } diff --git a/dart/test/protocol/sentry_trace_header_test.dart b/dart/test/protocol/sentry_trace_header_test.dart new file mode 100644 index 0000000000..f106f72580 --- /dev/null +++ b/dart/test/protocol/sentry_trace_header_test.dart @@ -0,0 +1,41 @@ +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final _traceId = SentryId.newId(); + final _spanId = SpanId.newId(); + test('header adds 1 to sampled', () { + final header = SentryTraceHeader(_traceId, _spanId, sampled: true); + + expect(header.value, '$_traceId-$_spanId-1'); + }); + + test('header adds 0 to not sampled', () { + final header = SentryTraceHeader(_traceId, _spanId, sampled: false); + + expect(header.value, '$_traceId-$_spanId-0'); + }); + + test('header does not add sampled if no sampled decision', () { + final header = SentryTraceHeader(_traceId, _spanId); + + expect(header.value, '$_traceId-$_spanId'); + }); + + test('header return its name', () { + final header = SentryTraceHeader(_traceId, _spanId); + + expect(header.name, 'sentry-trace'); + }); + + test('invalid header throws $InvalidSentryTraceHeaderException', () { + var exception; + try { + SentryTraceHeader.fromTraceHeader('invalidHeader'); + } catch (error) { + exception = error; + } + + expect(exception is InvalidSentryTraceHeaderException, true); + }); +} diff --git a/dart/test/sentry_span_test.dart b/dart/test/sentry_span_test.dart index 01bc8c47a3..92a7baaf4c 100644 --- a/dart/test/sentry_span_test.dart +++ b/dart/test/sentry_span_test.dart @@ -106,6 +106,13 @@ void main() { expect(sut.finished, true); }); + + test('toSentryTrace returns trace header', () { + final sut = fixture.getSut(); + + expect(sut.toSentryTrace().value, + '${sut.context.traceId}-${sut.context.spanId}-1'); + }); } class Fixture { diff --git a/dart/test/sentry_tracer_test.dart b/dart/test/sentry_tracer_test.dart index 3a9f3631f1..640a249fa8 100644 --- a/dart/test/sentry_tracer_test.dart +++ b/dart/test/sentry_tracer_test.dart @@ -105,6 +105,13 @@ void main() { expect(childSpan.context.operation, 'op'); expect(childSpan.context.parentSpanId.toString(), parentId.toString()); }); + + test('toSentryTrace returns trace header', () { + final sut = fixture.getSut(); + + expect(sut.toSentryTrace().value, + '${sut.context.traceId}-${sut.context.spanId}-1'); + }); } class Fixture { diff --git a/dart/test/sentry_transaction_context_test.dart b/dart/test/sentry_transaction_context_test.dart new file mode 100644 index 0000000000..f760954e28 --- /dev/null +++ b/dart/test/sentry_transaction_context_test.dart @@ -0,0 +1,31 @@ +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final _traceId = SentryId.newId(); + final _spanId = SpanId.newId(); + + SentryTransactionContext getSentryTransactionContext({bool? sampled}) { + final header = SentryTraceHeader(_traceId, _spanId, sampled: sampled); + return SentryTransactionContext.fromSentryTrace( + 'name', 'operation', header); + } + + test('parent span id is set from header', () { + final context = getSentryTransactionContext(); + + expect(context.parentSpanId, _spanId); + }); + + test('trace id is set from header', () { + final context = getSentryTransactionContext(); + + expect(context.traceId, _traceId); + }); + + test('parent sampled is set from header', () { + final context = getSentryTransactionContext(sampled: true); + + expect(context.parentSampled, true); + }); +} diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 702c8816e3..a707ad782d 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -473,14 +473,23 @@ class SecondaryScaffold extends StatelessWidget { } Future makeWebRequest(BuildContext context) async { + final transaction = Sentry.startTransaction( + 'flutterwebrequest', + 'request', + bindToScope: true, + ); + final client = SentryHttpClient( captureFailedRequests: true, + networkTracing: true, failedRequestStatusCodes: [SentryStatusCode.range(400, 500)], ); // We don't do any exception handling here. // In case of an exception, let it get caught and reported to Sentry final response = await client.get(Uri.parse('https://flutter.dev/')); + await transaction.finish(status: SpanStatus.ok()); + await showDialog( context: context, // gets tracked if using SentryNavigatorObserver