Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.sentry.Breadcrumb
import io.sentry.DateUtils
import io.sentry.JsonObjectDeserializer
import io.sentry.JsonObjectReader
import io.sentry.ObjectReader
import io.sentry.ScopesAdapter
import io.sentry.Sentry
import io.sentry.SentryOptions
import io.sentry.android.core.InternalSentrySdk
import io.sentry.android.core.SentryAndroid
import io.sentry.android.core.SentryAndroidOptions
Expand All @@ -29,6 +33,7 @@ import io.sentry.protocol.User
import io.sentry.transport.CurrentDateProvider
import org.json.JSONObject
import org.json.JSONArray
import java.io.StringReader
import java.lang.ref.WeakReference
import kotlin.math.roundToInt

Expand Down Expand Up @@ -65,8 +70,6 @@ class SentryFlutterPlugin :
"setContexts" -> setContexts(call.argument("key"), call.argument("value"), result)
"removeContexts" -> removeContexts(call.argument("key"), result)
"setUser" -> setUser(call.argument("user"), result)
"addBreadcrumb" -> addBreadcrumb(call.argument("breadcrumb"), result)
"clearBreadcrumbs" -> clearBreadcrumbs(result)
"setExtra" -> setExtra(call.argument("key"), call.argument("value"), result)
"removeExtra" -> removeExtra(call.argument("key"), result)
"setTag" -> setTag(call.argument("key"), call.argument("value"), result)
Expand Down Expand Up @@ -190,24 +193,6 @@ class SentryFlutterPlugin :
result.success("")
}

private fun addBreadcrumb(
breadcrumb: Map<String, Any?>?,
result: Result,
) {
if (breadcrumb != null) {
val options = ScopesAdapter.getInstance().options
val breadcrumbInstance = Breadcrumb.fromMap(breadcrumb, options)
Sentry.addBreadcrumb(breadcrumbInstance)
}
result.success("")
}

private fun clearBreadcrumbs(result: Result) {
Sentry.clearBreadcrumbs()

result.success("")
}

private fun setExtra(
key: String?,
value: String?,
Expand Down Expand Up @@ -450,6 +435,22 @@ class SentryFlutterPlugin :
return json.toByteArray(Charsets.UTF_8)
}

@Suppress("unused") // Used by native/jni bindings
@JvmStatic
fun addBreadcrumbAsBytes(breadcrumbBytes: ByteArray) {
val logger = ScopesAdapter.getInstance().options.logger
val breadcrumbJson = breadcrumbBytes.toString(Charsets.UTF_8)
val reader = JsonObjectReader(StringReader(breadcrumbJson))
val breadcrumb = Breadcrumb.Deserializer().deserialize(reader, logger)
Sentry.addBreadcrumb(breadcrumb)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: JSON Handling Fails in addBreadcrumbAsBytes Method

The addBreadcrumbAsBytes method on Android doesn't gracefully handle malformed JSON input. Deserialization errors can lead to uncaught exceptions and app crashes, which goes against the PR's intent to log such issues rather than failing.

Fix in Cursor Fix in Web


@Suppress("unused") // Used by native/jni bindings
@JvmStatic
fun clearBreadcrumbs() {
Sentry.clearBreadcrumbs()
}

private fun List<DebugImage>?.serialize() = this?.map { it.serialize() }

private fun DebugImage.serialize() =
Expand Down
45 changes: 45 additions & 0 deletions packages/flutter/example/integration_test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,51 @@ void main() {
}
});

testWidgets('addBreadcrumb and clearBreadcrumbs sync to native',
(tester) async {
await restoreFlutterOnErrorAfter(() async {
await setupSentryAndApp(tester);
});

// 1. Add a breadcrumb via Dart
final testBreadcrumb = Breadcrumb(
message: 'test-breadcrumb-message',
category: 'test-category',
level: SentryLevel.info,
);
await Sentry.addBreadcrumb(testBreadcrumb);

// 2. Verify it appears in native via loadContexts
var contexts = await SentryFlutter.native?.loadContexts();
expect(contexts, isNotNull);

var breadcrumbs = contexts!['breadcrumbs'] as List<dynamic>?;
expect(breadcrumbs, isNotNull,
reason: 'Breadcrumbs should not be null after adding');
expect(breadcrumbs!.isNotEmpty, isTrue,
reason: 'Breadcrumbs should not be empty after adding');

// Find our test breadcrumb
final testCrumb = breadcrumbs.firstWhere(
(b) => b['message'] == 'test-breadcrumb-message',
orElse: () => null,
);
expect(testCrumb, isNotNull,
reason: 'Test breadcrumb should exist in native breadcrumbs');
expect(testCrumb['category'], equals('test-category'));

// 3. Clear breadcrumbs
await Sentry.configureScope((scope) async {
await scope.clearBreadcrumbs();
});

// 4. Verify they're cleared in native
contexts = await SentryFlutter.native?.loadContexts();
breadcrumbs = contexts!['breadcrumbs'] as List<dynamic>?;
expect(breadcrumbs == null || breadcrumbs.isEmpty, isTrue,
reason: 'Breadcrumbs should be null or empty after clearing');
});

testWidgets('loads debug images through loadDebugImages', (tester) async {
await restoreFlutterOnErrorAfter(() async {
await setupSentryAndApp(tester);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
let user = arguments?["user"] as? [String: Any]
setUser(user: user, result: result)

case "addBreadcrumb":
let arguments = call.arguments as? [String: Any?]
let breadcrumb = arguments?["breadcrumb"] as? [String: Any]
addBreadcrumb(breadcrumb: breadcrumb, result: result)

case "clearBreadcrumbs":
clearBreadcrumbs(result: result)

case "setExtra":
let arguments = call.arguments as? [String: Any?]
let key = arguments?["key"] as? String
Expand Down Expand Up @@ -322,22 +314,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
result("")
}

private func addBreadcrumb(breadcrumb: [String: Any]?, result: @escaping FlutterResult) {
if let breadcrumb = breadcrumb {
let breadcrumbInstance = PrivateSentrySDKOnly.breadcrumb(with: breadcrumb)
SentrySDK.addBreadcrumb(breadcrumbInstance)
}
result("")
}

private func clearBreadcrumbs(result: @escaping FlutterResult) {
SentrySDK.configureScope { scope in
scope.clearBreadcrumbs()

result("")
}
}

private func setExtra(key: String?, value: Any?, result: @escaping FlutterResult) {
guard let key = key else {
result("")
Expand Down Expand Up @@ -646,6 +622,22 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
}
return nil
}

@objc public class func addBreadcrumbAsBytes(_ breadcrumbBytes: NSData) {
guard let breadcrumbString = String(data: breadcrumbBytes as Data, encoding: .utf8),
let jsonData = breadcrumbString.data(using: .utf8),
let breadcrumbDict = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else {
return
}
let breadcrumbInstance = PrivateSentrySDKOnly.breadcrumb(with: breadcrumbDict)
SentrySDK.addBreadcrumb(breadcrumbInstance)
}

@objc public class func clearBreadcrumbs() {
SentrySDK.configureScope { scope in
scope.clearBreadcrumbs()
}
}
}
// swiftlint:enable type_body_length

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
+ (nullable NSData *)fetchNativeAppStartAsBytes;
+ (nullable NSData *)loadContextsAsBytes;
+ (nullable NSData *)loadDebugImagesAsBytes:(NSSet<NSString *> *)instructionAddresses;
+ (void)addBreadcrumbAsBytes:(NSData *)breadcrumbBytes;
+ (void)clearBreadcrumbs;
+ (void)nativeCrash;
+ (void)pauseAppHangTracking;
+ (void)resumeAppHangTracking;
Expand Down
16 changes: 15 additions & 1 deletion packages/flutter/lib/src/native/cocoa/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1128,7 +1128,9 @@ late final _sel_fetchNativeAppStartAsBytes =
late final _sel_loadContextsAsBytes = objc.registerName("loadContextsAsBytes");
late final _sel_loadDebugImagesAsBytes_ =
objc.registerName("loadDebugImagesAsBytes:");
late final _sel_nativeCrash = objc.registerName("nativeCrash");
late final _sel_addBreadcrumbAsBytes_ =
objc.registerName("addBreadcrumbAsBytes:");
late final _sel_clearBreadcrumbs = objc.registerName("clearBreadcrumbs");
final _objc_msgSend_1pl9qdv = objc.msgSendPointer
.cast<
ffi.NativeFunction<
Expand All @@ -1137,6 +1139,7 @@ final _objc_msgSend_1pl9qdv = objc.msgSendPointer
.asFunction<
void Function(
ffi.Pointer<objc.ObjCObject>, ffi.Pointer<objc.ObjCSelector>)>();
late final _sel_nativeCrash = objc.registerName("nativeCrash");
late final _sel_pauseAppHangTracking =
objc.registerName("pauseAppHangTracking");
late final _sel_resumeAppHangTracking =
Expand Down Expand Up @@ -1199,6 +1202,17 @@ class SentryFlutterPlugin extends objc.NSObject {
: objc.NSData.castFromPointer(_ret, retain: true, release: true);
}

/// addBreadcrumbAsBytes:
static void addBreadcrumbAsBytes(objc.NSData breadcrumbBytes) {
_objc_msgSend_xtuoz7(_class_SentryFlutterPlugin, _sel_addBreadcrumbAsBytes_,
breadcrumbBytes.ref.pointer);
}

/// clearBreadcrumbs
static void clearBreadcrumbs() {
_objc_msgSend_1pl9qdv(_class_SentryFlutterPlugin, _sel_clearBreadcrumbs);
}

/// nativeCrash
static void nativeCrash() {
_objc_msgSend_1pl9qdv(_class_SentryFlutterPlugin, _sel_nativeCrash);
Expand Down
32 changes: 32 additions & 0 deletions packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:objective_c/objective_c.dart';
Expand All @@ -7,6 +8,7 @@ import '../../../sentry_flutter.dart';
import '../../replay/replay_config.dart';
import '../native_app_start.dart';
import '../sentry_native_channel.dart';
import '../utils/data_normalizer.dart';
import '../utils/utf8_json.dart';
import 'binding.dart' as cocoa;
import 'cocoa_replay_recorder.dart';
Expand Down Expand Up @@ -197,4 +199,34 @@ class SentryNativeCocoa extends SentryNativeChannel {
cocoa.SentryFlutterPlugin.resumeAppHangTracking();
});
}

@override
Future<void> addBreadcrumb(Breadcrumb breadcrumb) async {
tryCatchSync('addBreadcrumb', () {
// Normalize breadcrumb data like the method channel does
final normalizedBreadcrumb = Breadcrumb(
message: breadcrumb.message,
category: breadcrumb.category,
data: normalizeMap(breadcrumb.data),
level: breadcrumb.level,
type: breadcrumb.type,
timestamp: breadcrumb.timestamp,
// ignore: invalid_use_of_internal_member
unknown: breadcrumb.unknown,
);

final jsonString = json.encode(normalizedBreadcrumb.toJson());
final bytes = utf8.encode(jsonString);
final nsData = bytes.toNSData();

cocoa.SentryFlutterPlugin.addBreadcrumbAsBytes(nsData);
});
}

@override
Future<void> clearBreadcrumbs() async {
tryCatchSync('clearBreadcrumbs', () {
cocoa.SentryFlutterPlugin.clearBreadcrumbs();
});
}
}
Loading
Loading