Skip to content

Implement loadStructuredBinaryData from updated AssetBundle #1272

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

Merged
merged 4 commits into from
Feb 28, 2023
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Implement `loadStructuredBinaryData` from updated AssetBundle ([#1272](https://github.com/getsentry/sentry-dart/pull/1272))

### Dependencies

- Bump Android SDK from v6.13.0 to v6.13.1 ([#1273](https://github.com/getsentry/sentry-dart/pull/1273))
Expand Down
104 changes: 100 additions & 4 deletions flutter/lib/src/sentry_asset_bundle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:sentry/sentry.dart';

typedef _Parser<T> = Future<T> Function(String value);
typedef _StringParser<T> = Future<T> Function(String value);
typedef _ByteParser<T> = FutureOr<T> Function(ByteData value);

/// An [AssetBundle] which creates automatic performance traces for loading
/// assets.
Expand Down Expand Up @@ -79,15 +80,15 @@ class SentryAssetBundle implements AssetBundle {
}

@override
Future<T> loadStructuredData<T>(String key, _Parser<T> parser) {
Future<T> loadStructuredData<T>(String key, _StringParser<T> parser) {
if (_enableStructuredDataTracing) {
return _loadStructuredDataWithTracing(key, parser);
}
return _bundle.loadStructuredData(key, parser);
}

Future<T> _loadStructuredDataWithTracing<T>(
String key, _Parser<T> parser) async {
String key, _StringParser<T> parser) async {
final span = _hub.getSpan()?.startChild(
'file.read',
description: 'AssetBundle.loadStructuredData<$T>: ${_fileName(key)}',
Expand Down Expand Up @@ -125,6 +126,46 @@ class SentryAssetBundle implements AssetBundle {
return data;
}

FutureOr<T> _loadStructuredBinaryDataWithTracing<T>(
String key, _ByteParser<T> parser) async {
final span = _hub.getSpan()?.startChild(
'file.read',
description:
'AssetBundle.loadStructuredBinaryData<$T>: ${_fileName(key)}',
);
span?.setData('file.path', key);

final completer = Completer<T>();

// This future is intentionally not awaited. Otherwise we deadlock with
// the completer.
// ignore: unawaited_futures
runZonedGuarded(() async {
final data = await _loadStructuredBinaryDataWrapper(
key,
(value) async => await _wrapBinaryParsing(parser, value, key, span),
);
span?.status = SpanStatus.ok();
completer.complete(data);
}, (exception, stackTrace) {
completer.completeError(exception, stackTrace);
});

T data;
try {
data = await completer.future;
_setDataLength(data, span);
span?.status = const SpanStatus.ok();
} catch (e) {
span?.throwable = e;
span?.status = const SpanStatus.internalError();
rethrow;
} finally {
await span?.finish();
}
return data;
}

@override
Future<String> loadString(String key, {bool cache = true}) async {
final span = _hub.getSpan()?.startChild(
Expand Down Expand Up @@ -236,7 +277,7 @@ class SentryAssetBundle implements AssetBundle {
}

static Future<T> _wrapParsing<T>(
_Parser<T> parser,
_StringParser<T> parser,
String value,
String key,
ISentrySpan? outerSpan,
Expand All @@ -259,4 +300,59 @@ class SentryAssetBundle implements AssetBundle {

return data;
}

static FutureOr<T> _wrapBinaryParsing<T>(
_ByteParser<T> parser,
ByteData value,
String key,
ISentrySpan? outerSpan,
) async {
final span = outerSpan?.startChild(
'serialize.file.read',
description: 'parsing "$key" to "$T"',
);
T data;
try {
data = await parser(value);
span?.status = const SpanStatus.ok();
} catch (e) {
span?.throwable = e;
span?.status = const SpanStatus.internalError();
rethrow;
} finally {
await span?.finish();
}

return data;
}

@override
// ignore: override_on_non_overriding_member
Future<T> loadStructuredBinaryData<T>(
String key,
FutureOr<T> Function(ByteData data) parser,
) async {
if (_enableStructuredDataTracing) {
return _loadStructuredBinaryDataWithTracing<T>(key, parser);
}

return _loadStructuredBinaryDataWrapper<T>(key, parser);
}

// helper method to have a "typesafe" method
Future<T> _loadStructuredBinaryDataWrapper<T>(
String key,
FutureOr<T> Function(ByteData data) parser,
) async {
// The loadStructuredBinaryData method exists as of Flutter greater than 3.8
// Previous versions don't have it, but later versions do.
// We can't use `extends` in order to provide this method because this is
// a wrapper and thus the method call must be forwarded.
// On Flutter versions <=3.8 we can't forward this call.
// On later version the call gets correctly forwarded.
// The error doesn't need to handled since it can't be called on earlier versions,
// and it's correctly forwarded on later versions.
return (_bundle as dynamic).loadStructuredBinaryData<T>(key, parser)
as Future<T>;
}
}
167 changes: 167 additions & 0 deletions flutter/test/sentry_asset_bundle_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// ignore_for_file: invalid_use_of_internal_member
// The lint above is okay, because we're using another Sentry package
import 'dart:async';
import 'dart:convert';
// backcompatibility for Flutter < 3.3
// ignore: unnecessary_import
Expand Down Expand Up @@ -333,6 +334,158 @@ void main() {
},
);

test(
'loadStructuredBinaryData: does not create any spans and just forwords the call to the underlying assetbundle if disabled',
() async {
final sut = fixture.getSut(structuredDataTracing: false);
final tr = fixture._hub.startTransaction(
'name',
'op',
bindToScope: true,
);

final data = await sut.loadStructuredBinaryData<String>(
_testFileName,
(value) async => utf8.decode(
value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes),
),
);
expect(data, 'Hello World!');

await tr.finish();

final tracer = (tr as SentryTracer);

expect(tracer.children.length, 0);
},
);

test(
'loadStructuredBinaryData: finish with errored span if loading fails',
() async {
final sut = fixture.getSut(throwException: true);
final tr = fixture._hub.startTransaction(
'name',
'op',
bindToScope: true,
);
await expectLater(
sut.loadStructuredBinaryData<String>(
_testFileName,
(value) async => utf8.decode(
value.buffer
.asUint8List(value.offsetInBytes, value.lengthInBytes),
),
),
throwsA(isA<Exception>()),
);

await tr.finish();

final tracer = (tr as SentryTracer);
final span = tracer.children.first;

expect(span.status, SpanStatus.internalError());
expect(span.finished, true);
expect(span.throwable, isA<Exception>());
expect(span.context.operation, 'file.read');
expect(
span.context.description,
'AssetBundle.loadStructuredBinaryData<String>: test.txt',
);
},
);

test(
'loadStructuredBinaryData: finish with errored span if parsing fails',
() async {
final sut = fixture.getSut(throwException: false);
final tr = fixture._hub.startTransaction(
'name',
'op',
bindToScope: true,
);
await expectLater(
sut.loadStructuredBinaryData<String>(
_testFileName,
(value) async => throw Exception('error while parsing'),
),
throwsA(isA<Exception>()),
);

await tr.finish();

final tracer = (tr as SentryTracer);
var span = tracer.children.first;

expect(tracer.children.length, 2);

expect(span.status, SpanStatus.internalError());
expect(span.finished, true);
expect(span.throwable, isA<Exception>());
expect(span.context.operation, 'file.read');
expect(
span.context.description,
'AssetBundle.loadStructuredBinaryData<String>: test.txt',
);

span = tracer.children[1];

expect(span.status, SpanStatus.internalError());
expect(span.finished, true);
expect(span.throwable, isA<Exception>());
expect(span.context.operation, 'serialize.file.read');
expect(
span.context.description,
'parsing "resources/test.txt" to "String"',
);
},
);

test(
'loadStructuredBinaryData: finish with successfully',
() async {
final sut = fixture.getSut(throwException: false);
final tr = fixture._hub.startTransaction(
'name',
'op',
bindToScope: true,
);

await sut.loadStructuredBinaryData<String>(
_testFileName,
(value) async => utf8.decode(
value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes),
),
);

await tr.finish();

final tracer = (tr as SentryTracer);
var span = tracer.children.first;

expect(tracer.children.length, 2);

expect(span.status, SpanStatus.ok());
expect(span.finished, true);
expect(span.context.operation, 'file.read');
expect(
span.context.description,
'AssetBundle.loadStructuredBinaryData<String>: test.txt',
);

span = tracer.children[1];

expect(span.status, SpanStatus.ok());
expect(span.finished, true);
expect(span.context.operation, 'serialize.file.read');
expect(
span.context.description,
'parsing "resources/test.txt" to "String"',
);
},
);

test(
'evict call gets forwarded',
() {
Expand Down Expand Up @@ -393,6 +546,20 @@ class TestAssetBundle extends CachingAssetBundle {
bool throwException = false;
String? evictKey;

@override
// ignore: override_on_non_overriding_member
Future<T> loadStructuredBinaryData<T>(
String key, FutureOr<T> Function(ByteData data) parser) async {
if (throwException) {
throw Exception('exception thrown for testing purposes');
}
if (key == _testFileName) {
return parser(ByteData.view(
Uint8List.fromList(utf8.encode('Hello World!')).buffer));
}
return parser(ByteData(0));
}

@override
Future<ByteData> load(String key) async {
if (throwException) {
Expand Down