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: iOS replay support #2209

Merged
merged 120 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
294317b
minor gradle fixes
vaind Apr 3, 2024
861ebab
tmp: local sentry-java build
vaind Apr 3, 2024
2f55619
tmp: use relative path to sentry-java
vaind Apr 4, 2024
209ca41
tmp: local java build patches
vaind Apr 4, 2024
a282fad
replay options
vaind Apr 10, 2024
7d8d4e8
replay recorder
vaind Apr 11, 2024
c887e6d
Merge branch 'main' into feat/replay
vaind Apr 11, 2024
3444961
wip: JNI native bindings
vaind Apr 11, 2024
ca03c95
use compatible jnigen
vaind Apr 17, 2024
0b06dd5
add missing gradlew to flutter/android
vaind Apr 17, 2024
314665d
Merge branch 'feat/jni-ffi' into feat/replay
vaind Apr 17, 2024
8bf52d8
replay recorder JNI binding code
vaind Apr 17, 2024
a4e056a
replay recorder binding jni code
vaind Apr 17, 2024
1cde833
jni 0.6
vaind Apr 18, 2024
010f575
wip: android jni replay
vaind Apr 18, 2024
4c4f132
replay binding
vaind Apr 18, 2024
87d18e7
glue code for jni
vaind Apr 22, 2024
2eaaa28
Merge branch 'main' into feat/replay
vaind Apr 23, 2024
afa9f50
chore: update to cocoa 8.24.1-alpha.0
vaind Apr 23, 2024
3df2523
wip: cocoa integration
vaind Apr 24, 2024
179b5d4
wip: ios replay
vaind Apr 24, 2024
9284db8
Merge branch 'main' into feat/replay
vaind Apr 25, 2024
6432b98
cleanup
vaind Apr 25, 2024
25fd690
formatting
vaind Apr 25, 2024
658a132
android fixes
vaind Apr 26, 2024
fb8bbb4
move native setup to the native sdk integration
vaind Apr 26, 2024
7c8fd42
cleanup & improvements
vaind Apr 29, 2024
0570f35
improve widget filter and implement redact options
vaind Apr 29, 2024
5c08b21
fix image scaling
vaind Apr 29, 2024
c00788a
Merge branch 'main' into feat/replay
vaind May 2, 2024
0c55dc6
ktlint format
vaind May 2, 2024
f136795
ci fixes
vaind May 2, 2024
2aa129c
fix tests
vaind May 2, 2024
698276d
add jnigen scripts
vaind May 2, 2024
c6c3a17
use android 7.9.0 alpha.1
vaind May 2, 2024
cf8ed52
move native init & close to SentryNative
vaind May 2, 2024
55c7056
cleanup
vaind May 2, 2024
ff2a8ed
add macOS integration link
vaind May 6, 2024
46a96b0
Merge branch 'main' into feat/replay
vaind May 6, 2024
c3b60aa
rollback cocoa changes
vaind May 6, 2024
3f6d05e
remove jni/jnigen
vaind May 6, 2024
af22a59
wip: methodchannel based android recorder
vaind May 6, 2024
6bf1f00
callback
vaind May 6, 2024
9dc14aa
linter issues
vaind May 6, 2024
9daf297
Merge branch 'main' into feat/android-replay
vaind May 6, 2024
3639f00
minor fixes
vaind May 6, 2024
14ba742
more fixes
vaind May 6, 2024
585386e
linter issues
vaind May 6, 2024
ee1dbd6
cleanup
vaind May 6, 2024
266a85a
improve logging
vaind May 6, 2024
509c15f
move replay to experimental, same as in other SDKs
vaind May 7, 2024
960f2da
improve tree shaking
vaind May 7, 2024
b5c935f
Merge branch 'main' into feat/android-replay
vaind May 7, 2024
95e3a34
test: scheduler
vaind May 7, 2024
aa28200
support browser test
vaind May 7, 2024
16f3677
fix compat with old flutter
vaind May 7, 2024
86db5c4
cleanup
vaind May 7, 2024
942044a
rename recorder_widget_filter.dart
vaind May 7, 2024
9efae7b
fixup scheduler test
vaind May 7, 2024
35ed86b
improve test coverage
vaind May 7, 2024
63af017
pr cleanup
vaind May 7, 2024
12f5774
test: widget filter
vaind May 8, 2024
50a13f6
cleanup
vaind May 8, 2024
725fd02
test widget filter visibility
vaind May 8, 2024
4bda0ab
cleanup
vaind May 8, 2024
afb65f6
always add screenshot widget
vaind May 8, 2024
f6b9266
recorder test
vaind May 8, 2024
5dc1255
cleanup
vaind May 8, 2024
225c0c0
limit recorder test to vm
vaind May 8, 2024
46527a3
wip: integration test
vaind May 9, 2024
0bc8fff
cleanup
vaind May 9, 2024
571dfbc
Merge branch 'main' into feat/android-replay
vaind May 12, 2024
81f4689
ktlint format
vaind May 12, 2024
0f6764b
detekt suppression
vaind May 12, 2024
d35f630
ktlint format
vaind May 12, 2024
fee9580
improve scheduler stop behavior
vaind May 12, 2024
8be8d20
wip: error replay mapping
vaind May 13, 2024
c7b166d
Merge branch 'main' into feat/android-replay
vaind May 13, 2024
f3057cd
suppress detekt TooGenericExceptionThrown
vaind May 13, 2024
943acea
Update flutter/lib/src/replay/recorder.dart
vaind May 14, 2024
0d82f13
Update flutter/lib/src/native/java/sentry_native_java.dart
vaind May 14, 2024
49d4239
improve comments
vaind May 14, 2024
8ef5d15
Merge branch 'main' into feat/android-replay
vaind May 20, 2024
a93da0b
Merge branch 'main' into feat/android-replay
vaind May 28, 2024
114ed86
feat: associate dart errors with replays (#2070)
vaind Jun 13, 2024
5014d1c
Merge branch 'main' into feat/android-replay
vaind Jun 25, 2024
5baab7c
Merge branch 'main' into feat/android-replay
vaind Jun 27, 2024
c26a8a2
chote: remove path dependency
vaind Jun 27, 2024
09392a8
wip: ios replay
vaind Jul 2, 2024
6f9e741
fix result callback
vaind Jul 2, 2024
35cba3d
iOS related refactorings
vaind Jul 3, 2024
f88ce8b
logs
vaind Jul 4, 2024
e4c0654
Merge branch 'main' into feat/android-replay
vaind Jul 10, 2024
8919fff
Merge branch 'main' into feat/android-replay
vaind Jul 12, 2024
087d894
Merge branch 'feat/android-replay' into feat/ios-replay
vaind Jul 12, 2024
3f12988
fix tests
vaind Jul 12, 2024
1706c68
Merge branch 'main' into feat/android-replay
vaind Jul 12, 2024
320c245
Merge branch 'feat/android-replay' into feat/ios-replay
vaind Jul 15, 2024
cc0194a
call captureReplay on iOS & set
vaind Jul 15, 2024
f1c94a7
ios replay breadcrumbs
vaind Jul 15, 2024
5dc8bd6
feat: replay breadcrumbs (android) (#2163)
vaind Jul 17, 2024
26d7b9c
Merge branch 'main' into feat/android-replay
vaind Jul 17, 2024
8cd2d63
test: native replay integration binding (#2189)
vaind Jul 24, 2024
4883e14
Merge branch 'main' into feat/android-replay
vaind Jul 24, 2024
8bcde3d
Merge branch 'main' into feat/android-replay
vaind Jul 24, 2024
f1157fc
chore: update changelog
vaind Jul 24, 2024
93293f7
fix publishing
vaind Jul 24, 2024
87971db
release: 8.6.0-alpha.2
getsentry-bot Jul 24, 2024
3bf3cca
Merge branch 'release/8.6.0-alpha.2' into feat/android-replay
Jul 24, 2024
fb0dc4f
Merge branch 'feat/android-replay' into feat/ios-replay
vaind Jul 31, 2024
bf85993
Merge branch 'feat/replay' into feat/ios-replay
vaind Aug 1, 2024
51f3181
cleanup
vaind Aug 1, 2024
f54faee
Merge branch 'feat/replay' into feat/ios-replay
vaind Aug 3, 2024
17f5d9b
fix macos compilation
vaind Aug 3, 2024
7eaafe1
test: iOS support
vaind Aug 4, 2024
97b0b7b
linter issues
vaind Aug 4, 2024
e610e26
linter issues
vaind Aug 5, 2024
d496e49
Merge branch 'feat/replay' into feat/ios-replay
vaind Aug 5, 2024
cfefe5b
chore: update changelog
vaind Aug 5, 2024
87ef33a
Update flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
vaind Aug 6, 2024
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

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

## 8.6.0

### Improvements
Expand Down
8 changes: 8 additions & 0 deletions flutter/ios/Classes/SentryFlutter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ public final class SentryFlutter {
if let appHangTimeoutIntervalMillis = data["appHangTimeoutIntervalMillis"] as? NSNumber {
options.appHangTimeoutInterval = appHangTimeoutIntervalMillis.doubleValue / 1000
}
#if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS))
if let replayOptions = data["replay"] as? [String: Any] {
options.experimental.sessionReplay.sessionSampleRate =
(replayOptions["sessionSampleRate"] as? NSNumber)?.floatValue ?? 0
options.experimental.sessionReplay.errorSampleRate =
(replayOptions["errorSampleRate"] as? NSNumber)?.floatValue ?? 0
}
#endif
}

private func logLevelFrom(diagnosticLevel: String) -> SentryLevel {
Expand Down
25 changes: 23 additions & 2 deletions flutter/ios/Classes/SentryFlutterPluginApple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import CoreVideo

// swiftlint:disable:next type_body_length
public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
private let channel: FlutterMethodChannel

private static let nativeClientName = "sentry.cocoa.flutter"

Expand All @@ -38,12 +39,16 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
let channel = FlutterMethodChannel(name: "sentry_flutter", binaryMessenger: registrar.messenger)
#endif

let instance = SentryFlutterPluginApple()
let instance = SentryFlutterPluginApple(channel: channel)
instance.registerObserver()

registrar.addMethodCallDelegate(instance, channel: channel)
}

private init(channel: FlutterMethodChannel) {
self.channel = channel
super.init()
}

private lazy var sentryFlutter = SentryFlutter()

private func registerObserver() {
Expand Down Expand Up @@ -174,6 +179,14 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
case "resumeAppHangTracking":
resumeAppHangTracking(result)

case "sendReplayForEvent":
#if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|| os(tvOS)

we also support replay for tvos?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no (it's not even supported by Flutter officially AFAIK), the condition is exactly the same as in cocoa core code

PrivateSentrySDKOnly.captureReplay()
result(PrivateSentrySDKOnly.getReplayId())
#else
result(nil)
#endif

default:
result(FlutterMethodNotImplemented)
}
Expand Down Expand Up @@ -323,6 +336,14 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
didReceiveDidBecomeActiveNotification = false
}

#if canImport(UIKit) && !SENTRY_NO_UIKIT
#if os(iOS) || os(tvOS)
let breadcrumbConverter = SentryFlutterReplayBreadcrumbConverter()
let screenshotProvider = SentryFlutterReplayScreenshotProvider(channel: self.channel)
PrivateSentrySDKOnly.configureSessionReplay(with: breadcrumbConverter, screenshotProvider: screenshotProvider)
#endif
#endif

result("")
}

Expand Down
15 changes: 15 additions & 0 deletions flutter/ios/Classes/SentryFlutterReplayBreadcrumbConverter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@import Sentry;

#if SENTRY_TARGET_REPLAY_SUPPORTED
@class SentryRRWebEvent;

@interface SentryFlutterReplayBreadcrumbConverter
: NSObject <SentryReplayBreadcrumbConverter>

- (instancetype _Nonnull)init;

- (id<SentryRRWebEvent> _Nullable)convertFrom:
(SentryBreadcrumb *_Nonnull)breadcrumb;

@end
#endif
117 changes: 117 additions & 0 deletions flutter/ios/Classes/SentryFlutterReplayBreadcrumbConverter.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#import "SentryFlutterReplayBreadcrumbConverter.h"

@import Sentry;

#if SENTRY_TARGET_REPLAY_SUPPORTED

@implementation SentryFlutterReplayBreadcrumbConverter {
SentrySRDefaultBreadcrumbConverter *defaultConverter;
}

- (instancetype _Nonnull)init {
if (self = [super init]) {
self->defaultConverter =
[SentrySessionReplayIntegration createDefaultBreadcrumbConverter];
}
return self;
}

- (id<SentryRRWebEvent> _Nullable)convertFrom:
(SentryBreadcrumb *_Nonnull)breadcrumb {
assert(breadcrumb.timestamp != nil);

if (breadcrumb.category == nil
// Do not add Sentry Event breadcrumbs to replay
|| [breadcrumb.category isEqualToString:@"sentry.event"] ||
[breadcrumb.category isEqualToString:@"sentry.transaction"]) {
return nil;
}

if ([breadcrumb.category isEqualToString:@"http"]) {
return [self convertNetwork:breadcrumb];
}

if ([breadcrumb.category isEqualToString:@"navigation"]) {
return [self convertFrom:breadcrumb withCategory:nil andMessage:nil];
}

if ([breadcrumb.category isEqualToString:@"ui.click"]) {
return [self convertFrom:breadcrumb
withCategory:@"ui.tap"
andMessage:breadcrumb.data[@"path"]];
}

SentryRRWebEvent *nativeBreadcrumb =
[self->defaultConverter convertFrom:breadcrumb];

// ignore native navigation breadcrumbs
if (nativeBreadcrumb && nativeBreadcrumb.data &&
nativeBreadcrumb.data[@"payload"] &&
nativeBreadcrumb.data[@"payload"][@"category"] &&
[nativeBreadcrumb.data[@"payload"][@"category"]
isEqualToString:@"navigation"]) {
return nil;
}

return nativeBreadcrumb;
}

- (id<SentryRRWebEvent> _Nullable)convertFrom:
(SentryBreadcrumb *_Nonnull)breadcrumb
withCategory:(NSString *)category
andMessage:(NSString *)message {
return [SentrySessionReplayIntegration
createBreadcrumbwithTimestamp:breadcrumb.timestamp
category:category ?: breadcrumb.category
message:message ?: breadcrumb.message
level:breadcrumb.level
data:breadcrumb.data];
}

- (id<SentryRRWebEvent> _Nullable)convertNetwork:
(SentryBreadcrumb *_Nonnull)breadcrumb {
NSNumber *startTimestamp =
[breadcrumb.data[@"start_timestamp"] isKindOfClass:[NSNumber class]]
? breadcrumb.data[@"start_timestamp"]
: nil;
NSNumber *endTimestamp =
[breadcrumb.data[@"end_timestamp"] isKindOfClass:[NSNumber class]]
? breadcrumb.data[@"end_timestamp"]
: nil;
NSString *url = [breadcrumb.data[@"url"] isKindOfClass:[NSString class]]
? breadcrumb.data[@"url"]
: nil;

if (startTimestamp == nil || endTimestamp == nil || url == nil) {
return nil;
}

NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
if ([breadcrumb.data[@"method"] isKindOfClass:[NSString class]]) {
data[@"method"] = breadcrumb.data[@"method"];
}
if ([breadcrumb.data[@"status_code"] isKindOfClass:[NSNumber class]]) {
data[@"statusCode"] = breadcrumb.data[@"status_code"];
}
if ([breadcrumb.data[@"request_body_size"] isKindOfClass:[NSNumber class]]) {
data[@"requestBodySize"] = breadcrumb.data[@"request_body_size"];
}
if ([breadcrumb.data[@"response_body_size"] isKindOfClass:[NSNumber class]]) {
data[@"responseBodySize"] = breadcrumb.data[@"response_body_size"];
}

return [SentrySessionReplayIntegration
createNetworkBreadcrumbWithTimestamp:[self dateFrom:startTimestamp]
endTimestamp:[self dateFrom:endTimestamp]
operation:@"resource.http"
description:url
data:data];
}

- (NSDate *_Nonnull)dateFrom:(NSNumber *_Nonnull)timestamp {
return [NSDate dateWithTimeIntervalSince1970:(timestamp.doubleValue / 1000)];
}

@end

#endif
12 changes: 12 additions & 0 deletions flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import Sentry;

#if SENTRY_TARGET_REPLAY_SUPPORTED
@class SentryRRWebEvent;

@interface SentryFlutterReplayScreenshotProvider
: NSObject <SentryViewScreenshotProvider>

- (instancetype)initWithChannel:(id)FlutterMethodChannel;

@end
#endif
46 changes: 46 additions & 0 deletions flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@import Sentry;

#if SENTRY_TARGET_REPLAY_SUPPORTED
#import "SentryFlutterReplayScreenshotProvider.h"
#import <Flutter/Flutter.h>

@implementation SentryFlutterReplayScreenshotProvider {
FlutterMethodChannel *channel;
}

- (instancetype _Nonnull)initWithChannel:
(FlutterMethodChannel *_Nonnull)channel {
if (self = [super init]) {
self->channel = channel;
}
return self;
}

buenaflor marked this conversation as resolved.
Show resolved Hide resolved
- (void)imageWithView:(UIView *_Nonnull)view
options:(id<SentryRedactOptions> _Nonnull)options
onComplete:(void (^_Nonnull)(UIImage *_Nonnull))onComplete {
[self->channel
invokeMethod:@"captureReplayScreenshot"
arguments:@{@"replayId" : [PrivateSentrySDKOnly getReplayId]}
result:^(id value) {
if (value == nil) {
NSLog(@"SentryFlutterReplayScreenshotProvider received null "
@"result. "
@"Cannot capture a replay screenshot.");
} else if ([value
isKindOfClass:[FlutterStandardTypedData class]]) {
FlutterStandardTypedData *typedData =
(FlutterStandardTypedData *)value;
UIImage *image = [UIImage imageWithData:typedData.data];
onComplete(image);
} else {
NSLog(@"SentryFlutterReplayScreenshotProvider received an "
@"unexpected result. "
@"Cannot capture a replay screenshot.");
}
}];
}

@end

#endif
59 changes: 59 additions & 0 deletions flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,76 @@
import 'dart:ffi';
import 'dart:typed_data';
import 'dart:ui';

import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import '../../event_processor/replay_event_processor.dart';
import '../../replay/recorder.dart';
import '../../replay/recorder_config.dart';
import '../sentry_native_channel.dart';
import 'binding.dart' as cocoa;

@internal
class SentryNativeCocoa extends SentryNativeChannel {
late final _lib = cocoa.SentryCocoa(DynamicLibrary.process());
ScreenshotRecorder? _replayRecorder;
SentryId? _replayId;

SentryNativeCocoa(super.options, super.channel);

@override
Future<void> init(Hub hub) async {
// We only need these when replay is enabled (session or error capture)
// so let's set it up conditionally. This allows Dart to trim the code.
if (options.experimental.replay.isEnabled &&
options.platformChecker.platform.isIOS) {
// We only need the integration when error-replay capture is enabled.
if ((options.experimental.replay.errorSampleRate ?? 0) > 0) {
options.addEventProcessor(ReplayEventProcessor(this));
}

channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'captureReplayScreenshot':
_replayRecorder ??=
ScreenshotRecorder(ScreenshotRecorderConfig(), options);
final replayId =
SentryId.fromId(call.arguments['replayId'] as String);
if (_replayId != replayId) {
_replayId = replayId;
hub.configureScope((s) {
// ignore: invalid_use_of_internal_member
s.replayId = replayId;
});
}

Uint8List? imageBytes;
await _replayRecorder?.capture((image) async {
final imageData =
await image.toByteData(format: ImageByteFormat.png);
if (imageData != null) {
options.logger(
SentryLevel.debug,
'Replay: captured screenshot ('
'${image.width}x${image.height} pixels, '
'${imageData.lengthInBytes} bytes)');
imageBytes = imageData.buffer.asUint8List();
} else {
options.logger(SentryLevel.warning,

Check warning on line 60 in flutter/lib/src/native/cocoa/sentry_native_cocoa.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/cocoa/sentry_native_cocoa.dart#L60

Added line #L60 was not covered by tests
'Replay: failed to convert screenshot to PNG');
}
});
return imageBytes;
default:
throw UnimplementedError('Method ${call.method} not implemented');

Check warning on line 66 in flutter/lib/src/native/cocoa/sentry_native_cocoa.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/cocoa/sentry_native_cocoa.dart#L66

Added line #L66 was not covered by tests
}
});
}

return super.init(hub);
}

@override
int? startProfiler(SentryId traceId) => tryCatchSync('startProfiler', () {
final cSentryId = cocoa.SentryId1.alloc(_lib)
Expand Down
Loading
Loading