vkdeploy-push 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +55 -1
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  4. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  5. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  6. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  7. package/android/.gradle/8.9/gc.properties +0 -0
  8. package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
  9. package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
  10. package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
  11. package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
  12. package/android/.gradle/9.2.0/gc.properties +0 -0
  13. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  14. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  15. package/android/.gradle/vcs-1/gc.properties +0 -0
  16. package/android/build.gradle +53 -0
  17. package/android/src/main/AndroidManifest.xml +1 -0
  18. package/android/src/main/java/com/vkdeploy/push/VkDeployMonitorModule.kt +197 -0
  19. package/android/src/main/java/com/vkdeploy/push/VkDeployMonitorPackage.kt +40 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +54 -1
  22. package/dist/monitoring.d.ts +18 -0
  23. package/dist/monitoring.js +118 -0
  24. package/dist/network.d.ts +18 -2
  25. package/dist/network.js +65 -8
  26. package/dist/runtime.d.ts +15 -1
  27. package/dist/runtime.js +32 -0
  28. package/dist/types.d.ts +43 -2
  29. package/dist/utils/functions.js +2 -2
  30. package/package.json +6 -2
package/README.md CHANGED
@@ -52,7 +52,7 @@ if (result.status === 'error') {
52
52
 
53
53
  ## Configuracao
54
54
 
55
- Crie o arquivo `android/app/src/main/assets/versionamento-vktech-config.json`:
55
+ Crie o arquivo `android/app/src/main/assets/vkdeploy-config.json`:
56
56
 
57
57
  ```json
58
58
  {
@@ -96,3 +96,57 @@ setLogger({
96
96
  error: (message, context) => console.error(message, context),
97
97
  });
98
98
  ```
99
+
100
+ ## Monitoramento de falhas (opcional, desligado por padrão)
101
+
102
+ A partir da versão 1.1.0 o SDK pode reportar as saídas anormais do app. O recurso
103
+ nasce **desligado** e só é habilitado pelo painel, por projeto — não há chave de
104
+ ativação no `vkdeploy-config.json` nem no código do app.
105
+
106
+ ### O que é coletado
107
+
108
+ A fonte é o `ApplicationExitInfo` do Android (API 30+), lido na abertura seguinte
109
+ à falha. Isso cobre quatro classes de problema que o JavaScript não enxerga:
110
+
111
+ | Motivo | Cobertura |
112
+ | --- | --- |
113
+ | `CRASH` | exceção Java/Kotlin não tratada |
114
+ | `CRASH_NATIVE` | SIGSEGV e afins, com o tombstone do sistema |
115
+ | `ANR` | dump de todas as threads |
116
+ | `LOW_MEMORY` | processo morto por falta de memória, sem stack trace |
117
+
118
+ Saídas normais (usuário fechou o app, atualização do pacote, limpeza de processo
119
+ em segundo plano) são descartadas no lado nativo.
120
+
121
+ ### Como ligar
122
+
123
+ 1. Abra o projeto no painel do Versionamento.
124
+ 2. Ative a chave **Monitoramento de falhas**.
125
+
126
+ A configuração viaja junto da resposta de `POST /versao`, que o app já consulta a
127
+ cada abertura, então não há requisição adicional. A propagação é imediata para
128
+ quem alterna a chave e de no máximo 5 minutos caso a linha seja alterada direto
129
+ no banco.
130
+
131
+ ### Garantias
132
+
133
+ - **Nada é coletado com a chave desligada.** O módulo nativo só é consultado após
134
+ o servidor confirmar que o projeto tem o recurso habilitado, e o servidor
135
+ descarta lotes de projetos desligados.
136
+ - **Ligar a chave não sobe o histórico anterior.** O instante da ativação é usado
137
+ como marca d'água mínima.
138
+ - **Nenhuma falha é perdida por erro de rede.** A marca d'água local só avança
139
+ após confirmação do servidor; um envio que falha é repetido na abertura
140
+ seguinte, e o servidor deduplica pela chave `(projeto, deviceId, ocorridoEm)`.
141
+ - **O monitoramento nunca bloqueia a atualização.** Qualquer erro de coleta ou
142
+ envio é registrado no logger e ignorado.
143
+
144
+ ### Requisito de build
145
+
146
+ Esta versão adiciona código nativo. Código nativo **não é entregue por bundle
147
+ OTA**: após atualizar para a 1.1.0 é preciso gerar e publicar um APK novo. Em
148
+ apps que atualizaram apenas o bundle, o SDK detecta a ausência do módulo nativo,
149
+ registra um aviso e segue funcionando sem monitoramento.
150
+
151
+ Dispositivos abaixo do Android 11 não expõem o histórico de saídas do sistema e
152
+ ficam sem esta camada de cobertura.
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ #Fri Sep 18 11:46:28 BRT 2026
2
+ gradle.version=9.2.0
File without changes
@@ -0,0 +1,53 @@
1
+ buildscript {
2
+ // O app consumidor define estas versoes em rootProject.ext. Os valores de
3
+ // fallback existem apenas para quem compila a biblioteca isolada.
4
+ ext.getExtOrDefault = { name, fallback ->
5
+ rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
6
+ }
7
+
8
+ repositories {
9
+ google()
10
+ mavenCentral()
11
+ }
12
+
13
+ dependencies {
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion', '2.0.21')}"
15
+ }
16
+ }
17
+
18
+ apply plugin: 'com.android.library'
19
+ apply plugin: 'org.jetbrains.kotlin.android'
20
+
21
+ android {
22
+ namespace "com.vkdeploy.push"
23
+ compileSdk getExtOrDefault('compileSdkVersion', 35)
24
+
25
+ defaultConfig {
26
+ minSdk getExtOrDefault('minSdkVersion', 21)
27
+ }
28
+
29
+ compileOptions {
30
+ sourceCompatibility JavaVersion.VERSION_17
31
+ targetCompatibility JavaVersion.VERSION_17
32
+ }
33
+
34
+ kotlinOptions {
35
+ jvmTarget = "17"
36
+ }
37
+
38
+ lintOptions {
39
+ abortOnError false
40
+ }
41
+ }
42
+
43
+ repositories {
44
+ google()
45
+ mavenCentral()
46
+ }
47
+
48
+ dependencies {
49
+ // Sem versao: o plugin Gradle do React Native resolve o artefato correto
50
+ // a partir da versao de react-native usada pelo app consumidor.
51
+ implementation "com.facebook.react:react-android"
52
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:${getExtOrDefault('kotlinVersion', '2.0.21')}"
53
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,197 @@
1
+ package com.vkdeploy.push
2
+
3
+ import android.annotation.TargetApi
4
+ import android.app.ActivityManager
5
+ import android.app.ApplicationExitInfo
6
+ import android.content.Context
7
+ import android.os.Build
8
+ import com.facebook.react.bridge.Arguments
9
+ import com.facebook.react.bridge.Promise
10
+ import com.facebook.react.bridge.ReactApplicationContext
11
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
12
+ import com.facebook.react.bridge.ReactMethod
13
+ import com.facebook.react.bridge.WritableArray
14
+ import com.facebook.react.bridge.WritableMap
15
+ import com.facebook.react.module.annotations.ReactModule
16
+
17
+ /**
18
+ * Le as saidas anormais do proprio processo registradas pelo sistema.
19
+ *
20
+ * O Android mantem um historico curto de como cada processo do app terminou
21
+ * (ActivityManager.getHistoricalProcessExitReasons, API 30+). Consultar esse
22
+ * historico na abertura seguinte cobre, sem instalar nenhum handler, quatro
23
+ * classes de falha que o JavaScript nao enxerga:
24
+ *
25
+ * - crash de Java/Kotlin em modulo nativo;
26
+ * - crash nativo (SIGSEGV e afins), com o tombstone completo;
27
+ * - ANR, com o dump de todas as threads;
28
+ * - morte por falta de memoria, que nao gera stack trace em lugar nenhum e
29
+ * costuma ser relatada pelo usuario como "o app fechou sozinho".
30
+ *
31
+ * Nada e coletado enquanto o monitoramento estiver desligado: o lado JavaScript
32
+ * so chama este modulo depois de o servidor confirmar que o projeto tem o
33
+ * recurso habilitado.
34
+ */
35
+ @ReactModule(name = VkDeployMonitorModule.NAME)
36
+ class VkDeployMonitorModule(
37
+ reactContext: ReactApplicationContext,
38
+ ) : ReactContextBaseJavaModule(reactContext) {
39
+
40
+ override fun getName(): String = NAME
41
+
42
+ /**
43
+ * Retorna as saidas anormais ocorridas depois de [desdeTimestamp].
44
+ *
45
+ * @param desdeTimestamp marca d'agua em epoch ms; saidas anteriores ou iguais
46
+ * sao descartadas. O lado JavaScript guarda esse valor e o avanca somente
47
+ * apos um envio confirmado, o que evita tanto perda quanto duplicata.
48
+ * @param maximo teto de itens lidos do historico do sistema.
49
+ */
50
+ @ReactMethod
51
+ fun coletaSaidasAnormais(desdeTimestamp: Double, maximo: Int, promise: Promise) {
52
+ try {
53
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
54
+ // Abaixo da API 30 a API nao existe. Devolver lista vazia mantem o
55
+ // fluxo do SDK identico e deixa a decisao de cobertura para a
56
+ // proxima camada (UncaughtExceptionHandler), que roda em qualquer
57
+ // versao.
58
+ promise.resolve(Arguments.createArray())
59
+ return
60
+ }
61
+
62
+ promise.resolve(leHistorico(desdeTimestamp.toLong(), maximo))
63
+ } catch (erro: Throwable) {
64
+ promise.reject("EXIT_INFO_ERRO", erro.message, erro)
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Informa se este dispositivo consegue fornecer o historico de saidas.
70
+ */
71
+ @ReactMethod
72
+ fun suportaExitInfo(promise: Promise) {
73
+ promise.resolve(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
74
+ }
75
+
76
+ @TargetApi(Build.VERSION_CODES.R)
77
+ private fun leHistorico(desde: Long, maximo: Int): WritableArray {
78
+ val resultado = Arguments.createArray()
79
+ val contexto = reactApplicationContext
80
+ val activityManager = contexto.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
81
+ ?: return resultado
82
+
83
+ val limite = maximo.coerceIn(1, MAX_ITENS)
84
+
85
+ val historico = activityManager.getHistoricalProcessExitReasons(
86
+ contexto.packageName,
87
+ PID_TODOS,
88
+ limite,
89
+ )
90
+
91
+ for (saida in historico) {
92
+ if (saida.timestamp <= desde) {
93
+ continue
94
+ }
95
+
96
+ if (saida.reason !in MOTIVOS_RELEVANTES) {
97
+ continue
98
+ }
99
+
100
+ resultado.pushMap(mapeia(saida))
101
+ }
102
+
103
+ return resultado
104
+ }
105
+
106
+ @TargetApi(Build.VERSION_CODES.R)
107
+ private fun mapeia(saida: ApplicationExitInfo): WritableMap {
108
+ val mapa = Arguments.createMap()
109
+
110
+ mapa.putInt("reason", saida.reason)
111
+ mapa.putString("reasonLabel", rotuloMotivo(saida.reason))
112
+ mapa.putString("descricao", saida.description)
113
+ mapa.putInt("importancia", saida.importance)
114
+ mapa.putString("importanciaLabel", rotuloImportancia(saida.importance))
115
+ // pss e rss vem em kB.
116
+ mapa.putDouble("pss", saida.pss.toDouble())
117
+ mapa.putDouble("rss", saida.rss.toDouble())
118
+ mapa.putDouble("ocorridoEm", saida.timestamp.toDouble())
119
+ mapa.putString("trace", leTrace(saida))
120
+ mapa.putString("osVersion", Build.VERSION.RELEASE)
121
+ mapa.putString("modelo", Build.MODEL)
122
+ mapa.putString("fabricante", Build.MANUFACTURER)
123
+
124
+ return mapa
125
+ }
126
+
127
+ /**
128
+ * Le o trace anexado a saida, quando existe.
129
+ *
130
+ * O sistema so anexa trace para ANR (dump das threads) e para crash nativo
131
+ * (tombstone). Em REASON_CRASH o resumo da excecao vem apenas em
132
+ * [ApplicationExitInfo.getDescription], e o retorno aqui e nulo.
133
+ *
134
+ * O tombstone pode passar de centenas de kB, entao a leitura e truncada: as
135
+ * primeiras linhas concentram o sinal (sinal recebido, endereco de falha e
136
+ * a pilha da thread que morreu).
137
+ */
138
+ @TargetApi(Build.VERSION_CODES.R)
139
+ private fun leTrace(saida: ApplicationExitInfo): String? {
140
+ return try {
141
+ saida.traceInputStream?.use { entrada ->
142
+ val buffer = ByteArray(MAX_TRACE_BYTES)
143
+ var lidos = 0
144
+
145
+ while (lidos < MAX_TRACE_BYTES) {
146
+ val n = entrada.read(buffer, lidos, MAX_TRACE_BYTES - lidos)
147
+ if (n <= 0) break
148
+ lidos += n
149
+ }
150
+
151
+ if (lidos <= 0) null else String(buffer, 0, lidos, Charsets.UTF_8)
152
+ }
153
+ } catch (erro: Throwable) {
154
+ // Um trace ilegivel nao pode impedir o envio do resto do lote: o
155
+ // motivo, o instante e o uso de memoria ja identificam a falha.
156
+ null
157
+ }
158
+ }
159
+
160
+ private fun rotuloMotivo(reason: Int): String = when (reason) {
161
+ ApplicationExitInfo.REASON_CRASH -> "CRASH"
162
+ ApplicationExitInfo.REASON_CRASH_NATIVE -> "CRASH_NATIVE"
163
+ ApplicationExitInfo.REASON_ANR -> "ANR"
164
+ ApplicationExitInfo.REASON_LOW_MEMORY -> "LOW_MEMORY"
165
+ else -> "DESCONHECIDO_$reason"
166
+ }
167
+
168
+ private fun rotuloImportancia(importancia: Int): String = when (importancia) {
169
+ ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND -> "FOREGROUND"
170
+ ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE -> "FOREGROUND_SERVICE"
171
+ ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE -> "VISIBLE"
172
+ ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE -> "PERCEPTIBLE"
173
+ ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE -> "SERVICE"
174
+ ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED -> "CACHED"
175
+ else -> "OUTRO_$importancia"
176
+ }
177
+
178
+ companion object {
179
+ const val NAME = "VkDeployMonitor"
180
+
181
+ private const val PID_TODOS = 0
182
+ private const val MAX_ITENS = 30
183
+ private const val MAX_TRACE_BYTES = 60_000
184
+
185
+ /**
186
+ * Motivos que representam falha. Saidas normais (usuario fechou o app,
187
+ * atualizacao do pacote, limpeza de processo em segundo plano) ficam de
188
+ * fora para nao poluir o painel com ruido que nao pede acao.
189
+ */
190
+ private val MOTIVOS_RELEVANTES = setOf(
191
+ ApplicationExitInfo.REASON_CRASH,
192
+ ApplicationExitInfo.REASON_CRASH_NATIVE,
193
+ ApplicationExitInfo.REASON_ANR,
194
+ ApplicationExitInfo.REASON_LOW_MEMORY,
195
+ )
196
+ }
197
+ }
@@ -0,0 +1,40 @@
1
+ package com.vkdeploy.push
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ /**
11
+ * Registro do modulo nativo junto ao autolinking do React Native.
12
+ *
13
+ * Herda de BaseReactPackage para que a instancia so seja criada quando o modulo
14
+ * for de fato usado. Como o monitoramento nasce desligado, na maioria dos apps
15
+ * este modulo nunca chega a ser instanciado.
16
+ */
17
+ class VkDeployMonitorPackage : BaseReactPackage() {
18
+
19
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
20
+ if (name == VkDeployMonitorModule.NAME) VkDeployMonitorModule(reactContext) else null
21
+
22
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
23
+ mapOf(
24
+ VkDeployMonitorModule.NAME to ReactModuleInfo(
25
+ VkDeployMonitorModule.NAME,
26
+ VkDeployMonitorModule.NAME,
27
+ false, // canOverrideExistingModule
28
+ false, // needsEagerInit
29
+ false, // isCxxModule
30
+ // Modulo escrito a mao, sem spec de codegen: o React Native o expoe
31
+ // pela camada de interoperabilidade da nova arquitetura.
32
+ false, // isTurboModule
33
+ ),
34
+ )
35
+ }
36
+
37
+ override fun createViewManagers(
38
+ reactContext: ReactApplicationContext,
39
+ ): List<ViewManager<*, *>> = emptyList()
40
+ }
package/dist/index.d.ts CHANGED
@@ -11,5 +11,5 @@ declare const vkdeployPush: {
11
11
  resetLogger: typeof resetLogger;
12
12
  };
13
13
  export default vkdeployPush;
14
- export type { AtualizarResult, CheckUpdateResult, ErrorCheckResult, ErrorUpdateResult, LoggerContext, RemoteVersionResponse, UpdateLifecycleCallback, UpdatedCheckResult, UpdatedResult, UpToDateCheckResult, VersaoInfo, VkDeployError, VkDeployErrorCode, VkDeployLogger, } from './types';
14
+ export type { AtualizarResult, CheckUpdateResult, ErrorCheckResult, ErrorUpdateResult, LoggerContext, MonitoramentoConfig, MonitoramentoResult, MonitoramentoStatus, RemoteVersionResponse, SaidaAnormal, UpdateLifecycleCallback, UpdatedCheckResult, UpdatedResult, UpToDateCheckResult, VersaoInfo, VkDeployError, VkDeployErrorCode, VkDeployLogger, } from './types';
15
15
  export { resetLogger, setLogger };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ const DBSQL_1 = require("./database/DBSQL");
9
9
  const logger_1 = require("./logger");
10
10
  Object.defineProperty(exports, "setLogger", { enumerable: true, get: function () { return logger_1.setLogger; } });
11
11
  Object.defineProperty(exports, "resetLogger", { enumerable: true, get: function () { return logger_1.resetLogger; } });
12
+ const monitoring_1 = require("./monitoring");
12
13
  const network_1 = require("./network");
13
14
  const runtime_1 = require("./runtime");
14
15
  const functions_1 = require("./utils/functions");
@@ -87,6 +88,18 @@ async function getBuildVersion() {
87
88
  return (0, logger_1.createError)('INVALID_DEPENDENCY', 'Falha ao executar getVersion() de "react-native-device-info".', cause);
88
89
  }
89
90
  }
91
+ async function getDeviceId() {
92
+ const deviceInfo = await (0, runtime_1.loadDeviceInfo)();
93
+ if (!deviceInfo.ok) {
94
+ return deviceInfo.error;
95
+ }
96
+ try {
97
+ return await deviceInfo.module.getUniqueId();
98
+ }
99
+ catch (cause) {
100
+ return (0, logger_1.createError)('INVALID_DEPENDENCY', 'Falha ao executar getUniqueId() de "react-native-device-info".', cause);
101
+ }
102
+ }
90
103
  async function downloadBundleArchive(fromUrl, toFile) {
91
104
  const rnfs = await (0, runtime_1.loadRNFS)();
92
105
  if (!rnfs.ok) {
@@ -102,6 +115,28 @@ async function downloadBundleArchive(fromUrl, toFile) {
102
115
  return (0, logger_1.createError)('DOWNLOAD_FAILED', 'Falha ao baixar o bundle remoto.', cause);
103
116
  }
104
117
  }
118
+ /**
119
+ * Executa o ciclo de monitoramento sem deixar que ele afete a atualizacao.
120
+ *
121
+ * Monitorar e um recurso acessorio: qualquer falha ao coletar ou enviar precisa
122
+ * ser registrada no log e ignorada, jamais impedir o app de receber um bundle
123
+ * novo. Por isso o resultado e apenas anotado e nenhuma excecao escapa daqui.
124
+ */
125
+ async function executaMonitoramento(config, dados) {
126
+ try {
127
+ const resultado = await (0, monitoring_1.sincronizaMonitoramento)(API_BASE_URL, config, dados);
128
+ if (resultado.status === 'erro' && resultado.error) {
129
+ (0, logger_1.logError)(resultado.error.message, { code: resultado.error.code });
130
+ return;
131
+ }
132
+ if (resultado.status === 'enviado') {
133
+ (0, logger_1.logInfo)('Monitoramento sincronizado.', { enviados: resultado.enviados });
134
+ }
135
+ }
136
+ catch (cause) {
137
+ (0, logger_1.logError)('Falha inesperada no ciclo de monitoramento.', { cause });
138
+ }
139
+ }
105
140
  async function getVersao() {
106
141
  return (0, DBSQL_1.buscaVersao)();
107
142
  }
@@ -119,12 +154,30 @@ async function checkUpdate(antes, depois) {
119
154
  if (typeof buildVersion !== 'string') {
120
155
  return toCheckErrorResult(buildVersion, versaoLocal);
121
156
  }
122
- const remoteVersion = await (0, network_1.requestRemoteVersion)(API_BASE_URL, projectIdResult.projectId, buildVersion);
157
+ const deviceId = await getDeviceId();
158
+ if (typeof deviceId !== 'string') {
159
+ return toCheckErrorResult(deviceId, versaoLocal);
160
+ }
161
+ const remoteVersion = await (0, network_1.requestRemoteVersion)(API_BASE_URL, projectIdResult.projectId, buildVersion, deviceId);
123
162
  if (!remoteVersion.ok) {
124
163
  return toCheckErrorResult(remoteVersion.error, versaoLocal);
125
164
  }
165
+ // O monitoramento roda antes da decisao sobre atualizar, e nao depois, porque a
166
+ // atualizacao termina com o app sendo reiniciado: deixar o envio para o final
167
+ // faria com que justamente os dispositivos que recebem bundle novo nunca
168
+ // reportassem as falhas da versao anterior.
169
+ await executaMonitoramento(remoteVersion.data.monitoramento, {
170
+ projetoId: projectIdResult.projectId,
171
+ deviceId,
172
+ versaoBuild: buildVersion,
173
+ versaoBundle: versaoLocal.versao,
174
+ });
126
175
  const versaoAtual = versaoLocal.versao;
127
176
  const versaoRemota = remoteVersion.data.versao;
177
+ if (typeof versaoRemota !== 'number') {
178
+ return toCheckErrorResult((0, logger_1.createError)('INVALID_REMOTE_RESPONSE', 'A resposta da API de versão não contém um campo "versao" válido.'), versaoLocal);
179
+ }
180
+ console.log(`Versão atual: ${versaoAtual}, Versão remota: ${versaoRemota}`);
128
181
  if (versaoRemota === versaoAtual) {
129
182
  return {
130
183
  status: 'up-to-date',
@@ -0,0 +1,18 @@
1
+ import type { MonitoramentoConfig, MonitoramentoResult } from './types';
2
+ /**
3
+ * Coleta e envia as saidas anormais registradas desde o ultimo ciclo.
4
+ *
5
+ * Executa apenas quando o painel tem o monitoramento ligado para o projeto. A
6
+ * marca d'agua efetiva e a maior entre a ultima saida ja enviada e o instante em
7
+ * que o recurso foi ativado, de modo que ligar a chave comeca a coleta dali para
8
+ * a frente e nunca sobe o historico anterior a ativacao.
9
+ *
10
+ * A marca d'agua so avanca apos confirmacao do servidor: se o envio falhar, o
11
+ * mesmo lote e tentado de novo na proxima abertura do app.
12
+ */
13
+ export declare function sincronizaMonitoramento(apiBaseUrl: string, config: MonitoramentoConfig | undefined, dados: {
14
+ projetoId: string;
15
+ deviceId: string;
16
+ versaoBuild: string;
17
+ versaoBundle: number;
18
+ }): Promise<MonitoramentoResult>;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sincronizaMonitoramento = sincronizaMonitoramento;
4
+ const logger_1 = require("./logger");
5
+ const network_1 = require("./network");
6
+ const runtime_1 = require("./runtime");
7
+ const ARQUIVO_MARCA_DAGUA = 'vkdeploy-monitor.json';
8
+ /**
9
+ * Teto de saidas lidas por execucao.
10
+ *
11
+ * O proprio Android guarda cerca de 16 saidas por processo, entao este valor nao
12
+ * costuma ser alcancado; ele limita o tamanho do lote em dispositivos que
13
+ * acumularam historico sem conseguir enviar.
14
+ */
15
+ const MAX_SAIDAS_POR_EXECUCAO = 25;
16
+ function caminhoMarcaDagua(documentDirectoryPath) {
17
+ return `${documentDirectoryPath}/${ARQUIVO_MARCA_DAGUA}`;
18
+ }
19
+ /**
20
+ * Le o instante da ultima saida anormal ja enviada com sucesso.
21
+ *
22
+ * Retorna 0 quando o arquivo nao existe ou esta corrompido, o que faz o proximo
23
+ * ciclo reenviar o que ainda estiver no historico do sistema. O indice unico do
24
+ * servidor absorve a repeticao, entao errar para o lado do reenvio e seguro,
25
+ * enquanto errar para o lado de pular perderia a falha de forma definitiva.
26
+ */
27
+ async function leMarcaDagua() {
28
+ const rnfs = await (0, runtime_1.loadRNFS)();
29
+ if (!rnfs.ok) {
30
+ return 0;
31
+ }
32
+ try {
33
+ const caminho = caminhoMarcaDagua(rnfs.module.DocumentDirectoryPath);
34
+ if (!(await rnfs.module.exists(caminho))) {
35
+ return 0;
36
+ }
37
+ const conteudo = await rnfs.module.readFile(caminho, 'utf8');
38
+ const dados = JSON.parse(conteudo);
39
+ return typeof dados.ultimaSaidaEnviada === 'number' && Number.isFinite(dados.ultimaSaidaEnviada)
40
+ ? dados.ultimaSaidaEnviada
41
+ : 0;
42
+ }
43
+ catch {
44
+ return 0;
45
+ }
46
+ }
47
+ async function salvaMarcaDagua(valor) {
48
+ const rnfs = await (0, runtime_1.loadRNFS)();
49
+ if (!rnfs.ok) {
50
+ return;
51
+ }
52
+ try {
53
+ await rnfs.module.writeFile(caminhoMarcaDagua(rnfs.module.DocumentDirectoryPath), JSON.stringify({ ultimaSaidaEnviada: valor }), 'utf8');
54
+ }
55
+ catch (cause) {
56
+ // Falhar ao gravar a marca d'agua apenas provoca reenvio no proximo ciclo,
57
+ // que o servidor deduplica. Nao ha motivo para interromper o fluxo.
58
+ (0, logger_1.logWarn)('Nao foi possivel salvar a marca d\'agua do monitoramento.', { cause });
59
+ }
60
+ }
61
+ /**
62
+ * Coleta e envia as saidas anormais registradas desde o ultimo ciclo.
63
+ *
64
+ * Executa apenas quando o painel tem o monitoramento ligado para o projeto. A
65
+ * marca d'agua efetiva e a maior entre a ultima saida ja enviada e o instante em
66
+ * que o recurso foi ativado, de modo que ligar a chave comeca a coleta dali para
67
+ * a frente e nunca sobe o historico anterior a ativacao.
68
+ *
69
+ * A marca d'agua so avanca apos confirmacao do servidor: se o envio falhar, o
70
+ * mesmo lote e tentado de novo na proxima abertura do app.
71
+ */
72
+ async function sincronizaMonitoramento(apiBaseUrl, config, dados) {
73
+ var _a;
74
+ if (!(config === null || config === void 0 ? void 0 : config.ativo)) {
75
+ return { status: 'desligado', enviados: 0 };
76
+ }
77
+ const nativo = await (0, runtime_1.loadMonitorNative)();
78
+ if (!nativo.ok) {
79
+ return { status: 'nao-suportado', enviados: 0, error: nativo.error };
80
+ }
81
+ try {
82
+ if (!(await nativo.module.suportaExitInfo())) {
83
+ // Dispositivo abaixo do Android 11. A cobertura desta camada depende da
84
+ // API de historico de saidas, que nao existe nessas versoes.
85
+ return { status: 'nao-suportado', enviados: 0 };
86
+ }
87
+ const marcaLocal = await leMarcaDagua();
88
+ const marca = Math.max(marcaLocal, (_a = config.ativadoEm) !== null && _a !== void 0 ? _a : 0);
89
+ const saidas = await nativo.module.coletaSaidasAnormais(marca, MAX_SAIDAS_POR_EXECUCAO);
90
+ if (!Array.isArray(saidas) || saidas.length === 0) {
91
+ return { status: 'sem-novidades', enviados: 0 };
92
+ }
93
+ const envio = await (0, network_1.enviarSaidasAnormais)(apiBaseUrl, {
94
+ projetoId: dados.projetoId,
95
+ deviceId: dados.deviceId,
96
+ versaoBuild: dados.versaoBuild,
97
+ versaoBundle: dados.versaoBundle,
98
+ saidas,
99
+ });
100
+ if (!envio.ok) {
101
+ return { status: 'erro', enviados: 0, error: envio.error };
102
+ }
103
+ const maisRecente = saidas.reduce((maior, saida) => (saida.ocorridoEm > maior ? saida.ocorridoEm : maior), marca);
104
+ await salvaMarcaDagua(maisRecente);
105
+ (0, logger_1.logInfo)('Saidas anormais enviadas.', {
106
+ quantidade: saidas.length,
107
+ gravados: envio.data.gravados,
108
+ });
109
+ return { status: 'enviado', enviados: saidas.length };
110
+ }
111
+ catch (cause) {
112
+ return {
113
+ status: 'erro',
114
+ enviados: 0,
115
+ error: (0, logger_1.createError)('MONITOR_COLLECT_FAILED', 'Falha ao coletar as saidas anormais do dispositivo.', cause),
116
+ };
117
+ }
118
+ }
package/dist/network.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { RemoteVersionResponse, VkDeployError } from './types';
1
+ import type { RemoteVersionResponse, SaidaAnormal, VkDeployError } from './types';
2
2
  type Result<T> = {
3
3
  ok: true;
4
4
  data: T;
@@ -6,6 +6,22 @@ type Result<T> = {
6
6
  ok: false;
7
7
  error: VkDeployError;
8
8
  };
9
- export declare function requestRemoteVersion(apiBaseUrl: string, projectId: string, buildVersion: string): Promise<Result<RemoteVersionResponse>>;
9
+ export declare function requestRemoteVersion(apiBaseUrl: string, projectId: string, buildVersion: string, deviceId: string): Promise<Result<RemoteVersionResponse>>;
10
+ /**
11
+ * Envia ao servidor as saidas anormais coletadas neste dispositivo.
12
+ *
13
+ * O servidor confere a chave de monitoramento do projeto antes de gravar, entao
14
+ * um lote enviado por um dispositivo com configuracao desatualizada e descartado
15
+ * do outro lado.
16
+ */
17
+ export declare function enviarSaidasAnormais(apiBaseUrl: string, payload: {
18
+ projetoId: string;
19
+ deviceId: string;
20
+ versaoBuild: string;
21
+ versaoBundle: number;
22
+ saidas: SaidaAnormal[];
23
+ }): Promise<Result<{
24
+ gravados: number;
25
+ }>>;
10
26
  export declare function resolveBundleFileName(bundleUrl: string): Promise<string>;
11
27
  export {};
package/dist/network.js CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.requestRemoteVersion = requestRemoteVersion;
4
+ exports.enviarSaidasAnormais = enviarSaidasAnormais;
4
5
  exports.resolveBundleFileName = resolveBundleFileName;
5
6
  const logger_1 = require("./logger");
6
- async function requestRemoteVersion(apiBaseUrl, projectId, buildVersion) {
7
+ async function requestRemoteVersion(apiBaseUrl, projectId, buildVersion, deviceId) {
7
8
  try {
8
9
  const response = await fetch(`${apiBaseUrl}/versao`, {
9
10
  method: 'POST',
@@ -14,6 +15,7 @@ async function requestRemoteVersion(apiBaseUrl, projectId, buildVersion) {
14
15
  body: JSON.stringify({
15
16
  projetoId: projectId,
16
17
  versaoBuild: buildVersion,
18
+ deviceId,
17
19
  }),
18
20
  });
19
21
  if (!response.ok) {
@@ -23,17 +25,17 @@ async function requestRemoteVersion(apiBaseUrl, projectId, buildVersion) {
23
25
  };
24
26
  }
25
27
  const data = (await response.json());
26
- if (typeof data.versao !== 'number' || Number.isNaN(data.versao)) {
27
- return {
28
- ok: false,
29
- error: (0, logger_1.createError)('INVALID_REMOTE_RESPONSE', 'A resposta da API de versão não contém um campo "versao" válido.'),
30
- };
31
- }
28
+ // A validacao de "versao" acontece em checkUpdate, e nao aqui. Um projeto sem
29
+ // versao ativa publicada recebe uma resposta sem esse campo, e mesmo assim
30
+ // precisa ter a configuracao de monitoramento lida: caso contrario ligar a
31
+ // chave no painel nao surtiria efeito nesses projetos.
32
+ const versao = typeof data.versao === 'number' && !Number.isNaN(data.versao) ? data.versao : undefined;
32
33
  return {
33
34
  ok: true,
34
35
  data: {
35
- versao: data.versao,
36
+ versao,
36
37
  mandatori: data.mandatori,
38
+ monitoramento: normalizaMonitoramento(data.monitoramento),
37
39
  },
38
40
  };
39
41
  }
@@ -44,6 +46,61 @@ async function requestRemoteVersion(apiBaseUrl, projectId, buildVersion) {
44
46
  };
45
47
  }
46
48
  }
49
+ /**
50
+ * Normaliza a configuracao de monitoramento vinda do servidor.
51
+ *
52
+ * Qualquer formato inesperado resulta em "desligado": o recurso e opt-in e nao
53
+ * pode ser ativado por acidente a partir de uma resposta malformada.
54
+ */
55
+ function normalizaMonitoramento(valor) {
56
+ if (!valor || typeof valor !== 'object') {
57
+ return { ativo: false, ativadoEm: null };
58
+ }
59
+ const bruto = valor;
60
+ const ativadoEm = typeof bruto.ativadoEm === 'number' && Number.isFinite(bruto.ativadoEm)
61
+ ? bruto.ativadoEm
62
+ : null;
63
+ return {
64
+ ativo: bruto.ativo === true,
65
+ ativadoEm,
66
+ };
67
+ }
68
+ /**
69
+ * Envia ao servidor as saidas anormais coletadas neste dispositivo.
70
+ *
71
+ * O servidor confere a chave de monitoramento do projeto antes de gravar, entao
72
+ * um lote enviado por um dispositivo com configuracao desatualizada e descartado
73
+ * do outro lado.
74
+ */
75
+ async function enviarSaidasAnormais(apiBaseUrl, payload) {
76
+ try {
77
+ const response = await fetch(`${apiBaseUrl}/erro`, {
78
+ method: 'POST',
79
+ headers: {
80
+ Accept: 'application/json',
81
+ 'Content-Type': 'application/json',
82
+ },
83
+ body: JSON.stringify(payload),
84
+ });
85
+ if (!response.ok) {
86
+ return {
87
+ ok: false,
88
+ error: (0, logger_1.createError)('MONITOR_SEND_FAILED', `Erro ao enviar saidas anormais: HTTP ${response.status}`),
89
+ };
90
+ }
91
+ const data = (await response.json());
92
+ return {
93
+ ok: true,
94
+ data: { gravados: typeof data.gravados === 'number' ? data.gravados : 0 },
95
+ };
96
+ }
97
+ catch (cause) {
98
+ return {
99
+ ok: false,
100
+ error: (0, logger_1.createError)('MONITOR_SEND_FAILED', 'Falha ao enviar as saidas anormais.', cause),
101
+ };
102
+ }
103
+ }
47
104
  async function resolveBundleFileName(bundleUrl) {
48
105
  var _a;
49
106
  try {
package/dist/runtime.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { VkDeployError, VersaoInfo } from './types';
1
+ import type { SaidaAnormal, VkDeployError, VersaoInfo } from './types';
2
2
  export type RNFSDirEntry = {
3
3
  path: string;
4
4
  name: string;
@@ -37,6 +37,11 @@ type ZipArchiveModule = {
37
37
  unzip(source: string, target: string): Promise<unknown>;
38
38
  };
39
39
  export type UnzipFunction = ZipArchiveModule['unzip'];
40
+ /** Superficie do modulo nativo de monitoramento (android/src/main/.../VkDeployMonitorModule.kt). */
41
+ export type MonitorNativeModule = {
42
+ coletaSaidasAnormais(desdeTimestamp: number, maximo: number): Promise<SaidaAnormal[]>;
43
+ suportaExitInfo(): Promise<boolean>;
44
+ };
40
45
  type ModuleLoadResult<T> = {
41
46
  ok: true;
42
47
  module: T;
@@ -49,5 +54,14 @@ export declare function ensureSupportedPlatform(): Promise<VkDeployError | undef
49
54
  export declare function loadRNFS(): Promise<ModuleLoadResult<RNFSModule>>;
50
55
  export declare function loadDeviceInfo(): Promise<ModuleLoadResult<DeviceInfoModule>>;
51
56
  export declare function loadUnzip(): Promise<ModuleLoadResult<UnzipFunction>>;
57
+ /**
58
+ * Carrega o modulo nativo de monitoramento.
59
+ *
60
+ * A ausencia do modulo e um caso esperado, nao um defeito: codigo nativo nao
61
+ * viaja em bundle OTA, entao um app que atualizou o vkdeploy-push por bundle sem
62
+ * gerar um APK novo continua sem ele. Nesse caso o monitoramento fica inativo e
63
+ * o restante do SDK segue funcionando normalmente.
64
+ */
65
+ export declare function loadMonitorNative(): Promise<ModuleLoadResult<MonitorNativeModule>>;
52
66
  export declare function getSafeBundleVersion(): Promise<string>;
53
67
  export {};
package/dist/runtime.js CHANGED
@@ -5,10 +5,12 @@ exports.ensureSupportedPlatform = ensureSupportedPlatform;
5
5
  exports.loadRNFS = loadRNFS;
6
6
  exports.loadDeviceInfo = loadDeviceInfo;
7
7
  exports.loadUnzip = loadUnzip;
8
+ exports.loadMonitorNative = loadMonitorNative;
8
9
  exports.getSafeBundleVersion = getSafeBundleVersion;
9
10
  const logger_1 = require("./logger");
10
11
  const UNKNOWN_BUNDLE_VERSION = '0.0.0';
11
12
  const loggedMessages = new Set();
13
+ const MONITOR_NATIVE_NAME = 'VkDeployMonitor';
12
14
  function logOnce(key, level, message, details) {
13
15
  if (loggedMessages.has(key)) {
14
16
  return;
@@ -135,6 +137,36 @@ async function loadUnzip() {
135
137
  module: unzip,
136
138
  };
137
139
  }
140
+ /**
141
+ * Carrega o modulo nativo de monitoramento.
142
+ *
143
+ * A ausencia do modulo e um caso esperado, nao um defeito: codigo nativo nao
144
+ * viaja em bundle OTA, entao um app que atualizou o vkdeploy-push por bundle sem
145
+ * gerar um APK novo continua sem ele. Nesse caso o monitoramento fica inativo e
146
+ * o restante do SDK segue funcionando normalmente.
147
+ */
148
+ async function loadMonitorNative() {
149
+ var _a;
150
+ const reactNative = await importModule('react-native');
151
+ if (!reactNative.ok) {
152
+ return reactNative;
153
+ }
154
+ const nativo = (_a = reactNative.module.NativeModules) === null || _a === void 0 ? void 0 : _a[MONITOR_NATIVE_NAME];
155
+ if (!nativo ||
156
+ typeof nativo.coletaSaidasAnormais !== 'function' ||
157
+ typeof nativo.suportaExitInfo !== 'function') {
158
+ const error = (0, logger_1.createError)('MONITOR_UNAVAILABLE', 'O modulo nativo "VkDeployMonitor" nao esta disponivel. Gere um novo APK apos atualizar o vkdeploy-push: codigo nativo nao e entregue por bundle OTA.');
159
+ logOnce('monitor:unavailable', 'warn', error.message, { code: error.code });
160
+ return {
161
+ ok: false,
162
+ error,
163
+ };
164
+ }
165
+ return {
166
+ ok: true,
167
+ module: nativo,
168
+ };
169
+ }
138
170
  async function getSafeBundleVersion() {
139
171
  const deviceInfo = await loadDeviceInfo();
140
172
  if (!deviceInfo.ok) {
package/dist/types.d.ts CHANGED
@@ -8,10 +8,51 @@ export interface VersionamentoConfig {
8
8
  app_secret: string;
9
9
  }
10
10
  export interface RemoteVersionResponse {
11
- versao: number;
11
+ versao?: number;
12
12
  mandatori?: boolean;
13
+ monitoramento?: MonitoramentoConfig;
14
+ }
15
+ /**
16
+ * Configuracao de monitoramento entregue pelo servidor junto da resposta de versao.
17
+ *
18
+ * O recurso nasce desligado e so e ligado pelo painel; o app nunca decide isso
19
+ * sozinho e nao ha como habilitar por arquivo de configuracao local.
20
+ */
21
+ export interface MonitoramentoConfig {
22
+ /** Indica se o projeto tem monitoramento habilitado no painel. */
23
+ ativo: boolean;
24
+ /**
25
+ * Epoch ms em que o monitoramento foi ligado, ou null quando desligado.
26
+ *
27
+ * Funciona como marca d'agua minima: saidas anormais anteriores a este instante
28
+ * sao descartadas, de modo que ligar a chave nunca envia o historico de falhas
29
+ * acumulado enquanto o recurso estava desligado.
30
+ */
31
+ ativadoEm: number | null;
32
+ }
33
+ /** Uma saida anormal do processo, como relatada pelo Android. */
34
+ export interface SaidaAnormal {
35
+ reason: number;
36
+ reasonLabel: string;
37
+ descricao?: string | null;
38
+ importancia?: number;
39
+ importanciaLabel?: string;
40
+ pss?: number;
41
+ rss?: number;
42
+ ocorridoEm: number;
43
+ trace?: string | null;
44
+ osVersion?: string;
45
+ modelo?: string;
46
+ fabricante?: string;
47
+ }
48
+ export type MonitoramentoStatus = 'desligado' | 'nao-suportado' | 'sem-novidades' | 'enviado' | 'erro';
49
+ export interface MonitoramentoResult {
50
+ status: MonitoramentoStatus;
51
+ /** Quantidade de saidas anormais enviadas ao servidor. */
52
+ enviados: number;
53
+ error?: VkDeployError;
13
54
  }
14
- export type VkDeployErrorCode = 'UNSUPPORTED_PLATFORM' | 'MISSING_DEPENDENCY' | 'INVALID_DEPENDENCY' | 'CONFIG_NOT_FOUND' | 'INVALID_CONFIG' | 'REMOTE_REQUEST_FAILED' | 'INVALID_REMOTE_RESPONSE' | 'DOWNLOAD_FAILED' | 'UNZIP_FAILED' | 'FILESYSTEM_ERROR' | 'UNKNOWN_ERROR';
55
+ export type VkDeployErrorCode = 'UNSUPPORTED_PLATFORM' | 'MISSING_DEPENDENCY' | 'INVALID_DEPENDENCY' | 'CONFIG_NOT_FOUND' | 'INVALID_CONFIG' | 'REMOTE_REQUEST_FAILED' | 'INVALID_REMOTE_RESPONSE' | 'DOWNLOAD_FAILED' | 'UNZIP_FAILED' | 'FILESYSTEM_ERROR' | 'MONITOR_UNAVAILABLE' | 'MONITOR_COLLECT_FAILED' | 'MONITOR_SEND_FAILED' | 'UNKNOWN_ERROR';
15
56
  export interface VkDeployError {
16
57
  code: VkDeployErrorCode;
17
58
  message: string;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buscProjetoId = buscProjetoId;
4
4
  const logger_1 = require("../logger");
5
5
  const runtime_1 = require("../runtime");
6
- const CONFIG_ASSET_PATH = 'versionamento-vktech-config.json';
6
+ const CONFIG_ASSET_PATH = 'vkdeploy-config.json';
7
7
  async function buscProjetoId() {
8
8
  var _a;
9
9
  const rnfs = await (0, runtime_1.loadRNFS)();
@@ -17,7 +17,7 @@ async function buscProjetoId() {
17
17
  if (!exists) {
18
18
  return {
19
19
  ok: false,
20
- error: (0, logger_1.createError)('CONFIG_NOT_FOUND', 'Arquivo de configuração não encontrado em android/app/src/main/assets/versionamento-vktech-config.json.'),
20
+ error: (0, logger_1.createError)('CONFIG_NOT_FOUND', 'Arquivo de configuração não encontrado em android/app/src/main/assets/vkdeploy-config.json.'),
21
21
  };
22
22
  }
23
23
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vkdeploy-push",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Biblioteca React Native para versionamento e atualizacao remota de bundle Android.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "android",
16
17
  "README.md",
17
18
  "LICENSE"
18
19
  ],
@@ -28,7 +29,10 @@
28
29
  "android",
29
30
  "bundle",
30
31
  "ota",
31
- "update"
32
+ "update",
33
+ "monitoring",
34
+ "crash",
35
+ "anr"
32
36
  ],
33
37
  "license": "MIT",
34
38
  "dependencies": {