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: Replay support for mobile #2208

Merged
merged 16 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 82 additions & 21 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,30 @@

### Features

- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208)).

To try out replay, you can set following options (access is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/)):

```dart
await SentryFlutter.init(
(options) {
...
options.experimental.replay.sessionSampleRate = 1.0;
options.experimental.replay.errorSampleRate = 1.0;
},
appRunner: () => runApp(MyApp()),
);
```

- Add `SentryFlutter.nativeCrash()` using MethodChannels for Android and iOS ([#2239](https://github.com/getsentry/sentry-dart/pull/2239))
- This can be used to test if native crash reporting works
- Add `ignoreRoutes` parameter to `SentryNavigatorObserver`. ([#2218](https://github.com/getsentry/sentry-dart/pull/2218))
- This will ignore the Routes and prevent the Route from being pushed to the Sentry server.
- Ignored routes will also create no TTID and TTFD spans.
```dart
SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]),
```
- This will ignore the Routes and prevent the Route from being pushed to the Sentry server.
- Ignored routes will also create no TTID and TTFD spans.

```dart
SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]),
```

### Improvements

Expand All @@ -38,12 +54,33 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]),
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7140)
- [diff](https://github.com/getsentry/sentry-java/compare/7.13.0...7.14.0)

## 8.8.0-alpha.1

### Features

- iOS Session Replay Alpha ([#2209](https://github.com/getsentry/sentry-dart/pull/2209))
- Android replay touch tracking support ([#2228](https://github.com/getsentry/sentry-dart/pull/2228))
- Add `ignoreRoutes` parameter to `SentryNavigatorObserver`. ([#2218](https://github.com/getsentry/sentry-dart/pull/2218))
- This will ignore the Routes and prevent the Route from being pushed to the Sentry server.
- Ignored routes will also create no TTID and TTFD spans.

```dart
SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]),
```

### Dependencies

- Bump Android SDK from v7.13.0 to v7.14.0 ([#2228](https://github.com/getsentry/sentry-dart/pull/2228))
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7140)
- [diff](https://github.com/getsentry/sentry-java/compare/7.13.0...7.14.0)

## 8.7.0

### Features

- Add support for span level measurements. ([#2214](https://github.com/getsentry/sentry-dart/pull/2214))
- Add `ignoreTransactions` and `ignoreErrors` to options ([#2207](https://github.com/getsentry/sentry-dart/pull/2207))

```dart
await SentryFlutter.init(
(options) {
Expand All @@ -55,8 +92,10 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]),
appRunner: () => runApp(MyApp()),
);
```

- Add proxy support ([#2192](https://github.com/getsentry/sentry-dart/pull/2192))
- Configure a `SentryProxy` object and set it on `SentryFlutter.init`

```dart
import 'package:flutter/widgets.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Expand Down Expand Up @@ -96,24 +135,25 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]),
- This is enabled automatically and will change grouping if you already have issues with obfuscated titles
- If you want to disable this feature, set `enableExceptionTypeIdentification` to `false` in your Sentry options
- You can add your custom exception identifier if there are exceptions that we do not identify out of the box
```dart
// How to add your own custom exception identifier
class MyCustomExceptionIdentifier implements ExceptionIdentifier {
@override
String? identifyType(Exception exception) {
if (exception is MyCustomException) {
return 'MyCustomException';
}
if (exception is MyOtherCustomException) {
return 'MyOtherCustomException';

```dart
// How to add your own custom exception identifier
class MyCustomExceptionIdentifier implements ExceptionIdentifier {
@override
String? identifyType(Exception exception) {
if (exception is MyCustomException) {
return 'MyCustomException';
}
if (exception is MyOtherCustomException) {
return 'MyOtherCustomException';
}
return null;
}
return null;
}
}

SentryFlutter.init((options) =>
options..prependExceptionTypeIdentifier(MyCustomExceptionIdentifier()));
```
SentryFlutter.init((options) =>
options..prependExceptionTypeIdentifier(MyCustomExceptionIdentifier()));
```

### Deprecated

Expand All @@ -129,6 +169,27 @@ SentryFlutter.init((options) =>
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7130)
- [diff](https://github.com/getsentry/sentry-java/compare/7.12.0...7.13.0)

## 8.6.0-alpha.2

### Features

- Android Session Replay Alpha ([#2032](https://github.com/getsentry/sentry-dart/pull/2032))

To try out replay, you can set following options:

```dart
await SentryFlutter.init(
(options) {
...
options.experimental.replay.sessionSampleRate = 1.0;
options.experimental.replay.errorSampleRate = 1.0;
},
appRunner: () => runApp(MyApp()),
);
```

Access is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/)

## 8.5.0

### Features
Expand All @@ -141,7 +202,7 @@ SentryFlutter.init((options) =>
### Fixes

- Disable sff & frame delay detection on web, linux and windows ([#2182](https://github.com/getsentry/sentry-dart/pull/2182))
- Display refresh rate is locked at 60 for these platforms which can lead to inaccurate metrics
- Display refresh rate is locked at 60 for these platforms which can lead to inaccurate metrics

### Improvements

Expand Down
30 changes: 30 additions & 0 deletions dart/lib/src/protocol/breadcrumb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class Breadcrumb {
String? httpQuery,
String? httpFragment,
}) {
// The timestamp is used as the request-end time, so we need to set it right
// now and not rely on the default constructor.
timestamp ??= getUtcDateTime();

return Breadcrumb(
type: 'http',
category: 'http',
Expand All @@ -67,6 +71,11 @@ class Breadcrumb {
if (responseBodySize != null) 'response_body_size': responseBodySize,
if (httpQuery != null) 'http.query': httpQuery,
if (httpFragment != null) 'http.fragment': httpFragment,
if (requestDuration != null)
'start_timestamp':
timestamp.millisecondsSinceEpoch - requestDuration.inMilliseconds,
if (requestDuration != null)
'end_timestamp': timestamp.millisecondsSinceEpoch,
},
);
}
Expand Down Expand Up @@ -97,11 +106,32 @@ class Breadcrumb {
String? viewClass,
}) {
final newData = data ?? {};
var path = '';

if (viewId != null) {
newData['view.id'] = viewId;
path = viewId;
}

if (newData.containsKey('label')) {
if (path.isEmpty) {
path = newData['label'];
} else {
path = "$path, label: ${newData['label']}";
}
}

if (viewClass != null) {
newData['view.class'] = viewClass;
if (path.isEmpty) {
path = viewClass;
} else {
path = "$viewClass($path)";
}
}

if (path.isNotEmpty && !newData.containsKey('path')) {
newData['path'] = path;
}

return Breadcrumb(
Expand Down
17 changes: 13 additions & 4 deletions dart/lib/src/protocol/sentry_trace_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class SentryTraceContext {
/// Id of a parent span
final SpanId? parentSpanId;

/// Replay associated with this trace.
final SentryId? replayId;

/// Whether the span is sampled or not
final bool? sampled;

Expand Down Expand Up @@ -50,6 +53,9 @@ class SentryTraceContext {
? null
: SpanId.fromId(json['parent_span_id'] as String),
traceId: SentryId.fromId(json['trace_id'] as String),
replayId: json['replay_id'] == null
? null
: SentryId.fromId(json['replay_id'] as String),
description: json['description'] as String?,
status: json['status'] == null
? null
Expand All @@ -68,6 +74,7 @@ class SentryTraceContext {
'trace_id': traceId.toString(),
'op': operation,
if (parentSpanId != null) 'parent_span_id': parentSpanId!.toString(),
if (replayId != null) 'replay_id': replayId!.toString(),
if (description != null) 'description': description,
if (status != null) 'status': status!.toString(),
if (origin != null) 'origin': origin,
Expand All @@ -84,6 +91,7 @@ class SentryTraceContext {
sampled: sampled,
origin: origin,
unknown: unknown,
replayId: replayId,
);

SentryTraceContext({
Expand All @@ -96,16 +104,17 @@ class SentryTraceContext {
this.status,
this.origin,
this.unknown,
this.replayId,
}) : traceId = traceId ?? SentryId.newId(),
spanId = spanId ?? SpanId.newId();

@internal
factory SentryTraceContext.fromPropagationContext(
PropagationContext propagationContext) {
return SentryTraceContext(
traceId: propagationContext.traceId,
spanId: propagationContext.spanId,
operation: 'default',
);
traceId: propagationContext.traceId,
spanId: propagationContext.spanId,
operation: 'default',
replayId: propagationContext.baggage?.getReplayId());
}
}
11 changes: 10 additions & 1 deletion dart/lib/src/scope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ class Scope {
/// they must be JSON-serializable.
Map<String, dynamic> get extra => Map.unmodifiable(_extra);

/// Active replay recording.
@internal
SentryId? get replayId => _replayId;
@internal
set replayId(SentryId? value) => _replayId = value;
SentryId? _replayId;

final Contexts _contexts = Contexts();

/// Unmodifiable map of the scope contexts key/value
Expand Down Expand Up @@ -237,6 +244,7 @@ class Scope {
_tags.clear();
_extra.clear();
_eventProcessors.clear();
_replayId = null;

_clearBreadcrumbsSync();
_setUserSync(null);
Expand Down Expand Up @@ -429,7 +437,8 @@ class Scope {
..fingerprint = List.from(fingerprint)
.._transaction = _transaction
..span = span
.._enableScopeSync = false;
.._enableScopeSync = false
.._replayId = _replayId;

clone._setUserSync(user);

Expand Down
10 changes: 10 additions & 0 deletions dart/lib/src/sentry_baggage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ class SentryBaggage {
// ignore: deprecated_member_use_from_same_package
setUserSegment(scope.user!.segment!);
}
if (scope.replayId != null && scope.replayId != SentryId.empty()) {
setReplayId(scope.replayId.toString());
}
}

static Map<String, String> _extractKeyValuesFromBaggageString(
Expand Down Expand Up @@ -205,5 +208,12 @@ class SentryBaggage {
return double.tryParse(sampleRate);
}

void setReplayId(String value) => set('sentry-replay_id', value);

SentryId? getReplayId() {
final replayId = get('sentry-replay_id');
return replayId == null ? null : SentryId.fromId(replayId);
}

Map<String, String> get keyValues => Map.unmodifiable(_keyValues);
}
10 changes: 5 additions & 5 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,15 @@ class SentryClient {

var traceContext = scope?.span?.traceContext();
if (traceContext == null) {
if (scope?.propagationContext.baggage == null) {
scope?.propagationContext.baggage =
SentryBaggage({}, logger: _options.logger);
scope?.propagationContext.baggage?.setValuesFromScope(scope, _options);
}
if (scope != null) {
scope.propagationContext.baggage ??=
SentryBaggage({}, logger: _options.logger)
..setValuesFromScope(scope, _options);
traceContext = SentryTraceContextHeader.fromBaggage(
scope.propagationContext.baggage!);
}
} else {
traceContext.replayId = scope?.replayId;
}

final envelope = SentryEnvelope.fromEvent(
Expand Down
Loading
Loading