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

test: native replay integration binding #2189

Merged
merged 6 commits into from
Jul 24, 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
2 changes: 1 addition & 1 deletion flutter/lib/src/integrations/native_sdk_integration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class NativeSdkIntegration implements Integration<SentryFlutterOptions> {
}

try {
await _native.init(options);
await _native.init(hub);
options.sdk.addIntegration('nativeSdkIntegration');
} catch (exception, stackTrace) {
options.logger(
Expand Down
39 changes: 22 additions & 17 deletions flutter/lib/src/native/java/sentry_native_java.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:ui';

import 'package:meta/meta.dart';
Expand All @@ -14,16 +13,13 @@
@internal
class SentryNativeJava extends SentryNativeChannel {
ScreenshotRecorder? _replayRecorder;
late final SentryFlutterOptions _options;
SentryNativeJava(super.options, super.channel);

@override
Future<void> init(SentryFlutterOptions options) async {
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 = options;

// We only need the integration when error-replay capture is enabled.
if ((options.experimental.replay.errorSampleRate ?? 0) > 0) {
options.addEventProcessor(ReplayEventProcessor(this));
Expand All @@ -44,7 +40,7 @@
),
);

Sentry.configureScope((s) {
hub.configureScope((s) {
// ignore: invalid_use_of_internal_member
s.replayId = replayId;
});
Expand All @@ -54,7 +50,7 @@
await _replayRecorder?.stop();
_replayRecorder = null;

Sentry.configureScope((s) {
hub.configureScope((s) {
// ignore: invalid_use_of_internal_member
s.replayId = null;
});
Expand All @@ -72,7 +68,14 @@
});
}

return super.init(options);
return super.init(hub);
}

@override
Future<void> close() async {
await _replayRecorder?.stop();
_replayRecorder = null;
return super.close();
}

void _startRecorder(String cacheDir, ScreenshotRecorderConfig config) {
Expand All @@ -89,33 +92,35 @@
final timestamp = DateTime.now().millisecondsSinceEpoch;
final filePath = "$cacheDir/$timestamp.png";

_options.logger(
options.logger(
SentryLevel.debug,
'Replay: Saving screenshot to $filePath ('
'${image.width}x${image.height} pixels, '
'${imageData.lengthInBytes} bytes)');
await File(filePath).writeAsBytes(imageData.buffer.asUint8List());

try {
await options.fileSystem
.file(filePath)
.writeAsBytes(imageData.buffer.asUint8List(), flush: true);

await channel.invokeMethod(
'addReplayScreenshot',
{'path': filePath, 'timestamp': timestamp},
);
} catch (error, stackTrace) {
_options.logger(
options.logger(

Check warning on line 110 in flutter/lib/src/native/java/sentry_native_java.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/java/sentry_native_java.dart#L110

Added line #L110 was not covered by tests
SentryLevel.error,
'Native call `addReplayScreenshot` failed',
exception: error,
stackTrace: stackTrace,
);
// ignore: invalid_use_of_internal_member
if (options.automatedTestMode) {

Check warning on line 117 in flutter/lib/src/native/java/sentry_native_java.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/java/sentry_native_java.dart#L117

Added line #L117 was not covered by tests
rethrow;
}
}
}
};

_replayRecorder = ScreenshotRecorder(
config,
callback,
_options,
)..start();
_replayRecorder = ScreenshotRecorder(config, callback, options)..start();
}
}
2 changes: 1 addition & 1 deletion flutter/lib/src/native/sentry_native_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'native_frames.dart';
/// Provide typed methods to access native layer.
@internal
abstract class SentryNativeBinding {
Future<void> init(SentryFlutterOptions options);
Future<void> init(Hub hub);

Future<void> close();

Expand Down
3 changes: 1 addition & 2 deletions flutter/lib/src/native/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ class SentryNativeChannel
: channel = SentrySafeMethodChannel(channel, options);

@override
Future<void> init(SentryFlutterOptions options) async {
assert(this.options == options);
Future<void> init(Hub hub) async {
return channel.invokeMethod('initNativeSdk', <String, dynamic>{
'dsn': options.dsn,
'debug': options.debug,
Expand Down
8 changes: 7 additions & 1 deletion flutter/lib/src/replay/recorder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
final ScreenshotRecorderCallback _callback;
final SentryLogger _logger;
final SentryReplayOptions _options;
final bool rethrowExceptions;
WidgetFilter? _widgetFilter;
late final Scheduler _scheduler;
bool warningLogged = false;

ScreenshotRecorder(this._config, this._callback, SentryFlutterOptions options)
: _logger = options.logger,
_options = options.experimental.replay {
_options = options.experimental.replay,
// ignore: invalid_use_of_internal_member
rethrowExceptions = options.automatedTestMode {
final frameDuration = Duration(milliseconds: 1000 ~/ _config.frameRate);
_scheduler = Scheduler(frameDuration, _capture,
options.bindingUtils.instance!.addPostFrameCallback);
Expand Down Expand Up @@ -121,6 +124,9 @@
} catch (e, stackTrace) {
_logger(SentryLevel.error, "Replay: failed to capture screenshot.",
exception: e, stackTrace: stackTrace);
if (rethrowExceptions) {

Check warning on line 127 in flutter/lib/src/replay/recorder.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/replay/recorder.dart#L127

Added line #L127 was not covered by tests
rethrow;
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions flutter/lib/src/sentry_flutter_options.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'dart:async';

import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:meta/meta.dart' as meta;
import 'package:sentry/sentry.dart';
import 'package:flutter/widgets.dart';
Expand Down Expand Up @@ -329,6 +331,9 @@ class SentryFlutterOptions extends SentryOptions {
/// The [navigatorKey] is used to add information of the currently used locale to the contexts.
GlobalKey<NavigatorState>? navigatorKey;

@meta.internal
FileSystem fileSystem = LocalFileSystem();

/// Configuration of experimental features that may change or be removed
/// without prior notice. Additionally, these features may not be ready for
/// production use yet.
Expand Down
1 change: 1 addition & 0 deletions flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies:
package_info_plus: '>=1.0.0'
meta: ^1.3.0
ffi: ^2.0.0
file: '>=6.1.4'

dev_dependencies:
build_runner: ^2.4.2
Expand Down
5 changes: 3 additions & 2 deletions flutter/test/integrations/init_native_sdk_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:sentry_flutter/src/native/sentry_native_channel.dart';
import 'package:sentry_flutter/src/version.dart';

import '../mocks.dart';
import '../mocks.mocks.dart';

void main() {
late Fixture fixture;
Expand All @@ -25,7 +26,7 @@ void main() {
});
var sut = fixture.getSut(channel);

await sut.init(fixture.options);
await sut.init(MockHub());

channel.setMethodCallHandler(null);

Expand Down Expand Up @@ -115,7 +116,7 @@ void main() {
fixture.options.sdk.addIntegration('foo');
fixture.options.sdk.addPackage('bar', '1');

await sut.init(fixture.options);
await sut.init(MockHub());

channel.setMethodCallHandler(null);

Expand Down
2 changes: 1 addition & 1 deletion flutter/test/integrations/native_sdk_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ void main() {

class _ThrowingMockSentryNative extends MockSentryNativeBinding {
@override
Future<void> init(SentryFlutterOptions? options) async {
Future<void> init(Hub? hub) async {
throw Exception();
}
}
29 changes: 29 additions & 0 deletions flutter/test/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,32 @@ final fakeFrameDurations = [
Duration(milliseconds: 40),
Duration(milliseconds: 710),
];

@GenerateMocks([Callbacks])
abstract class Callbacks {
Future<Object?>? methodCallHandler(String method, [dynamic arguments]);
}

class NativeChannelFixture {
late final MethodChannel channel;
late final Future<Object?>? Function(String method, [dynamic arguments])
handler;
static TestDefaultBinaryMessenger get _messenger =>
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;

NativeChannelFixture() {
TestWidgetsFlutterBinding.ensureInitialized();
channel = MethodChannel('test.channel', StandardMethodCodec(), _messenger);
handler = MockCallbacks().methodCallHandler;
_messenger.setMockMethodCallHandler(
channel, (call) => handler(call.method, call.arguments));
}

// Mock this call as if it was invoked by the native side.
Future<ByteData?> invokeFromNative(String method, [dynamic arguments]) async {
final call =
StandardMethodCodec().encodeMethodCall(MethodCall(method, arguments));
return _messenger.handlePlatformMessage(
channel.name, call, (ByteData? data) {});
}
}
Loading
Loading