Skip to content

Commit be08ed1

Browse files
authored
feat: spotlight support (#1786)
* Implement spotlight support (screenshots are currently disabled and removed from the envelope)
1 parent 4be7ec8 commit be08ed1

13 files changed

+322
-118
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features
66

7+
- Add [Spotlight](https://spotlightjs.com/about/) support ([#1786](https://github.com/getsentry/sentry-dart/pull/1786))
8+
- Set `options.spotlight = Spotlight(enabled: true)` to enable Spotlight
79
- Add `ConnectivityIntegration` for web ([#1765](https://github.com/getsentry/sentry-dart/pull/1765))
810
- We only get the info if online/offline on web platform. The added breadcrumb is set to either `wifi` or `none`.
911
- APM for isar ([#1726](https://github.com/getsentry/sentry-dart/pull/1726))

dart/lib/sentry.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,5 @@ export 'src/utils/http_header_utils.dart';
5050
export 'src/sentry_trace_origins.dart';
5151
// ignore: invalid_export_of_internal_element
5252
export 'src/utils.dart';
53+
// spotlight debugging
54+
export 'src/spotlight.dart';

dart/lib/src/sentry_client.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'sentry_options.dart';
1616
import 'sentry_stack_trace_factory.dart';
1717
import 'transport/http_transport.dart';
1818
import 'transport/noop_transport.dart';
19+
import 'transport/spotlight_http_transport.dart';
1920
import 'utils/isolate_utils.dart';
2021
import 'version.dart';
2122
import 'sentry_envelope.dart';
@@ -49,6 +50,9 @@ class SentryClient {
4950
final rateLimiter = RateLimiter(options);
5051
options.transport = HttpTransport(options, rateLimiter);
5152
}
53+
if (options.spotlight.enabled) {
54+
options.transport = SpotlightHttpTransport(options, options.transport);
55+
}
5256
return SentryClient._(options);
5357
}
5458

dart/lib/src/sentry_options.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,13 @@ class SentryOptions {
374374
/// Settings this to `false` will set the `level` to [SentryLevel.error].
375375
bool markAutomaticallyCollectedErrorsAsFatal = true;
376376

377+
/// The Spotlight configuration.
378+
/// Disabled by default.
379+
/// ```dart
380+
/// spotlight = Spotlight(enabled: true)
381+
/// ```
382+
Spotlight spotlight = Spotlight(enabled: false);
383+
377384
SentryOptions({this.dsn, PlatformChecker? checker}) {
378385
if (checker != null) {
379386
platformChecker = checker;

dart/lib/src/spotlight.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import 'platform_checker.dart';
2+
3+
/// Spotlight configuration class.
4+
class Spotlight {
5+
/// Whether to enable Spotlight for local development.
6+
bool enabled;
7+
8+
/// The Spotlight Sidecar URL.
9+
/// Defaults to http://10.0.2.2:8969/stream due to Emulator on Android.
10+
/// Otherwise defaults to http://localhost:8969/stream.
11+
String url;
12+
13+
Spotlight({required this.enabled, String? url})
14+
: url = url ?? _defaultSpotlightUrl();
15+
}
16+
17+
String _defaultSpotlightUrl() {
18+
return (PlatformChecker().platform.isAndroid
19+
? 'http://10.0.2.2:8969/stream'
20+
: 'http://localhost:8969/stream');
21+
}

dart/lib/src/transport/http_transport.dart

Lines changed: 19 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import 'dart:async';
22
import 'dart:convert';
33

44
import 'package:http/http.dart';
5+
import '../utils/transport_utils.dart';
6+
import 'http_transport_request_handler.dart';
57

6-
import '../client_reports/client_report_recorder.dart';
7-
import '../client_reports/discard_reason.dart';
8-
import 'data_category.dart';
9-
import 'noop_encode.dart' if (dart.library.io) 'encode.dart';
108
import '../noop_client.dart';
119
import '../protocol.dart';
1210
import '../sentry_options.dart';
@@ -18,15 +16,9 @@ import 'rate_limiter.dart';
1816
class HttpTransport implements Transport {
1917
final SentryOptions _options;
2018

21-
final Dsn _dsn;
22-
2319
final RateLimiter _rateLimiter;
2420

25-
final ClientReportRecorder _recorder;
26-
27-
late _CredentialBuilder _credentialBuilder;
28-
29-
final Map<String, String> _headers;
21+
final HttpTransportRequestHandler _requestHandler;
3022

3123
factory HttpTransport(SentryOptions options, RateLimiter rateLimiter) {
3224
if (options.httpClient is NoOpClient) {
@@ -37,17 +29,8 @@ class HttpTransport implements Transport {
3729
}
3830

3931
HttpTransport._(this._options, this._rateLimiter)
40-
: _dsn = Dsn.parse(_options.dsn!),
41-
_recorder = _options.recorder,
42-
_headers = _buildHeaders(
43-
_options.platformChecker.isWeb,
44-
_options.sentryClientName,
45-
) {
46-
_credentialBuilder = _CredentialBuilder(
47-
_dsn,
48-
_options.sentryClientName,
49-
);
50-
}
32+
: _requestHandler = HttpTransportRequestHandler(
33+
_options, Dsn.parse(_options.dsn!).postUri);
5134

5235
@override
5336
Future<SentryId?> send(SentryEnvelope envelope) async {
@@ -57,63 +40,31 @@ class HttpTransport implements Transport {
5740
}
5841
filteredEnvelope.header.sentAt = _options.clock();
5942

60-
final streamedRequest = await _createStreamedRequest(filteredEnvelope);
43+
final streamedRequest =
44+
await _requestHandler.createRequest(filteredEnvelope);
45+
6146
final response = await _options.httpClient
6247
.send(streamedRequest)
6348
.then(Response.fromStream);
6449

6550
_updateRetryAfterLimits(response);
6651

67-
if (response.statusCode != 200) {
68-
// body guard to not log the error as it has performance impact to allocate
69-
// the body String.
70-
if (_options.debug) {
71-
_options.logger(
72-
SentryLevel.error,
73-
'API returned an error, statusCode = ${response.statusCode}, '
74-
'body = ${response.body}',
75-
);
76-
}
77-
78-
if (response.statusCode >= 400 && response.statusCode != 429) {
79-
_recorder.recordLostEvent(
80-
DiscardReason.networkError, DataCategory.error);
81-
}
82-
83-
return SentryId.empty();
84-
} else {
85-
_options.logger(
86-
SentryLevel.debug,
87-
'Envelope ${envelope.header.eventId ?? "--"} was sent successfully.',
88-
);
89-
}
52+
TransportUtils.logResponse(_options, envelope, response, target: 'Sentry');
9053

91-
final eventId = json.decode(response.body)['id'];
92-
if (eventId == null) {
93-
return null;
54+
if (response.statusCode == 200) {
55+
return _parseEventId(response);
9456
}
95-
return SentryId.fromId(eventId);
57+
return SentryId.empty();
9658
}
9759

98-
Future<StreamedRequest> _createStreamedRequest(
99-
SentryEnvelope envelope) async {
100-
final streamedRequest = StreamedRequest('POST', _dsn.postUri);
101-
102-
if (_options.compressPayload) {
103-
final compressionSink = compressInSink(streamedRequest.sink, _headers);
104-
envelope
105-
.envelopeStream(_options)
106-
.listen(compressionSink.add)
107-
.onDone(compressionSink.close);
108-
} else {
109-
envelope
110-
.envelopeStream(_options)
111-
.listen(streamedRequest.sink.add)
112-
.onDone(streamedRequest.sink.close);
60+
SentryId? _parseEventId(Response response) {
61+
try {
62+
final eventId = json.decode(response.body)['id'];
63+
return eventId != null ? SentryId.fromId(eventId) : null;
64+
} catch (e) {
65+
_options.logger(SentryLevel.error, 'Error parsing response: $e');
66+
return null;
11367
}
114-
streamedRequest.headers.addAll(_credentialBuilder.configure(_headers));
115-
116-
return streamedRequest;
11768
}
11869

11970
void _updateRetryAfterLimits(Response response) {
@@ -131,51 +82,3 @@ class HttpTransport implements Transport {
13182
sentryRateLimitHeader, retryAfterHeader, response.statusCode);
13283
}
13384
}
134-
135-
class _CredentialBuilder {
136-
final String _authHeader;
137-
138-
_CredentialBuilder._(String authHeader) : _authHeader = authHeader;
139-
140-
factory _CredentialBuilder(Dsn dsn, String sdkIdentifier) {
141-
final authHeader = _buildAuthHeader(
142-
publicKey: dsn.publicKey,
143-
secretKey: dsn.secretKey,
144-
sdkIdentifier: sdkIdentifier,
145-
);
146-
147-
return _CredentialBuilder._(authHeader);
148-
}
149-
150-
static String _buildAuthHeader({
151-
required String publicKey,
152-
String? secretKey,
153-
required String sdkIdentifier,
154-
}) {
155-
var header = 'Sentry sentry_version=7, sentry_client=$sdkIdentifier, '
156-
'sentry_key=$publicKey';
157-
158-
if (secretKey != null) {
159-
header += ', sentry_secret=$secretKey';
160-
}
161-
162-
return header;
163-
}
164-
165-
Map<String, String> configure(Map<String, String> headers) {
166-
return headers
167-
..addAll(
168-
<String, String>{'X-Sentry-Auth': _authHeader},
169-
);
170-
}
171-
}
172-
173-
Map<String, String> _buildHeaders(bool isWeb, String sdkIdentifier) {
174-
final headers = {'Content-Type': 'application/x-sentry-envelope'};
175-
// NOTE(lejard_h) overriding user agent on VM and Flutter not sure why
176-
// for web it use browser user agent
177-
if (!isWeb) {
178-
headers['User-Agent'] = sdkIdentifier;
179-
}
180-
return headers;
181-
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'dart:async';
2+
3+
import 'package:http/http.dart';
4+
import 'package:meta/meta.dart';
5+
6+
import 'noop_encode.dart' if (dart.library.io) 'encode.dart';
7+
import '../protocol.dart';
8+
import '../sentry_options.dart';
9+
import '../sentry_envelope.dart';
10+
11+
@internal
12+
class HttpTransportRequestHandler {
13+
final SentryOptions _options;
14+
final Dsn _dsn;
15+
final Map<String, String> _headers;
16+
final Uri _requestUri;
17+
late _CredentialBuilder _credentialBuilder;
18+
19+
HttpTransportRequestHandler(this._options, this._requestUri)
20+
: _dsn = Dsn.parse(_options.dsn!),
21+
_headers = _buildHeaders(
22+
_options.platformChecker.isWeb,
23+
_options.sentryClientName,
24+
) {
25+
_credentialBuilder = _CredentialBuilder(
26+
_dsn,
27+
_options.sentryClientName,
28+
);
29+
}
30+
31+
Future<StreamedRequest> createRequest(SentryEnvelope envelope) async {
32+
final streamedRequest = StreamedRequest('POST', _requestUri);
33+
34+
if (_options.compressPayload) {
35+
final compressionSink = compressInSink(streamedRequest.sink, _headers);
36+
envelope
37+
.envelopeStream(_options)
38+
.listen(compressionSink.add)
39+
.onDone(compressionSink.close);
40+
} else {
41+
envelope
42+
.envelopeStream(_options)
43+
.listen(streamedRequest.sink.add)
44+
.onDone(streamedRequest.sink.close);
45+
}
46+
47+
streamedRequest.headers.addAll(_credentialBuilder.configure(_headers));
48+
return streamedRequest;
49+
}
50+
}
51+
52+
Map<String, String> _buildHeaders(bool isWeb, String sdkIdentifier) {
53+
final headers = {'Content-Type': 'application/x-sentry-envelope'};
54+
// NOTE(lejard_h) overriding user agent on VM and Flutter not sure why
55+
// for web it use browser user agent
56+
if (!isWeb) {
57+
headers['User-Agent'] = sdkIdentifier;
58+
}
59+
return headers;
60+
}
61+
62+
class _CredentialBuilder {
63+
final String _authHeader;
64+
65+
_CredentialBuilder._(String authHeader) : _authHeader = authHeader;
66+
67+
factory _CredentialBuilder(Dsn dsn, String sdkIdentifier) {
68+
final authHeader = _buildAuthHeader(
69+
publicKey: dsn.publicKey,
70+
secretKey: dsn.secretKey,
71+
sdkIdentifier: sdkIdentifier,
72+
);
73+
74+
return _CredentialBuilder._(authHeader);
75+
}
76+
77+
static String _buildAuthHeader({
78+
required String publicKey,
79+
String? secretKey,
80+
required String sdkIdentifier,
81+
}) {
82+
var header = 'Sentry sentry_version=7, sentry_client=$sdkIdentifier, '
83+
'sentry_key=$publicKey';
84+
85+
if (secretKey != null) {
86+
header += ', sentry_secret=$secretKey';
87+
}
88+
89+
return header;
90+
}
91+
92+
Map<String, String> configure(Map<String, String> headers) {
93+
return headers
94+
..addAll(
95+
<String, String>{'X-Sentry-Auth': _authHeader},
96+
);
97+
}
98+
}

0 commit comments

Comments
 (0)