Skip to content

Commit

Permalink
Merge branch 'main' into fix/stacktrace-flutter-error
Browse files Browse the repository at this point in the history
  • Loading branch information
buenaflor authored Jul 10, 2024
2 parents 901c8ff + 3d305b9 commit e591c65
Show file tree
Hide file tree
Showing 15 changed files with 295 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/testflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # [email protected]
- run: xcodes select 15.0.1
- uses: ruby/setup-ruby@1d0e911f615a112e322369596f10ee0b95b010ae # pin@v1.183.0
- uses: ruby/setup-ruby@3a77c29278ae80936b4cb030fefc7d21c96c786f # pin@v1.185.0
with:
ruby-version: '2.7.5'
bundler-cache: true
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Features

- Add memory usage to contexts ([#2133](https:/getsentry/sentry-dart/pull/2133))
- Only for Linux/Windows applications, as iOS/Android/macOS use native SDKs

### Fixes

- Now captures meaningful stack traces when unhandled errors have empty or missing stack traces ([#2152](https:/getsentry/sentry-dart/pull/2152))
Expand Down Expand Up @@ -29,6 +34,9 @@
- Bump Cocoa SDK from v8.29.0 to v8.30.0 ([#2132](https:/getsentry/sentry-dart/pull/2132))
- [changelog](https:/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8300)
- [diff](https:/getsentry/sentry-cocoa/compare/8.29.0...8.30.0)
- Bump Android SDK from v7.10.0 to v7.11.0 ([#2144](https:/getsentry/sentry-dart/pull/2144))
- [changelog](https:/getsentry/sentry-java/blob/main/CHANGELOG.md#7110)
- [diff](https:/getsentry/sentry-java/compare/7.10.0...7.11.0)

## 8.3.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import '../../../sentry.dart';
import 'enricher_event_processor.dart';
import 'io_platform_memory.dart';

EnricherEventProcessor enricherEventProcessor(SentryOptions options) {
return IoEnricherEventProcessor(options);
Expand All @@ -17,25 +18,29 @@ class IoEnricherEventProcessor implements EnricherEventProcessor {

@override
SentryEvent? apply(SentryEvent event, Hint hint) {
// Amend app with current memory usage, as this is not available on native.
final app = _getApp(event.contexts.app);

// If there's a native integration available, it probably has better
// information available than Flutter.

final os = _options.platformChecker.hasNativeIntegration
? null
: _getOperatingSystem(event.contexts.operatingSystem);

final device = _options.platformChecker.hasNativeIntegration
? null
: _getDevice(event.contexts.device);

final os = _options.platformChecker.hasNativeIntegration
? null
: _getOperatingSystem(event.contexts.operatingSystem);

final culture = _options.platformChecker.hasNativeIntegration
? null
: _getSentryCulture(event.contexts.culture);

final contexts = event.contexts.copyWith(
operatingSystem: os,
device: device,
operatingSystem: os,
runtimes: _getRuntimes(event.contexts.runtimes),
app: app,
culture: culture,
);

Expand Down Expand Up @@ -97,9 +102,18 @@ class IoEnricherEventProcessor implements EnricherEventProcessor {
}

SentryDevice _getDevice(SentryDevice? device) {
final platformMemory = PlatformMemory(_options);
return (device ?? SentryDevice()).copyWith(
name: device?.name ?? Platform.localHostname,
processorCount: device?.processorCount ?? Platform.numberOfProcessors,
memorySize: device?.memorySize ?? platformMemory.getTotalPhysicalMemory(),
freeMemory: device?.freeMemory ?? platformMemory.getFreePhysicalMemory(),
);
}

SentryApp _getApp(SentryApp? app) {
return (app ?? SentryApp()).copyWith(
appMemory: app?.appMemory ?? ProcessInfo.currentRss,
);
}

Expand Down
108 changes: 108 additions & 0 deletions dart/lib/src/event_processor/enricher/io_platform_memory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'dart:io';

import '../../protocol.dart';
import '../../sentry_options.dart';

// Get total & free platform memory (in bytes) for linux and windows operating systems.
// Source: https:/onepub-dev/system_info/blob/8a9bf6b8eb7c86a09b3c3df4bf6d7fa5a6b50732/lib/src/platform/memory.dart
class PlatformMemory {
PlatformMemory(this.options);

final SentryOptions options;

int? getTotalPhysicalMemory() {
if (options.platformChecker.platform.isLinux) {
return _getLinuxMemInfoValue('MemTotal');
} else if (options.platformChecker.platform.isWindows) {
return _getWindowsWmicValue('ComputerSystem', 'TotalPhysicalMemory');
} else {
return null;
}
}

int? getFreePhysicalMemory() {
if (options.platformChecker.platform.isLinux) {
return _getLinuxMemInfoValue('MemFree');
} else if (options.platformChecker.platform.isWindows) {
return _getWindowsWmicValue('OS', 'FreePhysicalMemory');
} else {
return null;
}
}

int? _getWindowsWmicValue(String section, String key) {
final os = _wmicGetValueAsMap(section, [key]);
final totalPhysicalMemoryValue = os?[key];
if (totalPhysicalMemoryValue == null) {
return null;
}
final size = int.tryParse(totalPhysicalMemoryValue);
if (size == null) {
return null;
}
return size;
}

int? _getLinuxMemInfoValue(String key) {
final meminfoList = _exec('cat', ['/proc/meminfo'])
?.trim()
.replaceAll('\r\n', '\n')
.split('\n') ??
[];

final meminfoMap = _listToMap(meminfoList, ':');
final memsizeResults = meminfoMap[key]?.split(' ') ?? [];

if (memsizeResults.isEmpty) {
return null;
}
final memsizeResult = memsizeResults.first;

final memsize = int.tryParse(memsizeResult);
if (memsize == null) {
return null;
}
return memsize;
}

String? _exec(String executable, List<String> arguments,
{bool runInShell = false}) {
try {
final result =
Process.runSync(executable, arguments, runInShell: runInShell);
if (result.exitCode == 0) {
return result.stdout.toString();
}
} catch (e) {
options.logger(SentryLevel.warning, "Failed to run process: $e");
}
return null;
}

Map<String, String>? _wmicGetValueAsMap(String section, List<String> fields) {
final arguments = <String>[section];
arguments
..add('get')
..addAll(fields.join(', ').split(' '))
..add('/VALUE');

final list =
_exec('wmic', arguments)?.trim().replaceAll('\r\n', '\n').split('\n') ??
[];

return _listToMap(list, '=');
}

Map<String, String> _listToMap(List<String> list, String separator) {
final map = <String, String>{};
for (final string in list) {
final index = string.indexOf(separator);
if (index != -1) {
final key = string.substring(0, index).trim();
final value = string.substring(index + 1).trim();
map[key] = value;
}
}
return map;
}
}
17 changes: 16 additions & 1 deletion dart/lib/src/sentry_envelope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ import 'sentry_user_feedback.dart';

/// Class representation of `Envelope` file.
class SentryEnvelope {
SentryEnvelope(this.header, this.items);
SentryEnvelope(this.header, this.items,
{this.containsUnhandledException = false});

/// Header describing envelope content.
final SentryEnvelopeHeader header;

/// All items contained in the envelope.
final List<SentryEnvelopeItem> items;

/// Whether the envelope contains an unhandled exception.
/// This is used to determine if the native SDK should start a new session.
final bool containsUnhandledException;

/// Create a [SentryEnvelope] containing one [SentryEnvelopeItem] which holds the [SentryEvent] data.
factory SentryEnvelope.fromEvent(
SentryEvent event,
Expand All @@ -29,6 +34,15 @@ class SentryEnvelope {
SentryTraceContextHeader? traceContext,
List<SentryAttachment>? attachments,
}) {
bool containsUnhandledException = false;

if (event.exceptions != null && event.exceptions!.isNotEmpty) {
// Check all exceptions for any unhandled ones
containsUnhandledException = event.exceptions!.any((exception) {
return exception.mechanism?.handled == false;
});
}

return SentryEnvelope(
SentryEnvelopeHeader(
event.eventId,
Expand All @@ -41,6 +55,7 @@ class SentryEnvelope {
if (attachments != null)
...attachments.map((e) => SentryEnvelopeItem.fromAttachment(e))
],
containsUnhandledException: containsUnhandledException,
);
}

Expand Down
60 changes: 60 additions & 0 deletions dart/test/event_processor/enricher/io_platform_memory_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@TestOn('vm')
library dart_test;

import 'dart:io';

import 'package:sentry/sentry.dart';
import 'package:sentry/src/event_processor/enricher/io_platform_memory.dart';
import 'package:test/test.dart';

void main() {
late Fixture fixture;

setUp(() {
fixture = Fixture();
});

test('total physical memory', () {
final sut = fixture.getSut();
final totalPhysicalMemory = sut.getTotalPhysicalMemory();

switch (Platform.operatingSystem) {
case 'linux':
expect(totalPhysicalMemory, isNotNull);
expect(totalPhysicalMemory! > 0, true);
break;
case 'windows':
expect(totalPhysicalMemory, isNotNull);
expect(totalPhysicalMemory! > 0, true);
break;
default:
expect(totalPhysicalMemory, isNull);
}
});

test('free physical memory', () {
final sut = fixture.getSut();
final freePhysicalMemory = sut.getTotalPhysicalMemory();

switch (Platform.operatingSystem) {
case 'linux':
expect(freePhysicalMemory, isNotNull);
expect(freePhysicalMemory! > 0, true);
break;
case 'windows':
expect(freePhysicalMemory, isNotNull);
expect(freePhysicalMemory! > 0, true);
break;
default:
expect(freePhysicalMemory, isNull);
}
});
}

class Fixture {
var options = SentryOptions();

PlatformMemory getSut() {
return PlatformMemory(options);
}
}
3 changes: 3 additions & 0 deletions dart/test/mocks/mock_envelope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ class MockEnvelope implements SentryEnvelope {

@override
List<SentryEnvelopeItem> items = [];

@override
bool get containsUnhandledException => false;
}
2 changes: 1 addition & 1 deletion flutter/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ android {
}

dependencies {
api 'io.sentry:sentry-android:7.10.0'
api 'io.sentry:sentry-android:7.11.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

// Required -- JUnit 4 framework
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,9 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
val args = call.arguments() as List<Any>? ?: listOf()
if (args.isNotEmpty()) {
val event = args.first() as ByteArray?
if (event != null && event.isNotEmpty()) {
val id = InternalSentrySdk.captureEnvelope(event)
val containsUnhandledException = args[1] as Boolean
if (event != null && event.isNotEmpty() && containsUnhandledException != null) {
val id = InternalSentrySdk.captureEnvelope(event, containsUnhandledException)
if (id != null) {
result.success("")
} else {
Expand Down
3 changes: 2 additions & 1 deletion flutter/lib/src/file_system_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class FileSystemTransport implements Transport {
await envelope.envelopeStream(_options).forEach(envelopeData.addAll);
try {
// TODO avoid copy
await _native.captureEnvelope(Uint8List.fromList(envelopeData));
await _native.captureEnvelope(Uint8List.fromList(envelopeData),
envelope.containsUnhandledException);
} catch (exception, stackTrace) {
_options.logger(
SentryLevel.error,
Expand Down
3 changes: 2 additions & 1 deletion flutter/lib/src/native/sentry_native_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ abstract class SentryNativeBinding {

Future<NativeAppStart?> fetchNativeAppStart();

Future<void> captureEnvelope(Uint8List envelopeData);
Future<void> captureEnvelope(
Uint8List envelopeData, bool containsUnhandledException);

Future<void> beginNativeFrames();

Expand Down
7 changes: 5 additions & 2 deletions flutter/lib/src/native/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,11 @@ class SentryNativeChannel
}

@override
Future<void> captureEnvelope(Uint8List envelopeData) =>
_channel.invokeMethod('captureEnvelope', [envelopeData]);
Future<void> captureEnvelope(
Uint8List envelopeData, bool containsUnhandledException) {
return _channel.invokeMethod(
'captureEnvelope', [envelopeData, containsUnhandledException]);
}

@override
Future<Map<String, dynamic>?> loadContexts() =>
Expand Down
Loading

0 comments on commit e591c65

Please sign in to comment.