Você já teve aquele bug que ninguém sabe reproduzir? Acontece em produção, o cliente liga reclamando, você olha o Sentry e… nada. Zero. Silêncio absoluto.
Esse é o pior tipo de bug. Não porque é difícil de resolver — mas porque você nem sabe o que está acontecendo.
No nosso caso, o sintoma era simples e frustrante: ao voltar do app externo de pagamento, o aplicativo de PDV reaparecia na tela de splash/login em vez de retomar de onde estava. O operador precisava fazer login de novo. No meio de um atendimento. Com a fila.
O Sentry não registrava nada porque não era um crash. O app estava vivo. Ele simplesmente tinha esquecido tudo.
O que estava acontecendo (spoiler: o Android te matou)
Quando o app chama um terminal de pagamento externo via startActivityForResult, ele vai para background. O app de pagamento assume o foreground.
Nesse momento, o Android olha para a memória disponível do dispositivo. Se estiver apertado, o processo do seu app é candidato a execução pelo sistema. O Android pode matar o processo para liberar RAM — e isso não gera crash, não gera stack trace, não chega no Sentry. É uma morte silenciosa e educada.
Quando o app de pagamento termina e devolve o resultado, o Android precisa retomar o seu app. Mas o processo não existe mais. Então ele recria do zero — e o savedInstanceState não foi preservado porque a task foi destruída pelo sistema, não pelo usuário. Resultado: a activity sobe nova, a splash aparece.
[app em background]
↓
Android: "preciso de memória"
↓
Android mata o processo (sem crash, sem trace)
↓
App de pagamento termina
↓
Android: preciso retomar o app anterior
↓
Sem processo → recria do zero
↓
Splash/login aparece
Há uma segunda possibilidade: a task é recriada com o processo ainda vivo (recriação da activity por back stack). Nesse caso o PID é o mesmo, mas o estado da UI foi perdido porque não foi persistido corretamente.
E há uma terceira: o launchMode do app de pagamento cria uma task nova, a sua task some do back stack, e o Android abre a intent como nova inicialização. Um launchMode=singleInstance mal configurado do outro lado pode fazer exatamente isso.
Por que o Sentry não vê isso
O Sentry captura crashes, ANRs e eventos explícitos. Ele não vê morte silenciosa de processo porque, quando o processo morre assim, o SDK não tem tempo de rodar — e não há exceção para capturar. Quando o processo ressurge, é uma nova instância do SDK sem contexto do que aconteceu antes.
O sentry_flutter 9.6 integra o AppExitInfo da API 30+ para capturar ANRs históricos, mas o nosso caso de baixa memória não gera um exit reason de ANR — gera REASON_LOW_MEMORY ou REASON_SIGNALED, que aparecem somente se você perguntar explicitamente.
A pergunta certa é: como guardar informação antes da morte e enviá-la depois da ressurreição?
A solução: Kotlin detecta, Dart envia
A ideia é simples: antes de chamar o app de pagamento, grava um marcador em disco. Quando o app sobe de novo, verifica se o marcador existe — se sim, significa que houve restart no meio de um pagamento. Aí você constrói o diagnóstico e manda pro Sentry.
O marcador precisa estar em SharedPreferences porque sobrevive à morte de processo (diferente de variáveis em memória).
Lado Kotlin (MainActivity)
class MainActivity : FlutterActivity() {
private val channel by lazy {
MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, "payment_diag")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val prefs = getSharedPreferences("payment_diag", Context.MODE_PRIVATE)
val paymentInProgress = prefs.getBoolean("payment_in_progress", false)
if (paymentInProgress) {
// Processo foi recriado durante um pagamento
val oldPid = prefs.getInt("pid", -1)
val newPid = android.os.Process.myPid()
val startTs = prefs.getLong("ts", 0)
val cnpj = prefs.getString("cnpj", "n/d") ?: "n/d"
val durationMs = System.currentTimeMillis() - startTs
val cause = when {
oldPid != newPid -> "PROCESS_KILLED"
savedInstanceState == null -> "TASK_RECREATED"
else -> "UNKNOWN"
}
val exitReason = getExitReason()
val memInfo = getMemoryInfo()
val diag = mapOf(
"cause" to cause,
"pid_old" to oldPid,
"pid_new" to newPid,
"exit_reason" to exitReason,
"low_memory" to memInfo.lowMemory,
"avail_mem_mb" to (memInfo.availMem / 1024 / 1024).toInt(),
"is_task_root" to isTaskRoot,
"intent_action" to (intent?.action ?: "null"),
"duration_ms" to durationMs,
"cnpj" to cnpj
)
prefs.edit().putString("pending_diag", diag.toString()).apply()
}
channel.setMethodCallHandler { call, result ->
when (call.method) {
"markPaymentStart" -> {
val cnpj = call.argument<String>("cnpj") ?: ""
prefs.edit()
.putBoolean("payment_in_progress", true)
.putInt("pid", android.os.Process.myPid())
.putLong("ts", System.currentTimeMillis())
.putString("cnpj", cnpj)
.apply()
result.success(null)
}
"clearPaymentMark" -> {
prefs.edit()
.putBoolean("payment_in_progress", false)
.apply()
result.success(null)
}
"getAndClearDiag" -> {
val diag = prefs.getString("pending_diag", null)
prefs.edit()
.remove("pending_diag")
.putBoolean("payment_in_progress", false)
.apply()
result.success(diag)
}
else -> result.notImplemented()
}
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun getExitReason(): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return "n/d (API < 30)"
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val reasons = am.getHistoricalProcessExitReasons(packageName, 0, 1)
return reasons.firstOrNull()?.let {
when (it.reason) {
ApplicationExitInfo.REASON_LOW_MEMORY -> "LOW_MEMORY"
ApplicationExitInfo.REASON_CRASH -> "CRASH"
ApplicationExitInfo.REASON_ANR -> "ANR"
ApplicationExitInfo.REASON_SIGNALED -> "SIGNALED"
ApplicationExitInfo.REASON_USER_REQUESTED -> "USER_REQUESTED"
else -> "OTHER(${it.reason})"
}
} ?: "n/d"
}
private fun getMemoryInfo(): ActivityManager.MemoryInfo {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
return ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) }
}
}Lado Dart (orquestra e envia)
class PaymentDiagService {
static const _channel = MethodChannel('payment_diag');
// Chamar antes de iniciar o pagamento
static Future<void> markStart(String cnpj) async {
await _channel.invokeMethod('markPaymentStart', {'cnpj': cnpj});
}
// Chamar quando o pagamento retorna normalmente
static Future<void> clearMark() async {
await _channel.invokeMethod('clearPaymentMark');
}
// Chamar após SentryFlutter.init() — verifica se houve restart
static Future<void> checkAndReport() async {
final diag = await _channel.invokeMethod<String>('getAndClearDiag');
if (diag == null) return;
await Sentry.captureMessage(
'payment_return_restart',
level: SentryLevel.warning,
withScope: (scope) {
scope.setTag('cause', _extractField(diag, 'cause'));
scope.setTag('exit_reason', _extractField(diag, 'exit_reason'));
scope.setTag('low_memory', _extractField(diag, 'low_memory'));
scope.setContexts('payment_restart_diag', {
'raw': diag,
});
},
);
}
static String _extractField(String diag, String key) {
// parse simples do toString() do mapa Kotlin
final match = RegExp('$key=([^,}]+)').firstMatch(diag);
return match?.group(1)?.trim() ?? 'n/d';
}
}No main.dart:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SentryFlutter.init((options) {
options.dsn = const String.fromEnvironment('SENTRY_DSN');
options.sampleRate = 1.0; // sobe para 1.0 — este evento é raro, não pode perder
});
// Verifica restart pós-pagamento imediatamente após o init
await PaymentDiagService.checkAndReport();
runApp(const MyApp());
}No fluxo de pagamento:
Future<void> processarPagamento(String cnpj, double valor) async {
await PaymentDiagService.markStart(cnpj);
try {
final resultado = await PosPlugin.iniciarPagamento(valor);
await PaymentDiagService.clearMark(); // retornou normalmente
// ... processa resultado
} catch (e) {
await PaymentDiagService.clearMark();
rethrow;
}
}O timing que faz tudo funcionar
Existe um detalhe crítico que não é óbvio: no onCreate do Android, o engine do Flutter ainda não inicializou. Isso significa que o SDK do Sentry não está disponível quando o Kotlin detecta o restart.
Por isso o Kotlin só detecta e persiste em disco. O Dart envia depois que o SentryFlutter.init completa. O marcador em SharedPreferences serve como ponte entre os dois mundos — e sobrevive à morte do processo porque está em disco, não em memória.
PROCESSO MORTO → RESURGE → onCreate (Kotlin detecta → grava em disco)
→ Flutter init → SentryFlutter.init
→ PaymentDiagService.checkAndReport() → Dart lê e envia
O que você recebe no Sentry
Com isso, cada restart durante um pagamento aparece como evento payment_return_restart com:
- cause:
PROCESS_KILLED(PID diferente),TASK_RECREATED(PID igual, savedInstanceState nulo),UNKNOWN - exit_reason:
LOW_MEMORY,ANR,CRASH,SIGNALED,USER_REQUESTED(só Android 11+) - low_memory:
true/false - duration_ms: tempo que o usuário ficou no app de pagamento
- is_task_root: se a activity subiu como root da task (sinal de lançamento limpo)
- intent_action:
MAIN/LAUNCHER= relançado do zero,MAINsem launcher = task recriada
Com esses dados você finalmente consegue responder: foi memória? Foi o launchMode? Acontece em devices específicos? Em quanto tempo de pagamento?
O problema foi identificado como memória baixa em terminais específicos (hardware limitado rodando uma versão customizada do Android). O launchMode foi a correção para os casos de task recriada pelo back stack; a flag de memória ajudou a identificar os casos de processo morto.
Sem a telemetria, teríamos ficado meses tentando reproduzir em lab um bug que só acontece em campo, sob carga, em dispositivo específico, com memória do sistema apertada.
O Sentry não viu o bug. Nós instrumentamos para que ele visse.






