Skip to content

Commit

Permalink
feat: spotlight support (#1786)
Browse files Browse the repository at this point in the history
* Implement spotlight support (screenshots are currently disabled and removed from the envelope)
  • Loading branch information
buenaflor authored Dec 27, 2023
1 parent 4be7ec8 commit be08ed1
Show file tree
Hide file tree
Showing 13 changed files with 322 additions and 118 deletions.
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);
}
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);

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:async';
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 @@ import 'rate_limiter.dart';
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 @@ class HttpTransport implements Transport {
}

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 @@ class HttpTransport implements Transport {
}
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');
return null;
}
streamedRequest.headers.addAll(_credentialBuilder.configure(_headers));

return streamedRequest;
}

void _updateRetryAfterLimits(Response response) {
Expand All @@ -131,51 +82,3 @@ class HttpTransport implements Transport {
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

0 comments on commit be08ed1

Please sign in to comment.