diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e03a61858..63c5a2721b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* Fix: Fix `SentryAssetBundle` on Flutter >= 3.1 (#877) * Feat: Add Android thread to platform stacktraces (#853) * Fix: Rename auto initialize property (#857) * Bump: Sentry-Android to 6.0.0-beta.4 (#871) diff --git a/flutter/lib/src/sentry_asset_bundle.dart b/flutter/lib/src/sentry_asset_bundle.dart index 640d24fe63..e20f33b16a 100644 --- a/flutter/lib/src/sentry_asset_bundle.dart +++ b/flutter/lib/src/sentry_asset_bundle.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; @@ -143,6 +145,8 @@ class SentryAssetBundle implements AssetBundle { byteLength = data.length; } else if (data is ByteData) { byteLength = data.lengthInBytes; + } else if (data is ImmutableBuffer) { + byteLength = data.length; } if (byteLength != null) { span?.setData('file.size', byteLength); @@ -177,6 +181,49 @@ class SentryAssetBundle implements AssetBundle { } } + @override + // This is an override on Flutter greater than 3.1 + // ignore: override_on_non_overriding_member + Future loadBuffer(String key) async { + final span = _hub.getSpan()?.startChild( + 'file.read', + description: 'AssetBundle.loadBuffer: ${_fileName(key)}', + ); + + span?.setData('file.path', key); + + ImmutableBuffer data; + try { + data = await _loadBuffer(key); + _setDataLength(data, span); + span?.status = SpanStatus.ok(); + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + rethrow; + } finally { + await span?.finish(); + } + return data; + } + + Future _loadBuffer(String key) async { + try { + return (_bundle as dynamic).loadBuffer(key); + } on NoSuchMethodError catch (_) { + // The loadBuffer method exists as of Flutter greater than 3.1 + // 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.1 we can't forward this call and + // just catch the error which is thrown. On later version the call gets + // correctly forwarded. + // + // In case of a NoSuchMethodError we just return an empty list + return ImmutableBuffer.fromUint8List(Uint8List.fromList([])); + } + } + static Future _wrapParsing( _Parser parser, String value, diff --git a/flutter/test/sentry_asset_bundle_test.dart b/flutter/test/sentry_asset_bundle_test.dart index a0d51ee58e..e2197b643d 100644 --- a/flutter/test/sentry_asset_bundle_test.dart +++ b/flutter/test/sentry_asset_bundle_test.dart @@ -2,6 +2,7 @@ // The lint above is okay, because we're using another Sentry package import 'dart:convert'; import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -137,6 +138,53 @@ void main() { expect(span.context.description, 'AssetBundle.loadString: test.txt'); }); + test('loadBuffer: creates a span if transaction is bound to scope', + () async { + final sut = fixture.getSut(); + final tr = fixture._hub.startTransaction( + 'name', + 'op', + bindToScope: true, + ); + + await sut.loadBuffer(_testFileName); + + await tr.finish(); + + final tracer = (tr as SentryTracer); + final span = tracer.children.first; + + expect(span.status, SpanStatus.ok()); + expect(span.finished, true); + expect(span.context.operation, 'file.read'); + expect(span.data['file.path'], 'resources/test.txt'); + expect(span.data['file.size'], 12); + expect(span.context.description, 'AssetBundle.loadBuffer: test.txt'); + }); + + test('loadBuffer: end span with error if exception is thrown', () async { + final sut = fixture.getSut(throwException: true); + final tr = fixture._hub.startTransaction( + 'name', + 'op', + bindToScope: true, + ); + + try { + await sut.loadBuffer(_testFileName); + } catch (_) {} + + await tr.finish(); + + final tracer = (tr as SentryTracer); + final span = tracer.children.first; + + expect(span.status, SpanStatus.internalError()); + expect(span.finished, true); + expect(span.context.operation, 'file.read'); + expect(span.context.description, 'AssetBundle.loadBuffer: test.txt'); + }); + test( 'loadStructuredData: does not create any spans and just forwords the call to the underlying assetbundle if disabled', () async { @@ -359,4 +407,18 @@ class TestAssetBundle extends CachingAssetBundle { super.evict(key); evictKey = key; } + + @override + // This is an override on Flutter greater than 3.1 + // ignore: override_on_non_overriding_member + Future loadBuffer(String key) async { + if (throwException) { + throw Exception('exception thrown for testing purposes'); + } + if (key == _testFileName) { + return ImmutableBuffer.fromUint8List( + Uint8List.fromList(utf8.encode('Hello World!'))); + } + return ImmutableBuffer.fromUint8List(Uint8List.fromList([])); + } }