Skip to content

Commit 2d42aac

Browse files
authored
fix(flutter): Avoid JNI callbacks for Android scope sync and beforeSend (#3676)
* fix(flutter): Avoid JNI callbacks for Android scope sync Moves Android beforeSend tagging and context sync updates from Dart-backed JNI callbacks to Kotlin static methods. This avoids long-lived or short-lived Java→Dart proxy callbacks in the affected scope-sync path while preserving existing event origin/environment tagging and context behavior. * fix lint issues
1 parent d0371bb commit 2d42aac

4 files changed

Lines changed: 233 additions & 74 deletions

File tree

packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import io.sentry.Breadcrumb
1717
import io.sentry.DateUtils
1818
import io.sentry.ScopesAdapter
1919
import io.sentry.Sentry
20+
import io.sentry.SentryOptions
2021
import io.sentry.SentryOptions.Proxy
2122
import io.sentry.android.core.BuildConfig
2223
import io.sentry.android.core.InternalSentrySdk
@@ -135,6 +136,50 @@ class SentryFlutterPlugin :
135136
@JvmStatic
136137
fun privateSentryGetReplayIntegration(): ReplayIntegration? = replay
137138

139+
@Suppress("unused") // Used by native/jni bindings
140+
@JvmStatic
141+
fun setupBeforeSend(options: SentryAndroidOptions) {
142+
options.beforeSend =
143+
SentryOptions.BeforeSendCallback { event, _ ->
144+
when (event.sdk?.name) {
145+
"sentry.dart.flutter" -> {
146+
event.setTag("event.origin", "flutter")
147+
event.setTag("event.environment", "dart")
148+
}
149+
150+
"sentry.java.android.flutter" -> {
151+
event.setTag("event.origin", "android")
152+
event.setTag("event.environment", "java")
153+
}
154+
155+
"sentry.native.android.flutter" -> {
156+
event.setTag("event.origin", "android")
157+
event.setTag("event.environment", "native")
158+
}
159+
}
160+
event
161+
}
162+
}
163+
164+
@Suppress("unused") // Used by native/jni bindings
165+
@JvmStatic
166+
fun setContext(
167+
key: String,
168+
value: Any?,
169+
) {
170+
Sentry.configureScope { scope ->
171+
scope.setContexts(key, value)
172+
}
173+
}
174+
175+
@Suppress("unused") // Used by native/jni bindings
176+
@JvmStatic
177+
fun removeContext(key: String) {
178+
Sentry.configureScope { scope ->
179+
scope.removeContexts(key)
180+
}
181+
}
182+
138183
@JvmStatic
139184
fun setupReplay(
140185
options: SentryAndroidOptions,

packages/flutter/lib/src/native/java/binding.dart

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4599,6 +4599,94 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject {
45994599
.object<ReplayIntegration?>(const $ReplayIntegration$NullableType());
46004600
}
46014601

4602+
static final _id_setupBeforeSend = _class.instanceMethodId(
4603+
r'setupBeforeSend',
4604+
r'(Lio/sentry/android/core/SentryAndroidOptions;)V',
4605+
);
4606+
4607+
static final _setupBeforeSend = jni$_.ProtectedJniExtensions.lookup<
4608+
jni$_.NativeFunction<
4609+
jni$_.JThrowablePtr Function(
4610+
jni$_.Pointer<jni$_.Void>,
4611+
jni$_.JMethodIDPtr,
4612+
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>)>>(
4613+
'globalEnv_CallVoidMethod')
4614+
.asFunction<
4615+
jni$_.JThrowablePtr Function(jni$_.Pointer<jni$_.Void>,
4616+
jni$_.JMethodIDPtr, jni$_.Pointer<jni$_.Void>)>();
4617+
4618+
/// from: `public final void setupBeforeSend(io.sentry.android.core.SentryAndroidOptions sentryAndroidOptions)`
4619+
void setupBeforeSend(
4620+
SentryAndroidOptions sentryAndroidOptions,
4621+
) {
4622+
final _$sentryAndroidOptions = sentryAndroidOptions.reference;
4623+
_setupBeforeSend(
4624+
reference.pointer,
4625+
_id_setupBeforeSend as jni$_.JMethodIDPtr,
4626+
_$sentryAndroidOptions.pointer)
4627+
.check();
4628+
}
4629+
4630+
static final _id_setContext = _class.instanceMethodId(
4631+
r'setContext',
4632+
r'(Ljava/lang/String;Ljava/lang/Object;)V',
4633+
);
4634+
4635+
static final _setContext = jni$_.ProtectedJniExtensions.lookup<
4636+
jni$_.NativeFunction<
4637+
jni$_.JThrowablePtr Function(
4638+
jni$_.Pointer<jni$_.Void>,
4639+
jni$_.JMethodIDPtr,
4640+
jni$_.VarArgs<
4641+
(
4642+
jni$_.Pointer<jni$_.Void>,
4643+
jni$_.Pointer<jni$_.Void>
4644+
)>)>>('globalEnv_CallVoidMethod')
4645+
.asFunction<
4646+
jni$_.JThrowablePtr Function(
4647+
jni$_.Pointer<jni$_.Void>,
4648+
jni$_.JMethodIDPtr,
4649+
jni$_.Pointer<jni$_.Void>,
4650+
jni$_.Pointer<jni$_.Void>)>();
4651+
4652+
/// from: `public final void setContext(java.lang.String string, java.lang.Object object)`
4653+
void setContext(
4654+
jni$_.JString string,
4655+
jni$_.JObject? object,
4656+
) {
4657+
final _$string = string.reference;
4658+
final _$object = object?.reference ?? jni$_.jNullReference;
4659+
_setContext(reference.pointer, _id_setContext as jni$_.JMethodIDPtr,
4660+
_$string.pointer, _$object.pointer)
4661+
.check();
4662+
}
4663+
4664+
static final _id_removeContext = _class.instanceMethodId(
4665+
r'removeContext',
4666+
r'(Ljava/lang/String;)V',
4667+
);
4668+
4669+
static final _removeContext = jni$_.ProtectedJniExtensions.lookup<
4670+
jni$_.NativeFunction<
4671+
jni$_.JThrowablePtr Function(
4672+
jni$_.Pointer<jni$_.Void>,
4673+
jni$_.JMethodIDPtr,
4674+
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>)>>(
4675+
'globalEnv_CallVoidMethod')
4676+
.asFunction<
4677+
jni$_.JThrowablePtr Function(jni$_.Pointer<jni$_.Void>,
4678+
jni$_.JMethodIDPtr, jni$_.Pointer<jni$_.Void>)>();
4679+
4680+
/// from: `public final void removeContext(java.lang.String string)`
4681+
void removeContext(
4682+
jni$_.JString string,
4683+
) {
4684+
final _$string = string.reference;
4685+
_removeContext(reference.pointer, _id_removeContext as jni$_.JMethodIDPtr,
4686+
_$string.pointer)
4687+
.check();
4688+
}
4689+
46024690
static final _id_setupReplay = _class.instanceMethodId(
46034691
r'setupReplay',
46044692
r'(Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/flutter/ReplayRecorderCallbacks;)V',
@@ -5167,6 +5255,94 @@ class SentryFlutterPlugin extends jni$_.JObject {
51675255
.object<ReplayIntegration?>(const $ReplayIntegration$NullableType());
51685256
}
51695257

5258+
static final _id_setupBeforeSend = _class.staticMethodId(
5259+
r'setupBeforeSend',
5260+
r'(Lio/sentry/android/core/SentryAndroidOptions;)V',
5261+
);
5262+
5263+
static final _setupBeforeSend = jni$_.ProtectedJniExtensions.lookup<
5264+
jni$_.NativeFunction<
5265+
jni$_.JThrowablePtr Function(
5266+
jni$_.Pointer<jni$_.Void>,
5267+
jni$_.JMethodIDPtr,
5268+
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>)>>(
5269+
'globalEnv_CallStaticVoidMethod')
5270+
.asFunction<
5271+
jni$_.JThrowablePtr Function(jni$_.Pointer<jni$_.Void>,
5272+
jni$_.JMethodIDPtr, jni$_.Pointer<jni$_.Void>)>();
5273+
5274+
/// from: `static public final void setupBeforeSend(io.sentry.android.core.SentryAndroidOptions sentryAndroidOptions)`
5275+
static void setupBeforeSend(
5276+
SentryAndroidOptions sentryAndroidOptions,
5277+
) {
5278+
final _$sentryAndroidOptions = sentryAndroidOptions.reference;
5279+
_setupBeforeSend(
5280+
_class.reference.pointer,
5281+
_id_setupBeforeSend as jni$_.JMethodIDPtr,
5282+
_$sentryAndroidOptions.pointer)
5283+
.check();
5284+
}
5285+
5286+
static final _id_setContext = _class.staticMethodId(
5287+
r'setContext',
5288+
r'(Ljava/lang/String;Ljava/lang/Object;)V',
5289+
);
5290+
5291+
static final _setContext = jni$_.ProtectedJniExtensions.lookup<
5292+
jni$_.NativeFunction<
5293+
jni$_.JThrowablePtr Function(
5294+
jni$_.Pointer<jni$_.Void>,
5295+
jni$_.JMethodIDPtr,
5296+
jni$_.VarArgs<
5297+
(
5298+
jni$_.Pointer<jni$_.Void>,
5299+
jni$_.Pointer<jni$_.Void>
5300+
)>)>>('globalEnv_CallStaticVoidMethod')
5301+
.asFunction<
5302+
jni$_.JThrowablePtr Function(
5303+
jni$_.Pointer<jni$_.Void>,
5304+
jni$_.JMethodIDPtr,
5305+
jni$_.Pointer<jni$_.Void>,
5306+
jni$_.Pointer<jni$_.Void>)>();
5307+
5308+
/// from: `static public final void setContext(java.lang.String string, java.lang.Object object)`
5309+
static void setContext(
5310+
jni$_.JString string,
5311+
jni$_.JObject? object,
5312+
) {
5313+
final _$string = string.reference;
5314+
final _$object = object?.reference ?? jni$_.jNullReference;
5315+
_setContext(_class.reference.pointer, _id_setContext as jni$_.JMethodIDPtr,
5316+
_$string.pointer, _$object.pointer)
5317+
.check();
5318+
}
5319+
5320+
static final _id_removeContext = _class.staticMethodId(
5321+
r'removeContext',
5322+
r'(Ljava/lang/String;)V',
5323+
);
5324+
5325+
static final _removeContext = jni$_.ProtectedJniExtensions.lookup<
5326+
jni$_.NativeFunction<
5327+
jni$_.JThrowablePtr Function(
5328+
jni$_.Pointer<jni$_.Void>,
5329+
jni$_.JMethodIDPtr,
5330+
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>)>>(
5331+
'globalEnv_CallStaticVoidMethod')
5332+
.asFunction<
5333+
jni$_.JThrowablePtr Function(jni$_.Pointer<jni$_.Void>,
5334+
jni$_.JMethodIDPtr, jni$_.Pointer<jni$_.Void>)>();
5335+
5336+
/// from: `static public final void removeContext(java.lang.String string)`
5337+
static void removeContext(
5338+
jni$_.JString string,
5339+
) {
5340+
final _$string = string.reference;
5341+
_removeContext(_class.reference.pointer,
5342+
_id_removeContext as jni$_.JMethodIDPtr, _$string.pointer)
5343+
.check();
5344+
}
5345+
51705346
static final _id_setupReplay = _class.staticMethodId(
51715347
r'setupReplay',
51725348
r'(Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/flutter/ReplayRecorderCallbacks;)V',

packages/flutter/lib/src/native/java/sentry_native_java.dart

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -237,35 +237,21 @@ class SentryNativeJava extends SentryNativeChannel {
237237

238238
@override
239239
void setContexts(String key, value) => tryCatchSync('setContexts', () {
240-
native.Sentry.configureScope(
241-
native.ScopeCallback.implement(
242-
native.$ScopeCallback(
243-
run: (iScope) {
244-
using((arena) {
245-
final jKey = key.toJString()..releasedBy(arena);
246-
final jVal = dartToJObject(value)..releasedBy(arena);
247-
248-
final scope = iScope.as(const native.$Scope$Type())
249-
..releasedBy(arena);
250-
scope.setContexts(jKey, jVal);
251-
});
252-
},
253-
),
254-
),
255-
);
240+
using((arena) {
241+
final jKey = key.toJString()..releasedBy(arena);
242+
final jVal = dartToJObject(value)..releasedBy(arena);
243+
244+
native.SentryFlutterPlugin.setContext(jKey, jVal);
245+
});
256246
});
257247

258248
@override
259249
void removeContexts(String key) => tryCatchSync('removeContexts', () {
260-
native.Sentry.configureScope(
261-
native.ScopeCallback.implement(native.$ScopeCallback(run: (iScope) {
262-
using((arena) {
263-
final jKey = key.toJString()..releasedBy(arena);
264-
final scope = iScope.as(const native.$Scope$Type())
265-
..releasedBy(arena);
266-
scope.removeContexts(jKey);
267-
});
268-
})));
250+
using((arena) {
251+
final jKey = key.toJString()..releasedBy(arena);
252+
253+
native.SentryFlutterPlugin.removeContext(jKey);
254+
});
269255
});
270256

271257
@override

packages/flutter/lib/src/native/java/sentry_native_java_init.dart

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
part of 'sentry_native_java.dart';
22

3-
const _flutterSdkName = 'sentry.dart.flutter';
4-
53
@internal
64
const androidSdkName = 'sentry.java.android.flutter';
75

@@ -21,7 +19,6 @@ void initSentryAndroid({
2119
);
2220

2321
final beforeSendReplayCallback = createBeforeSendReplayCallback(options);
24-
final beforeSendEventCallback = createBeforeSendCallback();
2522

2623
using((arena) {
2724
final context = native.SentryFlutterPlugin.getApplicationContext()
@@ -41,7 +38,6 @@ void initSentryAndroid({
4138
configureAndroidOptions(
4239
androidOptions: androidOptions,
4340
options: options,
44-
beforeSend: beforeSendEventCallback,
4541
beforeSendReplay: beforeSendReplayCallback,
4642
);
4743

@@ -58,47 +54,6 @@ void initSentryAndroid({
5854
});
5955
}
6056

61-
/// Builds the general beforeSend callback to tag events with origin/environment
62-
/// based on SDK name.
63-
native.SentryOptions$BeforeSendCallback createBeforeSendCallback() {
64-
return native.SentryOptions$BeforeSendCallback.implement(
65-
native.$SentryOptions$BeforeSendCallback(
66-
execute: (sentryEvent, hint) {
67-
using((arena) {
68-
final sdk = sentryEvent.getSdk()?..releasedBy(arena);
69-
if (sdk == null) return;
70-
71-
final originKey = 'event.origin'.toJString()..releasedBy(arena);
72-
final environmentKey = 'event.environment'.toJString()
73-
..releasedBy(arena);
74-
75-
void setTagPair(String origin, String environment) {
76-
final originVal = origin.toJString()..releasedBy(arena);
77-
final envVal = environment.toJString()..releasedBy(arena);
78-
sentryEvent.setTag(originKey, originVal);
79-
sentryEvent.setTag(environmentKey, envVal);
80-
}
81-
82-
switch (sdk.getName().toDartString(releaseOriginal: true)) {
83-
case _flutterSdkName:
84-
setTagPair('flutter', 'dart');
85-
break;
86-
case androidSdkName:
87-
setTagPair('android', 'java');
88-
break;
89-
case nativeSdkName:
90-
setTagPair('android', 'native');
91-
break;
92-
default:
93-
break;
94-
}
95-
});
96-
return sentryEvent;
97-
},
98-
),
99-
);
100-
}
101-
10257
/// Builds the beforeSendReplay callback to override rrweb masking options
10358
/// using Dart-layer privacy configuration.
10459
native.SentryOptions$BeforeSendReplayCallback createBeforeSendReplayCallback(
@@ -198,7 +153,6 @@ native.ReplayRecorderCallbacks? createReplayRecorderCallbacks({
198153
void configureAndroidOptions({
199154
required native.SentryAndroidOptions androidOptions,
200155
required SentryFlutterOptions options,
201-
required native.SentryOptions$BeforeSendCallback beforeSend,
202156
required native.SentryOptions$BeforeSendReplayCallback beforeSendReplay,
203157
}) {
204158
using((arena) {
@@ -305,9 +259,7 @@ void configureAndroidOptions({
305259
);
306260
}
307261

308-
beforeSend.use((cb) {
309-
androidOptions.setBeforeSend(cb);
310-
});
262+
native.SentryFlutterPlugin.setupBeforeSend(androidOptions);
311263

312264
final sessionReplay = androidOptions.getSessionReplay()..releasedBy(arena);
313265
switch (options.replay.quality) {

0 commit comments

Comments
 (0)