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

feat: spotlight support #1786

Merged
merged 20 commits into from
Dec 27, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features

- Add [Spotlight](https://spotlightjs.com/about/) support ([#1786](https://github.com/getsentry/sentry-dart/pull/1786))
- Set `options.spotlight = Spotlight(enabled: true)` to enable Spotlight
- Add `ConnectivityIntegration` for web ([#1765](https://github.com/getsentry/sentry-dart/pull/1765))
- We only get the info if online/offline on web platform. The added breadcrumb is set to either `wifi` or `none`.
- APM for isar ([#1726](https://github.com/getsentry/sentry-dart/pull/1726))
Expand Down
2 changes: 2 additions & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ export 'src/utils/http_header_utils.dart';
export 'src/sentry_trace_origins.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils.dart';
// spotlight debugging
export 'src/spotlight.dart';
4 changes: 4 additions & 0 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'sentry_options.dart';
import 'sentry_stack_trace_factory.dart';
import 'transport/http_transport.dart';
import 'transport/noop_transport.dart';
import 'transport/spotlight_http_transport.dart';
import 'utils/isolate_utils.dart';
import 'version.dart';
import 'sentry_envelope.dart';
Expand Down Expand Up @@ -49,6 +50,9 @@ class SentryClient {
final rateLimiter = RateLimiter(options);
options.transport = HttpTransport(options, rateLimiter);
}
if (options.spotlight.enabled) {
options.transport = SpotlightHttpTransport(options, options.transport);
}
buenaflor marked this conversation as resolved.
Show resolved Hide resolved
return SentryClient._(options);
}

Expand Down
7 changes: 7 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,13 @@ class SentryOptions {
/// Settings this to `false` will set the `level` to [SentryLevel.error].
bool markAutomaticallyCollectedErrorsAsFatal = true;

/// The Spotlight configuration.
/// Disabled by default.
/// ```dart
/// spotlight = Spotlight(enabled: true)
/// ```
Spotlight spotlight = Spotlight(enabled: false);
buenaflor marked this conversation as resolved.
Show resolved Hide resolved

SentryOptions({this.dsn, PlatformChecker? checker}) {
if (checker != null) {
platformChecker = checker;
Expand Down
21 changes: 21 additions & 0 deletions dart/lib/src/spotlight.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'platform_checker.dart';

/// Spotlight configuration class.
class Spotlight {
/// Whether to enable Spotlight for local development.
bool enabled;

/// The Spotlight Sidecar URL.
/// Defaults to http://10.0.2.2:8969/stream due to Emulator on Android.
/// Otherwise defaults to http://localhost:8969/stream.
String url;

Spotlight({required this.enabled, String? url})
: url = url ?? _defaultSpotlightUrl();
}

String _defaultSpotlightUrl() {
return (PlatformChecker().platform.isAndroid
? 'http://10.0.2.2:8969/stream'
: 'http://localhost:8969/stream');
}
135 changes: 19 additions & 116 deletions dart/lib/src/transport/http_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
import 'dart:convert';

import 'package:http/http.dart';
import '../utils/transport_utils.dart';
import 'http_transport_request_handler.dart';

import '../client_reports/client_report_recorder.dart';
import '../client_reports/discard_reason.dart';
import 'data_category.dart';
import 'noop_encode.dart' if (dart.library.io) 'encode.dart';
import '../noop_client.dart';
import '../protocol.dart';
import '../sentry_options.dart';
Expand All @@ -18,15 +16,9 @@
class HttpTransport implements Transport {
final SentryOptions _options;

final Dsn _dsn;

final RateLimiter _rateLimiter;

final ClientReportRecorder _recorder;

late _CredentialBuilder _credentialBuilder;

final Map<String, String> _headers;
final HttpTransportRequestHandler _requestHandler;

factory HttpTransport(SentryOptions options, RateLimiter rateLimiter) {
if (options.httpClient is NoOpClient) {
Expand All @@ -37,17 +29,8 @@
}

HttpTransport._(this._options, this._rateLimiter)
: _dsn = Dsn.parse(_options.dsn!),
_recorder = _options.recorder,
_headers = _buildHeaders(
_options.platformChecker.isWeb,
_options.sentryClientName,
) {
_credentialBuilder = _CredentialBuilder(
_dsn,
_options.sentryClientName,
);
}
: _requestHandler = HttpTransportRequestHandler(
_options, Dsn.parse(_options.dsn!).postUri);

@override
Future<SentryId?> send(SentryEnvelope envelope) async {
Expand All @@ -57,63 +40,31 @@
}
filteredEnvelope.header.sentAt = _options.clock();

final streamedRequest = await _createStreamedRequest(filteredEnvelope);
final streamedRequest =
await _requestHandler.createRequest(filteredEnvelope);

final response = await _options.httpClient
.send(streamedRequest)
.then(Response.fromStream);

_updateRetryAfterLimits(response);

if (response.statusCode != 200) {
// body guard to not log the error as it has performance impact to allocate
// the body String.
if (_options.debug) {
_options.logger(
SentryLevel.error,
'API returned an error, statusCode = ${response.statusCode}, '
'body = ${response.body}',
);
}

if (response.statusCode >= 400 && response.statusCode != 429) {
_recorder.recordLostEvent(
DiscardReason.networkError, DataCategory.error);
}

return SentryId.empty();
} else {
_options.logger(
SentryLevel.debug,
'Envelope ${envelope.header.eventId ?? "--"} was sent successfully.',
);
}
TransportUtils.logResponse(_options, envelope, response, target: 'Sentry');

final eventId = json.decode(response.body)['id'];
if (eventId == null) {
return null;
if (response.statusCode == 200) {
return _parseEventId(response);
}
return SentryId.fromId(eventId);
return SentryId.empty();
}

Future<StreamedRequest> _createStreamedRequest(
SentryEnvelope envelope) async {
final streamedRequest = StreamedRequest('POST', _dsn.postUri);

if (_options.compressPayload) {
final compressionSink = compressInSink(streamedRequest.sink, _headers);
envelope
.envelopeStream(_options)
.listen(compressionSink.add)
.onDone(compressionSink.close);
} else {
envelope
.envelopeStream(_options)
.listen(streamedRequest.sink.add)
.onDone(streamedRequest.sink.close);
SentryId? _parseEventId(Response response) {
try {
final eventId = json.decode(response.body)['id'];
return eventId != null ? SentryId.fromId(eventId) : null;
} catch (e) {
_options.logger(SentryLevel.error, 'Error parsing response: $e');

Check warning on line 65 in dart/lib/src/transport/http_transport.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/transport/http_transport.dart#L65

Added line #L65 was not covered by tests
return null;
}
streamedRequest.headers.addAll(_credentialBuilder.configure(_headers));

return streamedRequest;
}

void _updateRetryAfterLimits(Response response) {
Expand All @@ -131,51 +82,3 @@
sentryRateLimitHeader, retryAfterHeader, response.statusCode);
}
}

class _CredentialBuilder {
final String _authHeader;

_CredentialBuilder._(String authHeader) : _authHeader = authHeader;

factory _CredentialBuilder(Dsn dsn, String sdkIdentifier) {
final authHeader = _buildAuthHeader(
publicKey: dsn.publicKey,
secretKey: dsn.secretKey,
sdkIdentifier: sdkIdentifier,
);

return _CredentialBuilder._(authHeader);
}

static String _buildAuthHeader({
required String publicKey,
String? secretKey,
required String sdkIdentifier,
}) {
var header = 'Sentry sentry_version=7, sentry_client=$sdkIdentifier, '
'sentry_key=$publicKey';

if (secretKey != null) {
header += ', sentry_secret=$secretKey';
}

return header;
}

Map<String, String> configure(Map<String, String> headers) {
return headers
..addAll(
<String, String>{'X-Sentry-Auth': _authHeader},
);
}
}

Map<String, String> _buildHeaders(bool isWeb, String sdkIdentifier) {
final headers = {'Content-Type': 'application/x-sentry-envelope'};
// NOTE(lejard_h) overriding user agent on VM and Flutter not sure why
// for web it use browser user agent
if (!isWeb) {
headers['User-Agent'] = sdkIdentifier;
}
return headers;
}
98 changes: 98 additions & 0 deletions dart/lib/src/transport/http_transport_request_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'dart:async';

import 'package:http/http.dart';
import 'package:meta/meta.dart';

import 'noop_encode.dart' if (dart.library.io) 'encode.dart';
import '../protocol.dart';
import '../sentry_options.dart';
import '../sentry_envelope.dart';

@internal
class HttpTransportRequestHandler {
final SentryOptions _options;
final Dsn _dsn;
final Map<String, String> _headers;
final Uri _requestUri;
late _CredentialBuilder _credentialBuilder;

HttpTransportRequestHandler(this._options, this._requestUri)
: _dsn = Dsn.parse(_options.dsn!),
_headers = _buildHeaders(
_options.platformChecker.isWeb,
_options.sentryClientName,
) {
_credentialBuilder = _CredentialBuilder(
_dsn,
_options.sentryClientName,
);
}

Future<StreamedRequest> createRequest(SentryEnvelope envelope) async {
final streamedRequest = StreamedRequest('POST', _requestUri);

if (_options.compressPayload) {
final compressionSink = compressInSink(streamedRequest.sink, _headers);
envelope
.envelopeStream(_options)
.listen(compressionSink.add)
.onDone(compressionSink.close);
} else {
envelope
.envelopeStream(_options)
.listen(streamedRequest.sink.add)
.onDone(streamedRequest.sink.close);
}

streamedRequest.headers.addAll(_credentialBuilder.configure(_headers));
return streamedRequest;
}
}

Map<String, String> _buildHeaders(bool isWeb, String sdkIdentifier) {
final headers = {'Content-Type': 'application/x-sentry-envelope'};
// NOTE(lejard_h) overriding user agent on VM and Flutter not sure why
// for web it use browser user agent
if (!isWeb) {
headers['User-Agent'] = sdkIdentifier;
}
return headers;
}

class _CredentialBuilder {
final String _authHeader;

_CredentialBuilder._(String authHeader) : _authHeader = authHeader;

factory _CredentialBuilder(Dsn dsn, String sdkIdentifier) {
final authHeader = _buildAuthHeader(
publicKey: dsn.publicKey,
secretKey: dsn.secretKey,
sdkIdentifier: sdkIdentifier,
);

return _CredentialBuilder._(authHeader);
}

static String _buildAuthHeader({
required String publicKey,
String? secretKey,
required String sdkIdentifier,
}) {
var header = 'Sentry sentry_version=7, sentry_client=$sdkIdentifier, '
'sentry_key=$publicKey';

if (secretKey != null) {
header += ', sentry_secret=$secretKey';
}

return header;
}

Map<String, String> configure(Map<String, String> headers) {
return headers
..addAll(
<String, String>{'X-Sentry-Auth': _authHeader},
);
}
}
Loading
Loading