Skip to content

Commit 2d3b03d

Browse files
authored
Implement loadStructuredBinaryData from updated AssetBundle (#1272)
1 parent d301b11 commit 2d3b03d

File tree

3 files changed

+271
-4
lines changed

3 files changed

+271
-4
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Implement `loadStructuredBinaryData` from updated AssetBundle ([#1272](https://github.com/getsentry/sentry-dart/pull/1272))
8+
59
### Dependencies
610

711
- Bump Android SDK from v6.13.0 to v6.13.1 ([#1273](https://github.com/getsentry/sentry-dart/pull/1273))

flutter/lib/src/sentry_asset_bundle.dart

+100-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import 'package:flutter/services.dart';
99
import 'package:flutter/material.dart';
1010
import 'package:sentry/sentry.dart';
1111

12-
typedef _Parser<T> = Future<T> Function(String value);
12+
typedef _StringParser<T> = Future<T> Function(String value);
13+
typedef _ByteParser<T> = FutureOr<T> Function(ByteData value);
1314

1415
/// An [AssetBundle] which creates automatic performance traces for loading
1516
/// assets.
@@ -79,15 +80,15 @@ class SentryAssetBundle implements AssetBundle {
7980
}
8081

8182
@override
82-
Future<T> loadStructuredData<T>(String key, _Parser<T> parser) {
83+
Future<T> loadStructuredData<T>(String key, _StringParser<T> parser) {
8384
if (_enableStructuredDataTracing) {
8485
return _loadStructuredDataWithTracing(key, parser);
8586
}
8687
return _bundle.loadStructuredData(key, parser);
8788
}
8889

8990
Future<T> _loadStructuredDataWithTracing<T>(
90-
String key, _Parser<T> parser) async {
91+
String key, _StringParser<T> parser) async {
9192
final span = _hub.getSpan()?.startChild(
9293
'file.read',
9394
description: 'AssetBundle.loadStructuredData<$T>: ${_fileName(key)}',
@@ -125,6 +126,46 @@ class SentryAssetBundle implements AssetBundle {
125126
return data;
126127
}
127128

129+
FutureOr<T> _loadStructuredBinaryDataWithTracing<T>(
130+
String key, _ByteParser<T> parser) async {
131+
final span = _hub.getSpan()?.startChild(
132+
'file.read',
133+
description:
134+
'AssetBundle.loadStructuredBinaryData<$T>: ${_fileName(key)}',
135+
);
136+
span?.setData('file.path', key);
137+
138+
final completer = Completer<T>();
139+
140+
// This future is intentionally not awaited. Otherwise we deadlock with
141+
// the completer.
142+
// ignore: unawaited_futures
143+
runZonedGuarded(() async {
144+
final data = await _loadStructuredBinaryDataWrapper(
145+
key,
146+
(value) async => await _wrapBinaryParsing(parser, value, key, span),
147+
);
148+
span?.status = SpanStatus.ok();
149+
completer.complete(data);
150+
}, (exception, stackTrace) {
151+
completer.completeError(exception, stackTrace);
152+
});
153+
154+
T data;
155+
try {
156+
data = await completer.future;
157+
_setDataLength(data, span);
158+
span?.status = const SpanStatus.ok();
159+
} catch (e) {
160+
span?.throwable = e;
161+
span?.status = const SpanStatus.internalError();
162+
rethrow;
163+
} finally {
164+
await span?.finish();
165+
}
166+
return data;
167+
}
168+
128169
@override
129170
Future<String> loadString(String key, {bool cache = true}) async {
130171
final span = _hub.getSpan()?.startChild(
@@ -236,7 +277,7 @@ class SentryAssetBundle implements AssetBundle {
236277
}
237278

238279
static Future<T> _wrapParsing<T>(
239-
_Parser<T> parser,
280+
_StringParser<T> parser,
240281
String value,
241282
String key,
242283
ISentrySpan? outerSpan,
@@ -259,4 +300,59 @@ class SentryAssetBundle implements AssetBundle {
259300

260301
return data;
261302
}
303+
304+
static FutureOr<T> _wrapBinaryParsing<T>(
305+
_ByteParser<T> parser,
306+
ByteData value,
307+
String key,
308+
ISentrySpan? outerSpan,
309+
) async {
310+
final span = outerSpan?.startChild(
311+
'serialize.file.read',
312+
description: 'parsing "$key" to "$T"',
313+
);
314+
T data;
315+
try {
316+
data = await parser(value);
317+
span?.status = const SpanStatus.ok();
318+
} catch (e) {
319+
span?.throwable = e;
320+
span?.status = const SpanStatus.internalError();
321+
rethrow;
322+
} finally {
323+
await span?.finish();
324+
}
325+
326+
return data;
327+
}
328+
329+
@override
330+
// ignore: override_on_non_overriding_member
331+
Future<T> loadStructuredBinaryData<T>(
332+
String key,
333+
FutureOr<T> Function(ByteData data) parser,
334+
) async {
335+
if (_enableStructuredDataTracing) {
336+
return _loadStructuredBinaryDataWithTracing<T>(key, parser);
337+
}
338+
339+
return _loadStructuredBinaryDataWrapper<T>(key, parser);
340+
}
341+
342+
// helper method to have a "typesafe" method
343+
Future<T> _loadStructuredBinaryDataWrapper<T>(
344+
String key,
345+
FutureOr<T> Function(ByteData data) parser,
346+
) async {
347+
// The loadStructuredBinaryData method exists as of Flutter greater than 3.8
348+
// Previous versions don't have it, but later versions do.
349+
// We can't use `extends` in order to provide this method because this is
350+
// a wrapper and thus the method call must be forwarded.
351+
// On Flutter versions <=3.8 we can't forward this call.
352+
// On later version the call gets correctly forwarded.
353+
// The error doesn't need to handled since it can't be called on earlier versions,
354+
// and it's correctly forwarded on later versions.
355+
return (_bundle as dynamic).loadStructuredBinaryData<T>(key, parser)
356+
as Future<T>;
357+
}
262358
}

flutter/test/sentry_asset_bundle_test.dart

+167
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// ignore_for_file: invalid_use_of_internal_member
22
// The lint above is okay, because we're using another Sentry package
3+
import 'dart:async';
34
import 'dart:convert';
45
// backcompatibility for Flutter < 3.3
56
// ignore: unnecessary_import
@@ -333,6 +334,158 @@ void main() {
333334
},
334335
);
335336

337+
test(
338+
'loadStructuredBinaryData: does not create any spans and just forwords the call to the underlying assetbundle if disabled',
339+
() async {
340+
final sut = fixture.getSut(structuredDataTracing: false);
341+
final tr = fixture._hub.startTransaction(
342+
'name',
343+
'op',
344+
bindToScope: true,
345+
);
346+
347+
final data = await sut.loadStructuredBinaryData<String>(
348+
_testFileName,
349+
(value) async => utf8.decode(
350+
value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes),
351+
),
352+
);
353+
expect(data, 'Hello World!');
354+
355+
await tr.finish();
356+
357+
final tracer = (tr as SentryTracer);
358+
359+
expect(tracer.children.length, 0);
360+
},
361+
);
362+
363+
test(
364+
'loadStructuredBinaryData: finish with errored span if loading fails',
365+
() async {
366+
final sut = fixture.getSut(throwException: true);
367+
final tr = fixture._hub.startTransaction(
368+
'name',
369+
'op',
370+
bindToScope: true,
371+
);
372+
await expectLater(
373+
sut.loadStructuredBinaryData<String>(
374+
_testFileName,
375+
(value) async => utf8.decode(
376+
value.buffer
377+
.asUint8List(value.offsetInBytes, value.lengthInBytes),
378+
),
379+
),
380+
throwsA(isA<Exception>()),
381+
);
382+
383+
await tr.finish();
384+
385+
final tracer = (tr as SentryTracer);
386+
final span = tracer.children.first;
387+
388+
expect(span.status, SpanStatus.internalError());
389+
expect(span.finished, true);
390+
expect(span.throwable, isA<Exception>());
391+
expect(span.context.operation, 'file.read');
392+
expect(
393+
span.context.description,
394+
'AssetBundle.loadStructuredBinaryData<String>: test.txt',
395+
);
396+
},
397+
);
398+
399+
test(
400+
'loadStructuredBinaryData: finish with errored span if parsing fails',
401+
() async {
402+
final sut = fixture.getSut(throwException: false);
403+
final tr = fixture._hub.startTransaction(
404+
'name',
405+
'op',
406+
bindToScope: true,
407+
);
408+
await expectLater(
409+
sut.loadStructuredBinaryData<String>(
410+
_testFileName,
411+
(value) async => throw Exception('error while parsing'),
412+
),
413+
throwsA(isA<Exception>()),
414+
);
415+
416+
await tr.finish();
417+
418+
final tracer = (tr as SentryTracer);
419+
var span = tracer.children.first;
420+
421+
expect(tracer.children.length, 2);
422+
423+
expect(span.status, SpanStatus.internalError());
424+
expect(span.finished, true);
425+
expect(span.throwable, isA<Exception>());
426+
expect(span.context.operation, 'file.read');
427+
expect(
428+
span.context.description,
429+
'AssetBundle.loadStructuredBinaryData<String>: test.txt',
430+
);
431+
432+
span = tracer.children[1];
433+
434+
expect(span.status, SpanStatus.internalError());
435+
expect(span.finished, true);
436+
expect(span.throwable, isA<Exception>());
437+
expect(span.context.operation, 'serialize.file.read');
438+
expect(
439+
span.context.description,
440+
'parsing "resources/test.txt" to "String"',
441+
);
442+
},
443+
);
444+
445+
test(
446+
'loadStructuredBinaryData: finish with successfully',
447+
() async {
448+
final sut = fixture.getSut(throwException: false);
449+
final tr = fixture._hub.startTransaction(
450+
'name',
451+
'op',
452+
bindToScope: true,
453+
);
454+
455+
await sut.loadStructuredBinaryData<String>(
456+
_testFileName,
457+
(value) async => utf8.decode(
458+
value.buffer.asUint8List(value.offsetInBytes, value.lengthInBytes),
459+
),
460+
);
461+
462+
await tr.finish();
463+
464+
final tracer = (tr as SentryTracer);
465+
var span = tracer.children.first;
466+
467+
expect(tracer.children.length, 2);
468+
469+
expect(span.status, SpanStatus.ok());
470+
expect(span.finished, true);
471+
expect(span.context.operation, 'file.read');
472+
expect(
473+
span.context.description,
474+
'AssetBundle.loadStructuredBinaryData<String>: test.txt',
475+
);
476+
477+
span = tracer.children[1];
478+
479+
expect(span.status, SpanStatus.ok());
480+
expect(span.finished, true);
481+
expect(span.context.operation, 'serialize.file.read');
482+
expect(
483+
span.context.description,
484+
'parsing "resources/test.txt" to "String"',
485+
);
486+
},
487+
);
488+
336489
test(
337490
'evict call gets forwarded',
338491
() {
@@ -393,6 +546,20 @@ class TestAssetBundle extends CachingAssetBundle {
393546
bool throwException = false;
394547
String? evictKey;
395548

549+
@override
550+
// ignore: override_on_non_overriding_member
551+
Future<T> loadStructuredBinaryData<T>(
552+
String key, FutureOr<T> Function(ByteData data) parser) async {
553+
if (throwException) {
554+
throw Exception('exception thrown for testing purposes');
555+
}
556+
if (key == _testFileName) {
557+
return parser(ByteData.view(
558+
Uint8List.fromList(utf8.encode('Hello World!')).buffer));
559+
}
560+
return parser(ByteData(0));
561+
}
562+
396563
@override
397564
Future<ByteData> load(String key) async {
398565
if (throwException) {

0 commit comments

Comments
 (0)