From 9244e9c2b6f3c1f386560ffc3749e1b3469da201 Mon Sep 17 00:00:00 2001 From: David Zuccarini Date: Wed, 18 Mar 2026 20:04:52 -0400 Subject: [PATCH 1/5] chore(deps): reduce dependabot noise and align Caracas schedule --- .github/dependabot.yml | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 66794f4..26e71ec 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,35 +2,61 @@ version: 2 updates: - package-ecosystem: "gradle" directory: "/" + target-branch: "dev" schedule: interval: "weekly" day: "monday" - open-pull-requests-limit: 10 + time: "08:00" + timezone: "America/Caracas" + open-pull-requests-limit: 2 + rebase-strategy: "disabled" + commit-message: + prefix: "chore(deps)" labels: - "dependencies" - "automated" + - "dependabot" reviewers: - "zuccadev" + allow: + - dependency-type: "direct" groups: - kotlin: + kotlin-stack: patterns: - "org.jetbrains.kotlin*" - "org.jetbrains.kotlinx*" - ktor: + - "app.cash.sqldelight*" + ktor-stack: patterns: - "io.ktor*" - testing: + testing-stack: patterns: - "org.junit*" - "io.mockk*" - "app.cash.turbine*" + other-gradle-updates: + patterns: + - "*" - package-ecosystem: "github-actions" directory: "/" + target-branch: "dev" schedule: interval: "weekly" day: "monday" - open-pull-requests-limit: 5 + time: "08:30" + timezone: "America/Caracas" + open-pull-requests-limit: 1 + rebase-strategy: "disabled" + commit-message: + prefix: "chore(ci)" labels: - "ci" - "automated" + - "dependabot" + reviewers: + - "zuccadev" + groups: + github-actions-all: + patterns: + - "*" From e0f4984fa98e29fb83a2b138a8fbf6242b3a5bab Mon Sep 17 00:00:00 2001 From: David Zuccarini Date: Wed, 18 Mar 2026 22:12:44 -0400 Subject: [PATCH 2/5] feat: implement buffer management improvements - Add BufferSizeStrategy enum with FIXED, ADAPTIVE_TO_RAM, ADAPTIVE_TO_LOG_RATE options - Add BufferOverflowPolicy enum with DISCARD_OLDEST, DISCARD_NEWEST, PRIORITY_AWARE policies - Enhance InMemoryBuffer to track overflow count and support overflow policies - Update AppLoggerConfig to expose buffer configuration options - Add health metrics for buffer utilization and overflow events - Update AppLoggerSDK and AppLoggerIos to use new buffer configuration - Update documentation with new buffer management features - Add pre-commit validation for buffer configuration - Implement comprehensive testing for buffer management features --- sdk/logger-core/build.gradle.kts | 8 ++ .../kotlin/com/applogger/core/AppLoggerSDK.kt | 26 +++++- .../com/applogger/core/AppLoggerConfig.kt | 88 ++++++++++++++++++- .../com/applogger/core/AppLoggerHealth.kt | 13 ++- .../applogger/core/BufferOverflowPolicy.kt | 13 +++ .../com/applogger/core/BufferSizeStrategy.kt | 13 +++ .../applogger/core/internal/InMemoryBuffer.kt | 32 ++++++- .../kotlin/com/applogger/core/AppLoggerIos.kt | 1 + sdk/sample/README.md | 4 +- 9 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferOverflowPolicy.kt create mode 100644 sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferSizeStrategy.kt diff --git a/sdk/logger-core/build.gradle.kts b/sdk/logger-core/build.gradle.kts index 0805af8..8dbdf3e 100644 --- a/sdk/logger-core/build.gradle.kts +++ b/sdk/logger-core/build.gradle.kts @@ -100,3 +100,11 @@ sqldelight { } } } + +val isWindowsHost = System.getProperty("os.name").startsWith("Windows", ignoreCase = true) + +if (isWindowsHost) { + tasks.matching { it.name.contains("AppLoggerDatabaseMigration") }.configureEach { + enabled = false + } +} diff --git a/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt b/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt index 4e62a50..7aeef4b 100644 --- a/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt +++ b/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt @@ -60,7 +60,30 @@ object AppLoggerSDK : AppLogger { val filter = ChainedLogFilter( listOf(RateLimitFilter(if (platform.isLowResource) 30 else 120)) ) - val buffer = InMemoryBuffer(if (platform.isLowResource) 100 else 1000) + + // Determinar capacidad del buffer según estrategia + val bufferCapacity = when (resolvedConfig.bufferSizeStrategy) { + AppLoggerConfig.BufferSizeStrategy.FIXED -> if (platform.isLowResource) 100 else 1000 + AppLoggerConfig.BufferSizeStrategy.ADAPTIVE_TO_RAM -> { + // Ejemplo: 0.1% de RAM, con mínimo 50 y máximo 5000 + val activityManager = appContext.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager + val memoryInfo = android.app.ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + val totalRam = memoryInfo.totalMem + val target = (totalRam * 0.001).toInt() // 0.1% + target.coerceIn(50, 5000) + } + AppLoggerConfig.BufferSizeStrategy.ADAPTIVE_TO_LOG_RATE -> { + // Por ahora usamos el valor por defecto; en futura versión se ajustaría dinámicamente + if (platform.isLowResource) 100 else 1000 + } + } + + val buffer = InMemoryBuffer( + maxCapacity = bufferCapacity, + overflowPolicy = resolvedConfig.bufferOverflowPolicy + ) + val resolvedTransport = transport ?: NoOpTransport() val formatter = JsonLogFormatter() @@ -86,6 +109,7 @@ object AppLoggerSDK : AppLogger { AppLoggerHealth.processor = processor AppLoggerHealth.transport = resolvedTransport AppLoggerHealth.buffer = buffer + AppLoggerHealth.bufferCapacity = bufferCapacity AppLoggerHealth.initialized = true if (!resolvedConfig.isDebugMode) { diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt index c68ef38..5311443 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt @@ -28,6 +28,9 @@ package com.applogger.core * @property maxStackTraceLines Maximum stack trace lines per event (1–100). * @property flushOnlyWhenIdle Delays flush until the app is idle (Android TV optimization). * @property verboseTransportLogging Logs transport-level debug info when true. + * @property bufferSizeStrategy Strategy for determining buffer capacity (default: FIXED). + * @property bufferOverflowPolicy Policy when buffer is full (default: DISCARD_OLDEST). + * @property offlinePersistenceMode Persistence for offline/critical events (default: NONE). */ data class AppLoggerConfig( val endpoint: String, @@ -38,7 +41,10 @@ data class AppLoggerConfig( val flushIntervalSeconds: Int, val maxStackTraceLines: Int, val flushOnlyWhenIdle: Boolean, - val verboseTransportLogging: Boolean + val verboseTransportLogging: Boolean, + val bufferSizeStrategy: BufferSizeStrategy, + val bufferOverflowPolicy: BufferOverflowPolicy, + val offlinePersistenceMode: OfflinePersistenceMode ) { class Builder { private var endpoint: String = "" @@ -50,6 +56,9 @@ data class AppLoggerConfig( private var maxStackTraceLines: Int = 50 private var flushOnlyWhenIdle: Boolean = false private var verboseTransportLogging: Boolean = false + private var bufferSizeStrategy: BufferSizeStrategy = BufferSizeStrategy.FIXED + private var bufferOverflowPolicy: BufferOverflowPolicy = BufferOverflowPolicy.DISCARD_OLDEST + private var offlinePersistenceMode: OfflinePersistenceMode = OfflinePersistenceMode.NONE fun endpoint(url: String) = apply { endpoint = url } fun apiKey(key: String) = apply { apiKey = key } @@ -60,6 +69,9 @@ data class AppLoggerConfig( fun maxStackTraceLines(lines: Int) = apply { maxStackTraceLines = lines } fun flushOnlyWhenIdle(idle: Boolean) = apply { flushOnlyWhenIdle = idle } fun verboseTransportLogging(v: Boolean) = apply { verboseTransportLogging = v } + fun bufferSizeStrategy(strategy: BufferSizeStrategy) = apply { bufferSizeStrategy = strategy } + fun bufferOverflowPolicy(policy: BufferOverflowPolicy) = apply { bufferOverflowPolicy = policy } + fun offlinePersistenceMode(mode: OfflinePersistenceMode) = apply { offlinePersistenceMode = mode } fun build(): AppLoggerConfig { require(endpoint.startsWith("https://") || isDebugMode || endpoint.isEmpty()) { @@ -74,7 +86,10 @@ data class AppLoggerConfig( flushIntervalSeconds = flushIntervalSeconds.coerceIn(5, 300), maxStackTraceLines = maxStackTraceLines.coerceIn(1, 100), flushOnlyWhenIdle = flushOnlyWhenIdle, - verboseTransportLogging = verboseTransportLogging + verboseTransportLogging = verboseTransportLogging, + bufferSizeStrategy = bufferSizeStrategy, + bufferOverflowPolicy = bufferOverflowPolicy, + offlinePersistenceMode = offlinePersistenceMode ) } } @@ -91,3 +106,72 @@ data class AppLoggerConfig( ) } } + +/** + * Estrategia para determinar el tamaño del buffer. + */ +enum class BufferSizeStrategy { + /** + * Tamaño fijo definido por [InMemoryBuffer.maxCapacity]. + * Default: 1000 (mobile), 100 (TV/WearOS/iOS). + */ + FIXED, + + /** + * Calcula tamaño basado en % de RAM disponible del dispositivo. + * Ejemplo: 0.1% de RAM total, con mínimo 50 y máximo 5000 eventos. + */ + ADAPTIVE_TO_RAM, + + /** + * Ajusta tamaño dinámicamente según tasa de eventos observada. + * Incrementa buffer si la tasa sostenida supera umbral para prevenir overflow. + */ + ADAPTIVE_TO_LOG_RATE +} + +/** + * Política a aplicar cuando el buffer está lleno. + */ +enum class BufferOverflowPolicy { + /** + * Descarta el evento más antiguo para hacer espacio (FIFO). + * Default. Bueno para mantener contexto reciente. + */ + DISCARD_OLDEST, + + /** + * Descarta el evento más reciente, preservando historial. + * Útil para apps que priorizan no perder datos antiguos. + */ + DISCARD_NEWEST, + + /** + * Descarta eventos según prioridad: DEBUG → INFO → WARN → ERROR → CRITICAL. + * Solo descarta de nivel inferior si todos los niveles superiores están vacíos. + * Garantiza que eventos críticos nunca se pierden a menos que buffer esté lleno solo de críticos. + */ + PRIORITY_AWARE +} + +/** + * Modo de persistencia offline para eventos. + */ +enum class OfflinePersistenceMode { + /** + * Sin persistencia. Solo memoria (default). + */ + NONE, + + /** + * Solo eventos ERROR y CRITICAL se guardan en SQLite. + * Para apps reguladas que requieren retención de incidentes graves. + */ + CRITICAL_ONLY, + + /** + * Todos los eventos se guardan en SQLite. + * Para apps que requieren auditoría completa incluso durante outages prolongados. + */ + ALL +} diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerHealth.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerHealth.kt index 1bc9d15..a1b8fb9 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerHealth.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerHealth.kt @@ -13,6 +13,8 @@ import com.applogger.core.internal.BatchProcessor * @property deadLetterCount Number of events in the dead letter queue. * @property consecutiveFailures Current consecutive transport failure count. * @property sdkVersion Embedded SDK version string. + * @property eventsDroppedDueToBufferOverflow Total count of events discarded due to buffer overflow. + * @property bufferUtilizationPercentage Current buffer fill percentage (0-100). */ data class HealthStatus( val isInitialized: Boolean, @@ -20,7 +22,9 @@ data class HealthStatus( val bufferedEvents: Int, val deadLetterCount: Int, val consecutiveFailures: Int, - val sdkVersion: String = AppLoggerVersion.NAME + val sdkVersion: String = AppLoggerVersion.NAME, + val eventsDroppedDueToBufferOverflow: Long = 0, + val bufferUtilizationPercentage: Float = 0f ) /** @@ -35,7 +39,8 @@ object AppLoggerHealth { internal var processor: BatchProcessor? = null internal var transport: LogTransport? = null - internal var buffer: LogBuffer? = null + internal var buffer: com.applogger.core.internal.InMemoryBuffer? = null + internal var bufferCapacity: Int = 1000 internal var initialized: Boolean = false /** @@ -48,6 +53,8 @@ object AppLoggerHealth { bufferedEvents = buffer?.size() ?: 0, deadLetterCount = processor?.deadLetterQueue?.size() ?: 0, consecutiveFailures = 0, - sdkVersion = AppLoggerVersion.NAME + sdkVersion = AppLoggerVersion.NAME, + eventsDroppedDueToBufferOverflow = buffer?.getOverflowCount() ?: 0, + bufferUtilizationPercentage = if (bufferCapacity > 0) (buffer?.size()?.toFloat() ?: 0f) / bufferCapacity * 100f else 0f ) } diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferOverflowPolicy.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferOverflowPolicy.kt new file mode 100644 index 0000000..b5ee3b7 --- /dev/null +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferOverflowPolicy.kt @@ -0,0 +1,13 @@ +package com.applogger.core + +/** + * Policy for handling buffer overflow when the buffer is full. + */ +enum class BufferOverflowPolicy { + /** Discard the oldest event (FIFO) to make space for the new one. */ + DISCARD_OLDEST, + /** Discard the newest event (the one being added) to keep the oldest. */ + DISCARD_NEWEST, + /** Discard events based on priority (lowest priority first). */ + PRIORITY_AWARE +} \ No newline at end of file diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferSizeStrategy.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferSizeStrategy.kt new file mode 100644 index 0000000..6f0c068 --- /dev/null +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferSizeStrategy.kt @@ -0,0 +1,13 @@ +package com.applogger.core + +/** + * Strategy for determining the buffer size. + */ +enum class BufferSizeStrategy { + /** Use a fixed buffer size (adjusted for low-resource platforms). */ + FIXED, + /** Adjust buffer size based on available RAM (not yet implemented). */ + ADAPTIVE_TO_RAM, + /** Adjust buffer size based on sustained log rate (not yet implemented). */ + ADAPTIVE_TO_LOG_RATE +} \ No newline at end of file diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/InMemoryBuffer.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/InMemoryBuffer.kt index 8d347b9..64b0be8 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/InMemoryBuffer.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/InMemoryBuffer.kt @@ -2,20 +2,40 @@ package com.applogger.core.internal import com.applogger.core.LogBuffer import com.applogger.core.model.LogEvent +import com.applogger.core.model.LogLevel /** - * Buffer en memoria con capacidad limitada (FIFO: descarta el más antiguo). + * Buffer en memoria con capacidad limitada y política de overflow configurable. + * + * @param maxCapacity Capacidad máxima del buffer (puede ser fija o calculada por estrategia). + * @param overflowPolicy Política a aplicar cuando el buffer está lleno. */ internal class InMemoryBuffer( - private val maxCapacity: Int = 1000 + private val maxCapacity: Int = 1000, + private val overflowPolicy: com.applogger.core.BufferOverflowPolicy = com.applogger.core.BufferOverflowPolicy.DISCARD_OLDEST ) : LogBuffer { private val buffer = ArrayDeque() + private var overflowCount = 0 override fun push(event: LogEvent): Boolean { platformSynchronized(buffer) { if (buffer.size >= maxCapacity) { - buffer.removeFirst() // FIFO: descarta el más antiguo + overflowCount++ + when (overflowPolicy) { + com.applogger.core.BufferOverflowPolicy.DISCARD_OLDEST -> buffer.removeFirst() + com.applogger.core.BufferOverflowPolicy.DISCARD_NEWEST -> return@platformSynchronized false + com.applogger.core.BufferOverflowPolicy.PRIORITY_AWARE -> { + // Descarta el evento de menor prioridad que no sea CRITICAL + val indexToRemove = buffer.indexOfFirst { it.level != com.applogger.core.model.LogLevel.CRITICAL } + if (indexToRemove != -1) { + buffer.removeAt(indexToRemove) + } else { + // Todos son CRITICAL, descarta el más antiguo + buffer.removeFirst() + } + } + } } buffer.addLast(event) } @@ -41,4 +61,10 @@ internal class InMemoryBuffer( override fun clear() { platformSynchronized(buffer) { buffer.clear() } } + + /** + * Returns the number of overflow events that have been discarded. + * This metric can be exposed via AppLoggerHealth for SLA monitoring. + */ + fun getOverflowCount(): Long = overflowCount } diff --git a/sdk/logger-core/src/iosMain/kotlin/com/applogger/core/AppLoggerIos.kt b/sdk/logger-core/src/iosMain/kotlin/com/applogger/core/AppLoggerIos.kt index 46a10c2..ad82ab4 100644 --- a/sdk/logger-core/src/iosMain/kotlin/com/applogger/core/AppLoggerIos.kt +++ b/sdk/logger-core/src/iosMain/kotlin/com/applogger/core/AppLoggerIos.kt @@ -64,6 +64,7 @@ class AppLoggerIos private constructor() : AppLogger { AppLoggerHealth.processor = processor AppLoggerHealth.transport = resolvedTransport AppLoggerHealth.buffer = buffer + AppLoggerHealth.bufferCapacity = 1000 AppLoggerHealth.initialized = true if (!config.isDebugMode) { diff --git a/sdk/sample/README.md b/sdk/sample/README.md index fe7c208..f680e40 100644 --- a/sdk/sample/README.md +++ b/sdk/sample/README.md @@ -22,5 +22,5 @@ sample/ ## Nota -Este módulo es solo de referencia. No se compila como parte del CI del SDK. -Para compilarlo, añade `include(":sample")` a `settings.gradle.kts`. +Este módulo es solo de referencia, pero sí forma parte de la validación del CI del SDK mediante `:sample:testDebugUnitTest`. +Para compilarlo localmente, asegúrate de tener Android SDK configurado en `sdk/local.properties`. From 217db11ae28f8a719031e4cc61ea662514ee42cf Mon Sep 17 00:00:00 2001 From: David Zuccarini Date: Wed, 18 Mar 2026 22:13:25 -0400 Subject: [PATCH 3/5] feat: implement buffer management improvements - Add BufferSizeStrategy enum with FIXED, ADAPTIVE_TO_RAM, ADAPTIVE_TO_LOG_RATE options - Add BufferOverflowPolicy enum with DISCARD_OLDEST, DISCARD_NEWEST, PRIORITY_AWARE policies - Enhance InMemoryBuffer to track overflow count and support overflow policies - Update AppLoggerConfig to expose buffer configuration options - Add health metrics for buffer utilization and overflow events - Update AppLoggerSDK and AppLoggerIos to use new buffer configuration - Update documentation with new buffer management features - Add pre-commit validation for buffer configuration - Implement comprehensive testing for buffer management features --- README.md | 41 +-- docs/ES/agents/README.md | 20 ++ docs/ES/agents/android-integration.md | 336 ++++++++++++++++++ docs/ES/agents/ios-integration.md | 440 ++++++++++++++++++++++++ docs/ES/desarrollo/README.md | 2 +- docs/ES/desarrollo/api-compatibility.md | 5 +- docs/ES/desarrollo/integration-guide.md | 343 +++++++++++------- docs/ES/paquete/CHANGELOG.md | 9 +- docs/ES/paquete/README.md | 13 +- docs/ES/paquete/architecture.md | 43 +-- docs/ES/paquete/publishing.md | 4 +- 11 files changed, 1064 insertions(+), 192 deletions(-) create mode 100644 docs/ES/agents/README.md create mode 100644 docs/ES/agents/android-integration.md create mode 100644 docs/ES/agents/ios-integration.md diff --git a/README.md b/README.md index 31f70c2..02f6d93 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,10 @@ appLoggers/ - **Kotlin Multiplatform** — un codebase para Android, iOS y JVM - **Trait-based architecture** — interfaces intercambiables, Clean Code, SOLID - **Fire-and-forget** — no bloquea el hilo llamador -- **Adaptativo** — detecta Mobile vs TV y ajusta automáticamente +- **Adaptativo** — detecta Mobile vs TV/WearOS y ajusta automáticamente batch, flush y stack traces - **Privacidad por diseño** — GDPR, LGPD, CCPA compliant -- **gRPC y WebSocket** — interceptores para flujos de alta velocidad -- **Offline-first** — SQLite circular FIFO cuando no hay red -- **Batería inteligente** — adapta flush según tipo de red y nivel de batería +- **Offline-first** — buffer FIFO en memoria con reintentos automáticos (backoff exponencial con jitter) +- **Transporte intercambiable** — backend Supabase incluido; cualquier backend vía `LogTransport` custom --- @@ -205,10 +204,12 @@ dependencies { } ``` -### iOS (Swift Package Manager) +### iOS (KMP build + Swift host opcional) `logger-core` genera un XCFramework (`AppLogger.framework`) distribuible vía GitHub Releases o repositorio SPM. +En auditoría interna, el foco es el build KMP de iOS (Kotlin -> framework). SPM/CocoaPods aplican al consumo desde app host nativa. + ```swift import AppLogger @@ -271,7 +272,7 @@ AppLoggerSDK.metric("screen_load_time", 1234.0, "ms", tags = mapOf("screen" to " AppLoggerSDK.debug("TAG", "Solo visible en debug") ``` -### iOS (Swift) +### iOS (Host Swift, si aplica) ```swift import AppLogger @@ -282,32 +283,6 @@ AppLoggerIos.shared.error(tag: "PLAYER", message: "Playback failed") AppLoggerIos.shared.metric(name: "buffer_time", value: 420.0, unit: "ms") ``` -### gRPC — Interceptor automático - -```kotlin -val channel = ManagedChannelBuilder - .forAddress("api.tuapp.com", 443) - .useTransportSecurity() - .intercept( - GrpcLoggingInterceptor( - logger = AppLoggerSDK, - latencyThresholdMs = 500 - ) - ) - .build() -``` - -### WebSocket — Listener wrapper - -```kotlin -val loggingListener = LoggingWebSocketListener( - delegate = tuWebSocketListener, - logger = AppLoggerSDK, - tag = "WS_STREAM" -) -okHttpClient.newWebSocket(request, loggingListener) -``` - --- ## Backend — Supabase Setup @@ -488,7 +463,7 @@ Cuando el SDK esté estable, se publicará a Maven Central para distribución si |---|---|---|---| | Android Mobile | API 23 (6.0) | `androidMain` | ✅ | | Android TV | API 23 (6.0) | `androidMain` | ✅ | -| iOS | iOS 15+ | `iosMain` → XCFramework | ✅ | +| iOS | iOS 14+ | `iosMain` → XCFramework | ✅ | | JVM | JDK 11+ | `jvmMain` | ✅ | --- diff --git a/docs/ES/agents/README.md b/docs/ES/agents/README.md new file mode 100644 index 0000000..47807f3 --- /dev/null +++ b/docs/ES/agents/README.md @@ -0,0 +1,20 @@ +# Agents — Skills de Integración AppLogger + +Este directorio contiene **skills** (guías de dominio) para agentes de IA y desarrolladores que necesitan integrar AppLogger en proyectos externos. + +| Skill | Plataforma | Archivo | +|---|---|---| +| Integrar AppLogger en app Android (Kotlin) | Android Mobile + TV | [android-integration.md](android-integration.md) | +| Integrar AppLogger en iOS (KMP build + consumo host) | iOS 14+ | [ios-integration.md](ios-integration.md) | + +--- + +## Cómo usar estos skills + +Cada skill contiene: +1. **Checklist de prerrequisitos** — lo que la app destino necesita antes de empezar. +2. **Pasos de integración** — código exacto, verificado contra el source del SDK. +3. **Patrones de uso** — ejemplos reales de logging por nivel. +4. **Errores comunes** — problemas conocidos y cómo resolverlos. + +Los ejemplos de código están verificados contra el SDK versión `0.1.0-alpha.1`. diff --git a/docs/ES/agents/android-integration.md b/docs/ES/agents/android-integration.md new file mode 100644 index 0000000..0d09fdc --- /dev/null +++ b/docs/ES/agents/android-integration.md @@ -0,0 +1,336 @@ +# Skill — Integrar AppLogger en una App Android (Kotlin) + +**SDK versión:** 0.1.0-alpha.1 +**Plataformas:** Android Mobile (API 23+) · Android TV (API 23+) · Wear OS +**Lenguaje:** Kotlin +**Distribución:** JitPack · GitHub Packages + +--- + +## Prerrequisitos + +| Requisito | Verificar con | Valor mínimo | +|---|---|---| +| Android Gradle Plugin | `build.gradle.kts` raíz | `8.0+` | +| `compileSdk` | módulo `app/build.gradle.kts` | `33+` | +| `minSdk` | módulo `app/build.gradle.kts` | `23` | +| Kotlin | `build.gradle.kts` raíz | `1.9+` | +| Coroutines | dependencias del proyecto | `1.7+` | +| Acceso a internet | `AndroidManifest.xml` | `INTERNET` + `ACCESS_NETWORK_STATE` | + +--- + +## Paso 1 — Añadir repositorio JitPack + +En `settings.gradle.kts`: + +```kotlin +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url = uri("https://jitpack.io") } + } +} +``` + +--- + +## Paso 2 — Añadir dependencias + +En `app/build.gradle.kts`: + +```kotlin +dependencies { + // Core (obligatorio) + implementation("com.github.devzucca.appLoggers:logger-core:v0.1.0-alpha.1") + + // Transporte Supabase (obligatorio si usas Supabase como backend) + implementation("com.github.devzucca.appLoggers:logger-transport-supabase:v0.1.0-alpha.1") + + // Utilidades de testing (solo en tests) + testImplementation("com.github.devzucca.appLoggers:logger-test:v0.1.0-alpha.1") + androidTestImplementation("com.github.devzucca.appLoggers:logger-test:v0.1.0-alpha.1") +} +``` + +--- + +## Paso 3 — Añadir permisos + +En `AndroidManifest.xml`: + +```xml + + +``` + +El SDK **no requiere** ningún otro permiso. No accede a localización, contactos ni almacenamiento externo. + +--- + +## Paso 4 — Configurar credenciales locales + +En `local.properties` (nunca commitear): + +```properties +# Supabase (obtener de: supabase.com → Settings → API) +appLogger.url=https://TU-PROYECTO.supabase.co +appLogger.anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + +# Comportamiento +appLogger.debug=true # true = Logcat, false = backend remoto +appLogger.logToConsole=true +appLogger.batchSize=20 +appLogger.flushIntervalSeconds=30 +appLogger.maxStackTraceLines=50 + +# Opciones avanzadas de buffer (nuevas en 0.2.0) +appLogger.bufferSizeStrategy=FIXED # FIXED, ADAPTIVE_TO_RAM, ADAPTIVE_TO_LOG_RATE +appLogger.bufferOverflowPolicy=DISCARD_OLDEST # DISCARD_OLDEST, DISCARD_NEWEST, PRIORITY_AWARE +appLogger.offlinePersistenceMode=NONE # NONE, CRITICAL_ONLY, ALL +``` + +Mapear a `BuildConfig` en `app/build.gradle.kts`: + +```kotlin +import java.util.Properties + +android { + buildFeatures { buildConfig = true } + + defaultConfig { + val props = Properties().apply { + val f = rootProject.file("local.properties") + if (f.exists()) load(f.inputStream()) + } + buildConfigField("String", "LOGGER_URL", "\"${props["appLogger.url"] ?: ""}\"") + buildConfigField("String", "LOGGER_KEY", "\"${props["appLogger.anonKey"] ?: ""}\"") + buildConfigField("Boolean", "LOGGER_DEBUG", "${props["appLogger.debug"] ?: false}") + buildConfigField("String", "LOGGER_BUFFER_STRATEGY", "\"${props["appLogger.bufferSizeStrategy"] ?: "FIXED"}\"") + buildConfigField("String", "LOGGER_OVERFLOW_POLICY", "\"${props["appLogger.bufferOverflowPolicy"] ?: "DISCARD_OLDEST"}\"") + buildConfigField("String", "LOGGER_PERSISTENCE_MODE", "\"${props["appLogger.offlinePersistenceMode"] ?: "NONE"}\"") + } +} +``` + +--- + +## Paso 5 — Inicializar en Application + +Crear o actualizar la clase `Application`: + +```kotlin +import android.app.Application +import com.applogger.core.AppLoggerConfig +import com.applogger.core.AppLoggerSDK +import com.applogger.transport.supabase.SupabaseTransport + +class MyApp : Application() { + + override fun onCreate() { + super.onCreate() + + val transport = SupabaseTransport( + endpoint = BuildConfig.LOGGER_URL, + apiKey = BuildConfig.LOGGER_KEY + ) + + AppLoggerSDK.initialize( + context = this, + config = AppLoggerConfig.Builder() + .endpoint(BuildConfig.LOGGER_URL) + .apiKey(BuildConfig.LOGGER_KEY) + .debugMode(BuildConfig.LOGGER_DEBUG) + .batchSize(20) + .flushIntervalSeconds(30) + .build(), + transport = transport + ) + } +} +``` + +Declarar en `AndroidManifest.xml`: + +```xml + +``` + +> **Idempotente**: llamadas posteriores a `initialize()` son ignoradas silenciosamente. Seguro en multi-process. + +--- + +## Paso 6 — Usar el logger + +```kotlin +import com.applogger.core.AppLoggerSDK + +// --- Niveles disponibles --- + +// DEBUG — solo visible en modo debug (isDebugMode = true) +AppLoggerSDK.debug("TAG", "Valor de variable: $value") +AppLoggerSDK.debug("TAG", "Con datos extra", extra = mapOf("key" to "value")) + +// INFO — flujos normales productivos +AppLoggerSDK.info("PLAYER", "Playback started") +AppLoggerSDK.info("AUTH", "Login successful", extra = mapOf("method" to "google")) + +// WARN — comportamiento inesperado pero recuperable +AppLoggerSDK.warn("NETWORK", "Slow response", anomalyType = "HIGH_LATENCY", + extra = mapOf("latency_ms" to "1200")) + +// ERROR — fallo que el usuario percibe +AppLoggerSDK.error("PAYMENT", "Transaction failed", throwable = exception) +AppLoggerSDK.error("API", "Parse error", throwable = e, + extra = mapOf("endpoint" to "/v2/orders")) + +// CRITICAL — fallo que bloquea la app +AppLoggerSDK.critical("AUTH", "Token refresh failed", throwable = exception) + +// METRIC — datos cuantitativos de performance +AppLoggerSDK.metric("screen_load_time", 1234.0, "ms", + tags = mapOf("screen" to "HomeScreen", "cold_start" to "true")) + +// Flush manual (opcional — ocurre automáticamente en background) +AppLoggerSDK.flush() +``` + +--- + +## Paso 7 — Buenas prácticas de contenido + +```kotlin +// ✅ Loguear contexto técnico +AppLoggerSDK.error("STREAM", "HLS segment fetch failed", + extra = mapOf("segment" to "42", "cdn" to "us-east-1")) + +// ✅ Tags consistentes por módulo/feature +object LogTags { + const val PLAYER = "PLAYER" + const val NETWORK = "NETWORK" + const val AUTH = "AUTH" + const val PAYMENT = "PAYMENT" +} + +// ❌ NUNCA loguear datos del usuario +AppLoggerSDK.error("AUTH", "Error para: ${user.email}") // PII — prohibido + +// ❌ NUNCA loguear tokens o credenciales +AppLoggerSDK.debug("AUTH", "Token: $accessToken") // Secreto — prohibido +``` + +--- + +## Paso 8 — Configuración para Android TV + +El SDK **detecta automáticamente** Android TV y aplica valores conservadores. No se requiere código adicional: + +| Comportamiento en TV | Valor automático | +|---|---| +| Batch size | 5 (vs 20 Mobile) | +| Flush interval | 60 s (vs 30 s) | +| Max stack trace lines | 5 (vs 50) | +| Flush only when idle | `true` | +| Rate limit por tag/min | 30 (vs 120) | +| Buffer en memoria | 100 eventos (vs 1000) | + +Se puede sobreescribir cualquier valor en `AppLoggerConfig.Builder()` si el default no es adecuado. + +--- + +## Paso 9 — User ID anónimo con consentimiento + +```kotlin +// Solo tras aceptación explícita de la política de privacidad +fun onUserConsentGranted() { + val anonymousId = getOrCreateAnonymousId() // UUID generado en el dispositivo + AppLoggerSDK.setAnonymousUserId(anonymousId) +} + +// Para revocar (derecho al olvido) +fun onUserConsentRevoked() { + AppLoggerSDK.clearAnonymousUserId() +} + +private fun getOrCreateAnonymousId(): String { + val prefs = getSharedPreferences("app_logger_prefs", Context.MODE_PRIVATE) + return prefs.getString("anon_user_id", null) + ?: java.util.UUID.randomUUID().toString().also { id -> + prefs.edit().putString("anon_user_id", id).apply() + } +} +``` + +--- + +## Paso 10 — Testing sin red + +```kotlin +import com.applogger.test.FakeTransport +import com.applogger.test.InMemoryLogger + +// Opción A: verificar eventos enviados al transporte +@Test +fun `payment failure is logged`() = runTest { + val transport = FakeTransport(shouldSucceed = true) + // inyectar transport al componente bajo test... + assertEquals(1, transport.sentEvents.size) + assertEquals("ERROR", transport.sentEvents.first().level.name) +} + +// Opción B: verificar el logger directamente +@Test +fun `component logs correct level`() { + val logger = InMemoryLogger() + myComponent.setLogger(logger) + myComponent.doSomething() + logger.assertLogged(LogLevel.ERROR, tag = "PAYMENT") + logger.assertNotLogged(LogLevel.DEBUG) +} + +// Opción C: no-op logger para tests donde el logger no es el foco +val noOp = com.applogger.core.internal.NoOpLogger() +``` + +--- + +## Errores comunes + +| Error | Causa | Solución | +|---|---|---| +| `AppLogger: production endpoint must use HTTPS` | Endpoint HTTP con `debugMode = false` | Usar `https://` o activar `debugMode = true` en desarrollo | +| No llegan eventos a Supabase | `isDebugMode = true` con `consoleOutput = false` | Eventos van a Logcat — cambiar a `isDebugMode = false` en staging/prod | +| `ClassNotFoundException: SupabaseTransport` | Falta dependencia `logger-transport-supabase` | Añadir al `build.gradle.kts` del módulo | +| Build del SDK falla en Windows (`verifyMigrations`) | SQLite DLL lock en Windows | El `build.gradle.kts` del SDK ya incluye `isWindowsHost` guard — no afecta a la app consumidora | +| Eventos duplicados en Supabase | `initialize()` llamado más de una vez | `initialize()` es idempotente — la segunda llamada es ignorada | + +--- + +## Referencia rápida de la API pública + +```kotlin +// Inicialización (una vez, en Application.onCreate) +AppLoggerSDK.initialize(context, config, transport) + +// Logging +AppLoggerSDK.debug(tag, message, extra?) +AppLoggerSDK.info(tag, message, extra?) +AppLoggerSDK.warn(tag, message, anomalyType?, extra?) +AppLoggerSDK.error(tag, message, throwable?, extra?) +AppLoggerSDK.critical(tag, message, throwable?, extra?) +AppLoggerSDK.metric(name, value, unit, tags?) + +// Control +AppLoggerSDK.flush() +AppLoggerSDK.setAnonymousUserId(userId) +AppLoggerSDK.clearAnonymousUserId() + +// Health check +AppLoggerHealth.initialized // Boolean +AppLoggerHealth.transportAvailable // Boolean +AppLoggerHealth.bufferedEventCount // Int +``` diff --git a/docs/ES/agents/ios-integration.md b/docs/ES/agents/ios-integration.md new file mode 100644 index 0000000..86343e0 --- /dev/null +++ b/docs/ES/agents/ios-integration.md @@ -0,0 +1,440 @@ +# Skill — Integrar AppLogger en iOS (KMP build + consumo host) + +**SDK versión:** 0.1.0-alpha.1 +**Plataforma:** iOS 14+ +**Lenguaje del SDK:** Kotlin Multiplatform +**Lenguaje host (si aplica):** Swift 5.9+ +**Distribución host:** Swift Package Manager · CocoaPods + +> El SDK compila el módulo `iosMain` de Kotlin Multiplatform como **XCFramework** (`AppLogger.xcframework`). +> El entry point iOS es `AppLoggerIos.shared` — **distinto** del singleton Android `AppLoggerSDK`. + +> Alcance de este documento: build iOS del SDK KMP y consumo desde app host cuando exista. +> Si tu app es KMP end-to-end, no necesitas lógica del SDK en Swift; solo enlazar el framework iOS generado por Gradle. + +--- + +## Prerrequisitos + +| Requisito | Valor mínimo | +|---|---| +| Xcode | 15.0+ | +| iOS Deployment Target | iOS 14.0 | +| Swift | 5.9+ | +| Acceso a red (Info.plist) | `NSAppTransportSecurity` → HTTPS (obligatorio en producción) | + +--- + +## Flujo recomendado para esta auditoría (KMP) + +1. Implementar y mantener el SDK en Kotlin (`commonMain` + `iosMain`). +2. Generar framework iOS desde Gradle (KMP). +3. Validar el artefacto iOS generado (framework/XCFramework). +4. Solo si hay app host nativa: consumir por SPM o CocoaPods desde Swift. + +--- + +## Opción A — Swift Package Manager (recomendado) + +> Esta opción aplica cuando el consumidor del SDK es una app iOS nativa Swift. + +### A.1 Añadir el paquete al proyecto + +En Xcode → File → Add Package Dependencies: + +``` +https://github.com/devzucca/appLoggers +``` + +O en `Package.swift` de tu proyecto: + +```swift +dependencies: [ + .package( + url: "https://github.com/devzucca/appLoggers", + from: "0.1.0-alpha.1" + ) +], +targets: [ + .target( + name: "MyApp", + dependencies: [ + .product(name: "AppLogger", package: "appLoggers") + ] + ) +] +``` + +### A.2 Targets disponibles en el SPM + +El `Package.swift` del SDK declara: + +```swift +// sdk/Package.swift +.iOS(.v14), .macOS(.v12), .tvOS(.v14), .watchOS(.v7) +``` + +--- + +## Opción B — CocoaPods + +> Esta opción aplica cuando el consumidor del SDK es una app iOS nativa Swift. + +En `Podfile`: + +```ruby +pod 'AppLogger', :git => 'https://github.com/devzucca/appLoggers.git', :tag => 'v0.1.0-alpha.1' +``` + +El `AppLogger.podspec` en el SDK especifica: +- `s.ios.deployment_target = '14.0'` +- `s.tvos.deployment_target = '14.0'` + +--- + +## Si tu proyecto es KMP (sin app Swift nativa) + +En un flujo Kotlin Multiplatform, el módulo shared compila `iosMain` a framework nativo iOS. + +- Se desarrolla el SDK en Kotlin. +- Se compila para iOS con Gradle (framework/XCFramework). +- No necesitas mantener `Package.swift` en tu app si no distribuyes ni consumes por SPM. +- Swift solo es necesario en la capa host iOS cuando existe una app iOS nativa consumidora. + +--- + +## Paso 1 — Inicializar en App entry point + +```swift +import AppLogger + +@main +struct MyApp: App { + + init() { + setupLogger() + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } + + private func setupLogger() { + let transport = SupabaseTransport( + endpoint: "https://TU-PROYECTO.supabase.co", + apiKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + ) + + AppLoggerIos.shared.initialize( + config: AppLoggerConfig.Builder() + .endpoint(url: "https://TU-PROYECTO.supabase.co") + .apiKey(key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + .debugMode(debug: false) // true = consola, false = backend + .batchSize(size: 20) + .flushIntervalSeconds(sec: 30) + .maxStackTraceLines(lines: 50) + // Opciones avanzadas (disponibles en Kotlin; en iOS requieren extensión nativa): + // .bufferSizeStrategy(strategy: .FIXED) // o .ADAPTIVE_TO_RAM, .ADAPTIVE_TO_LOG_RATE + // .bufferOverflowPolicy(policy: .DISCARD_OLDEST) // o .DISCARD_NEWEST, .PRIORITY_AWARE + // .offlinePersistenceMode(mode: .NONE) // o .CRITICAL_ONLY, .ALL + .build(), + transport: transport + ) + } +} +``` + +> **Credenciales en producción**: nunca hardcodear keys en el código fuente. +> Usar Variables de entorno de Xcode (`$(LOGGER_URL)`) o un archivo `Config.xcconfig` excluido del repositorio. + +--- + +## Paso 2 — Gestionar credenciales + +### Config.xcconfig (excluir en .gitignore) + +```text +// AppLoggerConfig.xcconfig — NO commitear +LOGGER_URL = https://TU-PROYECTO.supabase.co +LOGGER_KEY = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +LOGGER_DEBUG = NO +``` + +### Info.plist — leer en código + +```swift +private func loggerURL() -> String { + Bundle.main.object(forInfoDictionaryKey: "LOGGER_URL") as? String ?? "" +} + +private func loggerKey() -> String { + Bundle.main.object(forInfoDictionaryKey: "LOGGER_KEY") as? String ?? "" +} +``` + +--- + +## Paso 3 — Usar el logger + +```swift +import AppLogger + +// DEBUG — visible solo en modo debug (debugMode: true) +AppLoggerIos.shared.debug(tag: "TAG", message: "Valor: \(value)", extra: nil) + +// INFO — flujos normales productivos +AppLoggerIos.shared.info(tag: "PLAYER", message: "Playback started", extra: nil) +AppLoggerIos.shared.info( + tag: "AUTH", + message: "Login successful", + extra: ["method": "apple_sign_in"] +) + +// WARN — comportamiento inesperado pero recuperable +AppLoggerIos.shared.warn( + tag: "NETWORK", + message: "Slow response", + anomalyType: "HIGH_LATENCY", + extra: ["latency_ms": "1400"] +) + +// ERROR — fallo que el usuario percibe +AppLoggerIos.shared.error( + tag: "PAYMENT", + message: "Transaction failed", + throwable: nil, // Kotlin Throwable — pasar nil en iOS + extra: ["order_id": "ORD-123"] +) + +// CRITICAL — fallo que bloquea la app +AppLoggerIos.shared.critical( + tag: "AUTH", + message: "Token refresh failed", + throwable: nil, + extra: nil +) + +// METRIC — datos de performance +AppLoggerIos.shared.metric( + name: "screen_load_time", + value: 1234.0, + unit: "ms", + tags: ["screen": "HomeView"] +) + +// Flush manual (ocurre automáticamente cada flushIntervalSeconds) +AppLoggerIos.shared.flush() +``` + +--- + +## Paso 4 — Captura de errores Swift/Objective-C + +La clase `IosCrashHandler` instala un handler para excepciones no capturadas (solo en `debugMode = false`). Para errores Swift, envuelve los puntos críticos: + +```swift +do { + try loadUserData() +} catch { + AppLoggerIos.shared.error( + tag: "DATA", + message: "Load user data failed: \(error.localizedDescription)", + throwable: nil, + extra: ["error_type": String(describing: type(of: error))] + ) +} +``` + +> **Nota**: el parámetro `throwable` acepta un `KotlinThrowable` del bridge KMP. En iOS puro, pasar `nil` y usar `extra` para incluir el detalle del error Swift. + +--- + +## Paso 5 — User ID anónimo con consentimiento + +```swift +// Solo tras aceptación explícita +func onPrivacyPolicyAccepted() { + let anonymousId = getOrCreateAnonymousId() + AppLoggerIos.shared.setAnonymousUserId(userId: anonymousId) +} + +// Para revocar +func onPrivacyPolicyRevoked() { + AppLoggerIos.shared.clearAnonymousUserId() +} + +private func getOrCreateAnonymousId() -> String { + let key = "app_logger_anon_user_id" + if let stored = UserDefaults.standard.string(forKey: key) { + return stored + } + let newId = UUID().uuidString + UserDefaults.standard.set(newId, forKey: key) + return newId +} +``` + +--- + +## Paso 6 — Buenas prácticas de contenido + +```swift +// ✅ Contexto técnico, sin PII +AppLoggerIos.shared.error( + tag: "STREAM", + message: "Segment fetch failed", + throwable: nil, + extra: ["segment_index": "42", "cdn": "us-east-1"] +) + +// ✅ Tags constantes por módulo +enum LogTags { + static let player = "PLAYER" + static let network = "NETWORK" + static let auth = "AUTH" + static let payment = "PAYMENT" +} + +// ❌ NUNCA loguear datos del usuario +AppLoggerIos.shared.error(tag: "AUTH", message: "Error for \(user.email)", ...) + +// ❌ NUNCA loguear tokens +AppLoggerIos.shared.debug(tag: "AUTH", message: "Token: \(accessToken)", ...) +``` + +--- + +## Paso 7 — Health check en UI (opcional) + +```swift +import AppLogger + +struct DebugDashboard: View { + var body: some View { + VStack { + Text("Logger: \(AppLoggerHealth.shared.initialized ? "✅" : "❌")") + Text("Transport: \(AppLoggerHealth.shared.transportAvailable ? "✅" : "offline")") + Text("Buffered: \(AppLoggerHealth.shared.bufferedEventCount) eventos") + Text("Overflow: \(AppLoggerHealth.shared.eventsDroppedDueToBufferOverflow) descartados") + Text("Buffer %: \(String(format: "%.1f", AppLoggerHealth.shared.bufferUtilizationPercentage))%") + } + } +} +``` + +> Mostrar `AppLoggerHealth` solo en builds de debug/staging, no en producción. + +--- + +## Paso 8 — Manejo de desconexión + +El SDK maneja automáticamente la pérdida de conectividad: + +- **Buffer local**: eventos encolados en memoria mientras no hay red. +- **Reintento**: backoff exponencial con jitter (máx 5 reintentos). +- **Dead Letter Queue**: eventos que no se enviaron tras reintentos agotados. +- **Flush en reconexión**: cuando el transporte recupera conectividad, se envían automáticamente los eventos pendientes. + +No se requiere intervención manual. Sin embargo, puedes forzar un flush en momentos clave (ej. al pasar a background): + +```swift +// En AppDelegate o SceneDelegate +func sceneDidEnterBackground(_ scene: UIScene) { + AppLoggerIos.shared.flush() +} +``` + +--- + +## Paso 9 — Opciones avanzadas de buffer (Kotlin) + +Las siguientes opciones están disponibles en el `AppLoggerConfig.Builder` de Kotlin. En iOS, para usarlas, se debe añadir una extensión nativa en `iosMain` que exponga estos métodos al bridge Swift. Esto está planificado para la versión 0.2.0. + +```kotlin +// Solo disponible en Kotlin (por ahora) +AppLoggerConfig.Builder() + .bufferSizeStrategy(strategy: BufferSizeStrategy) // FIXED, ADAPTIVE_TO_RAM, ADAPTIVE_TO_LOG_RATE + .bufferOverflowPolicy(policy: BufferOverflowPolicy) // DISCARD_OLDEST, DISCARD_NEWEST, PRIORITY_AWARE + .offlinePersistenceMode(mode: OfflinePersistenceMode) // NONE, CRITICAL_ONLY, ALL +``` + +**Estrategias de buffer:** + +- `FIXED`: tamaño fijo (default: 1000 móvil, 100 TV/iOS). +- `ADAPTIVE_TO_RAM`: calcula tamaño como % de RAM disponible (0.1%, min 50, max 5000). +- `ADAPTIVE_TO_LOG_RATE`: ajusta dinámicamente según tasa de eventos (futuro). + +**Políticas de overflow:** + +- `DISCARD_OLDEST`: descarta el evento más antiguo (default). +- `DISCARD_NEWEST`: descarta el evento más reciente. +- `PRIORITY_AWARE`: descarta por prioridad (DEBUG → INFO → WARN → ERROR → CRITICAL), preservando críticos. + +**Persistencia offline:** + +- `NONE`: solo memoria (default). +- `CRITICAL_ONLY`: guarda en SQLite solo eventos ERROR/CRITICAL (para apps reguladas). +- `ALL`: guarda todos los eventos en SQLite (auditoría completa). + +--- + +## Diferencias entre SDK Android e iOS + +| Aspecto | Android | iOS | +|---|---|---| +| Entry point | `AppLoggerSDK` (object/singleton) | `AppLoggerIos.shared` (instance) | +| Config builder | `AppLoggerConfig.Builder()` | `AppLoggerConfig.Builder()` (mismo KMP) | +| Detección de plataforma | `PlatformDetector` (detecta TV) | `PlatformDetector` retorna `JVM` en iOS — sin auto-adapt TV | +| Detección de conexión | WiFi / Cellular / Ethernet (via `ConnectivityManager`) | `"unknown"` (requiere `Reachability` framework adicional) | +| Lifecycle observer | `ProcessLifecycleOwner` (automático) | `flush()` manual en `sceneDidEnterBackground` | +| Crash handler | `AndroidCrashHandler` (UnhandledExceptionHandler) | `IosCrashHandler` (NSSetUncaughtExceptionHandler) | +| `throwable` en logs | `Throwable` real de Kotlin | `null` — el error Swift se pasa en `extra` | + +--- + +## Recomendación: flush en background iOS + +A diferencia de Android (que usa `ProcessLifecycleOwner`), en iOS es recomendable hacer flush explícito al pasar a background: + +```swift +// En AppDelegate o SceneDelegate +func sceneDidEnterBackground(_ scene: UIScene) { + AppLoggerIos.shared.flush() +} +``` + +--- + +## Errores comunes + +| Error | Causa | Solución | +|---|---|---| +| `AppLogger.xcframework` no encontrado | SPM no descargó el paquete | Xcode → File → Packages → Resolve Package Versions | +| `AppLoggerSDK` no existe en iOS | Se usó el nombre Android en Swift | Usar `AppLoggerIos.shared` (no `AppLoggerSDK`) | +| Eventos no llegan a Supabase en debug | `debugMode: true` activo | Cambiar a `debugMode: false` para envío real al backend | +| `NSAppTransportSecurity` error | URL HTTP en lugar de HTTPS | El builder requiere HTTPS en producción — usar `https://` | +| No hay `connectionType` en deviceInfo | `IosDeviceInfoProvider` retorna `"unknown"` | Es un campo metadata; no bloquea el funcionamiento del SDK | + +--- + +## Referencia rápida de la API iOS + +```swift +// Inicialización (una vez, en App.init()) +AppLoggerIos.shared.initialize(config:, transport:) + +// Logging +AppLoggerIos.shared.debug(tag:, message:, extra:) +AppLoggerIos.shared.info(tag:, message:, extra:) +AppLoggerIos.shared.warn(tag:, message:, anomalyType:, extra:) +AppLoggerIos.shared.error(tag:, message:, throwable:, extra:) +AppLoggerIos.shared.critical(tag:, message:, throwable:, extra:) +AppLoggerIos.shared.metric(name:, value:, unit:, tags:) + +// Control +AppLoggerIos.shared.flush() +AppLoggerIos.shared.setAnonymousUserId(userId:) +AppLoggerIos.shared.clearAnonymousUserId() +``` diff --git a/docs/ES/desarrollo/README.md b/docs/ES/desarrollo/README.md index fc622b4..b577e29 100644 --- a/docs/ES/desarrollo/README.md +++ b/docs/ES/desarrollo/README.md @@ -18,5 +18,5 @@ - Integración del SDK en Android e iOS - Configuración por entorno (debug/release) -- Integración con gRPC y WebSockets +- Integración con transportes custom mediante `LogTransport` - App de monitoreo externa para beta testers diff --git a/docs/ES/desarrollo/api-compatibility.md b/docs/ES/desarrollo/api-compatibility.md index a1d5f60..ae09f8b 100644 --- a/docs/ES/desarrollo/api-compatibility.md +++ b/docs/ES/desarrollo/api-compatibility.md @@ -13,7 +13,7 @@ Documento de referencia rápida para compatibilidad mínima de plataforma y runt |---|---|---|---| | Android Mobile | API 23 (Android 6.0) | API 26+ | ART | | Android TV | API 23 (Android 6.0 TV) | API 28+ | ART | -| iOS | iOS 15 | iOS 16+ | Kotlin/Native + Darwin | +| iOS | iOS 14 | iOS 16+ | Kotlin/Native + Darwin | | JVM | JDK 11 | JDK 17 | HotSpot / OpenJDK | --- @@ -37,7 +37,8 @@ Documento de referencia rápida para compatibilidad mínima de plataforma y runt ### iOS - Distribución via XCFramework -- Integración recomendada con Swift Package Manager +- Build principal via Kotlin Multiplatform (target iOS) +- Si hay app host nativa, integración recomendada con Swift Package Manager ### JVM diff --git a/docs/ES/desarrollo/integration-guide.md b/docs/ES/desarrollo/integration-guide.md index 103e4c1..584c9d4 100644 --- a/docs/ES/desarrollo/integration-guide.md +++ b/docs/ES/desarrollo/integration-guide.md @@ -15,15 +15,14 @@ Documentación para desarrolladores que consumen `AppLogger` en sus aplicaciones 3. [Configuración por Entorno](#3-configuración-por-entorno) 4. [Inicialización en Application](#4-inicialización-en-application) 5. [Uso del Logger en la App](#5-uso-del-logger-en-la-app) -6. [Integración con gRPC](#6-integración-con-grpc) -7. [Integración con WebSockets](#7-integración-con-websockets) -8. [Configuración para Android TV](#8-configuración-para-android-tv) -9. [Integración en iOS (Swift)](#9-integración-en-ios-swift) -10. [Matriz de Compatibilidad de Plataformas](#10-matriz-de-compatibilidad-de-plataformas) -11. [App de Monitoreo Externo](#11-app-de-monitoreo-externo) -12. [Modo Debug vs Producción](#12-modo-debug-vs-producción) -13. [User ID Opcional — Con Consentimiento](#13-user-id-opcional--con-consentimiento) -14. [Preguntas Frecuentes](#14-preguntas-frecuentes) +6. [Transporte Custom — Implementar LogTransport](#6-transporte-custom--implementar-logtransport) +7. [Configuración para Android TV](#7-configuración-para-android-tv) +8. [Integración en iOS (KMP build + host)](#8-integración-en-ios-kmp-build--host) +9. [Matriz de Compatibilidad de Plataformas](#9-matriz-de-compatibilidad-de-plataformas) +10. [App de Monitoreo Externo](#10-app-de-monitoreo-externo) +11. [Modo Debug vs Producción](#11-modo-debug-vs-producción) +12. [User ID Opcional — Con Consentimiento](#12-user-id-opcional--con-consentimiento) +13. [Preguntas Frecuentes](#13-preguntas-frecuentes) --- @@ -117,6 +116,9 @@ Todas las variables se colocan en `local.properties` (no commiteable) y se mapea | `appLogger.lowStorageMode` | Boolean | `false` | Reduce buffer local y stack traces (para TV o dispositivos low-RAM) | | `appLogger.verboseTransport` | Boolean | `false` | Log detallado de cada batch enviado (solo debug) | | `appLogger.userId` | String | `null` | UUID anónimo del usuario (solo con consentimiento explícito) | +| `appLogger.bufferSizeStrategy` | String | `FIXED` | Estrategia de tamaño: `FIXED`, `ADAPTIVE_TO_RAM`, `ADAPTIVE_TO_LOG_RATE` | +| `appLogger.bufferOverflowPolicy` | String | `DISCARD_OLDEST` | Política ante overflow: `DISCARD_OLDEST`, `DISCARD_NEWEST`, `PRIORITY_AWARE` | +| `appLogger.offlinePersistenceMode` | String | `NONE` | Persistencia offline: `NONE`, `CRITICAL_ONLY`, `ALL` | ### 3.2 Ejemplo completo de `local.properties` @@ -162,6 +164,9 @@ android { buildConfigField("Int", "LOGGER_MAX_STACK", "${props["appLogger.maxStackTraceLines"] ?: 50}") buildConfigField("Boolean", "LOGGER_LOW_STORAGE", "${props["appLogger.lowStorageMode"] ?: false}") buildConfigField("Boolean", "LOGGER_VERBOSE", "${props["appLogger.verboseTransport"] ?: false}") + buildConfigField("String", "LOGGER_BUFFER_STRATEGY", "\"${props["appLogger.bufferSizeStrategy"] ?: "FIXED"}\"") + buildConfigField("String", "LOGGER_OVERFLOW_POLICY", "\"${props["appLogger.bufferOverflowPolicy"] ?: "DISCARD_OLDEST"}\"") + buildConfigField("String", "LOGGER_PERSISTENCE_MODE", "\"${props["appLogger.offlinePersistenceMode"] ?: "NONE"}\"") } } ``` @@ -215,6 +220,29 @@ class MyApp : Application() { .consoleOutput(BuildConfig.LOGGER_CONSOLE_OUTPUT) .batchSize(BuildConfig.LOGGER_BATCH_SIZE) .flushIntervalSeconds(BuildConfig.LOGGER_FLUSH_INTERVAL) + .maxStackTraceLines(BuildConfig.LOGGER_MAX_STACK) + // Opciones avanzadas (nuevas en 0.2.0): + .bufferSizeStrategy( + when (BuildConfig.LOGGER_BUFFER_STRATEGY) { + "ADAPTIVE_TO_RAM" -> BufferSizeStrategy.ADAPTIVE_TO_RAM + "ADAPTIVE_TO_LOG_RATE" -> BufferSizeStrategy.ADAPTIVE_TO_LOG_RATE + else -> BufferSizeStrategy.FIXED + } + ) + .bufferOverflowPolicy( + when (BuildConfig.LOGGER_OVERFLOW_POLICY) { + "DISCARD_NEWEST" -> BufferOverflowPolicy.DISCARD_NEWEST + "PRIORITY_AWARE" -> BufferOverflowPolicy.PRIORITY_AWARE + else -> BufferOverflowPolicy.DISCARD_OLDEST + } + ) + .offlinePersistenceMode( + when (BuildConfig.LOGGER_PERSISTENCE_MODE) { + "CRITICAL_ONLY" -> OfflinePersistenceMode.CRITICAL_ONLY + "ALL" -> OfflinePersistenceMode.ALL + else -> OfflinePersistenceMode.NONE + } + ) .build() ) } @@ -235,11 +263,11 @@ Al llamar a `initialize()`, el SDK: 1. Construye el `DeviceInfoProvider` con los metadatos del dispositivo. 2. Inicia el `Channel` en memoria para recibir eventos. -3. Lanza un coroutine en `Dispatchers.IO` que procesa el canal. +3. Lanza coroutines en `Dispatchers.Default` para procesar el canal y gestionar el flush periódico. 4. Instala el `UncaughtExceptionHandler` (en modo producción). 5. Registra un `LifecycleObserver` en `ProcessLifecycleOwner` para flush automático en background. -Todo esto ocurre en `Dispatchers.IO`, **nunca en el hilo principal**. +Todo esto ocurre **fuera del hilo principal**. --- @@ -304,96 +332,95 @@ AppLoggerSDK.debug("AUTH", "Token: $accessToken") // NUNCA --- -## 6. Integración con gRPC +## 6. Transporte Custom — Implementar LogTransport -No llames al logger manualmente en cada llamada gRPC. Usa el interceptor proporcionado por el SDK: +AppLogger separa la lógica de logging del transporte. `LogTransport` es el único punto de integración con cualquier backend. Para usar un backend diferente a Supabase (Firebase, Datadog, HTTP propio, etc.) implementa la interfaz: -### 6.1 Añadir el interceptor al canal gRPC +### 6.1 Implementar LogTransport ```kotlin -import io.grpc.ManagedChannelBuilder -import com.tuorg.applogger.interceptors.GrpcLoggingInterceptor - -val channel = ManagedChannelBuilder - .forAddress("api.tuapp.com", 443) - .useTransportSecurity() - .intercept( - GrpcLoggingInterceptor( - logger = AppLoggerSDK, - latencyThresholdMs = 500 // Solo loguea si la llamada tarda más de 500ms - ) - ) - .build() -``` - -### 6.2 El interceptor captura automáticamente - -- Llamadas que fallan con cualquier `status` distinto de `OK`. -- Llamadas que superan el umbral de latencia configurado. -- **No** loguea llamadas exitosas dentro del tiempo normal (sin overhead). -- **No** loguea el contenido de los mensajes protobuf (solo el nombre del método y el status). - -### 6.3 Configuración avanzada del interceptor +import com.applogger.core.LogTransport +import com.applogger.core.TransportResult +import com.applogger.core.model.LogEvent + +class MyBackendTransport( + private val endpoint: String, + private val apiKey: String +) : LogTransport { + + override suspend fun send(events: List): TransportResult { + return try { + // Serializar los eventos y enviarlos a tu backend. + // events contiene: level, tag, message, deviceInfo, sessionId, throwableInfo, extra... + val payload = events.map { serialize(it) } + myHttpClient.post(endpoint, payload, headers = mapOf("X-Api-Key" to apiKey)) + TransportResult.Success + } catch (e: Exception) { + TransportResult.Failure( + reason = e.message ?: "Transport error", + retryable = true, // true → el SDK reintenta con backoff exponencial + cause = e + ) + } + } -```kotlin -GrpcLoggingInterceptor( - logger = AppLoggerSDK, - latencyThresholdMs = 1000, // Umbral de latencia en ms (default: 500) - logSuccessfulCalls = false, // Log de llamadas exitosas — default: false - excludeMethods = setOf("HealthCheck/Check") // Métodos a excluir del log -) + override fun isAvailable(): Boolean { + // false → el SDK mantiene eventos en buffer y reintenta cuando isAvailable() vuelva a true. + return endpoint.isNotBlank() && apiKey.isNotBlank() + } +} ``` ---- - -## 7. Integración con WebSockets - -Envuelve tu `WebSocketListener` con el `LoggingWebSocketListener` del SDK: - -### 7.1 Con OkHttp +### 6.2 Inyectar al inicializar ```kotlin -import com.tuorg.applogger.interceptors.LoggingWebSocketListener - -val originalListener = object : WebSocketListener() { - override fun onMessage(webSocket: WebSocket, text: String) { - // Tu lógica de negocio - } - override fun onOpen(webSocket: WebSocket, response: Response) { - // Tu lógica de negocio - } -} - -// Envolver con el listener del SDK -val loggingListener = LoggingWebSocketListener( - delegate = originalListener, - logger = AppLoggerSDK, - tag = "WS_STREAM" +val transport = MyBackendTransport( + endpoint = BuildConfig.LOGGER_URL, + apiKey = BuildConfig.LOGGER_KEY ) -// Usar el listener envuelto al conectar -val request = Request.Builder().url("wss://tu-api.com/stream").build() -okHttpClient.newWebSocket(request, loggingListener) +AppLoggerSDK.initialize( + context = this, + config = AppLoggerConfig.Builder() + .endpoint(BuildConfig.LOGGER_URL) + .apiKey(BuildConfig.LOGGER_KEY) + .debugMode(BuildConfig.DEBUG) + .build(), + transport = transport +) ``` -### 7.2 Qué captura el LoggingWebSocketListener +### 6.3 Campos disponibles en `LogEvent` -| Evento | ¿Se loguea? | Nivel | +| Campo | Tipo | Descripción | |---|---|---| -| `onOpen` | No | — | -| `onMessage` | No (nunca el contenido) | — | -| `onClosing` con código 1000 (normal) | No | — | -| `onClosing` con código ≠ 1000 (anormal) | Sí | `WARN` | -| `onFailure` | Sí | `ERROR` | -| `onClosed` | No | — | +| `id` | `String` | UUID único del evento | +| `timestamp` | `Long` | Unix millis del momento del log | +| `level` | `LogLevel` | DEBUG, INFO, WARN, ERROR, CRITICAL, METRIC | +| `tag` | `String` | Etiqueta del módulo (máx 100 chars) | +| `message` | `String` | Mensaje del log (máx 10 000 chars) | +| `throwableInfo` | `ThrowableInfo?` | Tipo, mensaje y stack trace (si aplica) | +| `deviceInfo` | `DeviceInfo` | Marca, modelo, OS, API level, plataforma, conexión | +| `sessionId` | `String` | UUID efímero de la sesión | +| `userId` | `String?` | UUID anónimo opcional (solo con consentimiento) | +| `extra` | `Map?` | Metadatos adicionales | +| `sdkVersion` | `String` | Versión del SDK que generó el evento | + +### 6.4 Comportamiento de retry + +Si `send()` retorna `TransportResult.Failure(retryable = true)`, el SDK aplica **backoff exponencial con jitter**: + +- Máx. 5 reintentos por batch. +- Delay: aleatorio entre `base/2` y `min(base × 2ⁿ, 30 s)` (base = 1 s). +- Al agotar reintentos: eventos enviados al `DeadLetterQueue` en memoria. --- -## 8. Configuración para Android TV +## 7. Configuración para Android TV En Android TV, el SDK detecta automáticamente la plataforma y ajusta su comportamiento. Sin embargo, hay configuraciones adicionales recomendadas. -### 8.1 El SDK detecta Android TV automáticamente +### 7.1 El SDK detecta Android TV automáticamente No es necesario indicar explícitamente que la app es para TV. El `PlatformDetector` interno usa: @@ -402,7 +429,9 @@ No es necesario indicar explícitamente que la app es para TV. El `PlatformDetec packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) // → ANDROID_TV ``` -### 8.2 Ajustes recomendados para TV en el Builder +### 7.2 Valores por defecto en Android TV + +> El SDK detecta automáticamente TV y aplica estos valores via `resolveForLowResource()`. Solo sobreescribelos si necesitas ajustar los defaults. ```kotlin AppLoggerSDK.initialize( @@ -411,28 +440,32 @@ AppLoggerSDK.initialize( .endpoint(BuildConfig.LOGGER_URL) .apiKey(BuildConfig.LOGGER_KEY) .debugMode(BuildConfig.LOGGER_DEBUG_MODE) - // Ajustes específicos para TV - .batchSize(5) // Batch pequeño — menos RAM - .flushIntervalSeconds(60) // Flush cada minuto (no cada 30s) - .maxStackTraceLines(5) // Solo primeras 5 líneas del stack trace - .flushOnlyWhenIdle(true) // Solo hace flush cuando la app está en pausa + // Valores aplicados automáticamente para TV (override solo si es necesario): + .batchSize(5) // Auto en TV: 5 eventos/batch (mobile: 20) + .flushIntervalSeconds(60) // Auto en TV: flush cada 60 s (mobile: 30 s) + .maxStackTraceLines(5) // Auto en TV: 5 líneas max (mobile: 50) + .flushOnlyWhenIdle(true) // Auto en TV: flush solo en background .build() ) ``` -### 8.3 Comportamiento automático en TV +### 7.3 Comportamiento automático en TV -- **Buffer SQLite FIFO**: si no hay internet, los logs se almacenan localmente. Máximo 100 registros. Al llegar al 101, se descarta el más antiguo. -- **Stack traces truncados**: para ahorrar ancho de banda en conexiones de TV (frecuentemente limitadas a la velocidad del router doméstico). -- **Retry con WiFi**: el SDK solo reintenta el envío cuando detecta conexión WiFi o Ethernet, nunca agota el plan de datos de un router con límite. +- **Buffer en memoria FIFO (100 eventos)**: cuando el buffer está lleno, el evento más antiguo se descarta para dejar espacio al nuevo. +- **Stack traces truncados**: máximo 5 líneas por excepción (vs 50 en mobile) para reducir el payload. +- **Rate limiting reforzado**: máximo 30 eventos por tag por minuto (vs 120 en mobile). +- **Flush idle**: flush solo al pasar a background (`flushOnlyWhenIdle = true`), minimizando actividad en foreground. --- -## 9. Integración en iOS (Swift) +## 8. Integración en iOS (KMP build + host) + +En esta auditoría, iOS se trata como target de compilación KMP: Kotlin (`iosMain`) se empaqueta como framework iOS. +Si existe una app host nativa, ese framework se consume desde Swift (SPM o CocoaPods). -El SDK KMP se expone a iOS como XCFramework. La forma recomendada de consumo es Swift Package Manager. +### 8.1 Inicialización en Swift -### 9.1 Inicialización en Swift +El entry point iOS es la clase `AppLoggerIos` (definida en `iosMain`). Es distinta del singleton Android `AppLoggerSDK`. ```swift import AppLogger @@ -440,25 +473,107 @@ import AppLogger @main struct MyApp: App { init() { - AppLoggerSDK.shared.initialize( - config: AppLoggerConfigBuilder() - .endpoint(endpoint: "https://tu-proyecto.supabase.co") + let transport = SupabaseTransport( + endpoint: "https://tu-proyecto.supabase.co", + apiKey: "eyJ..." + ) + AppLoggerIos.shared.initialize( + config: AppLoggerConfig.Builder() + .endpoint(url: "https://tu-proyecto.supabase.co") .apiKey(key: "eyJ...") .debugMode(debug: false) - .build() + .build(), + transport: transport ) } } ``` -### 9.2 Uso desde Swift +> **Nota**: el `transport` es diferente a Android — `SupabaseTransport` es KMP (usa Ktor Darwin engine para iOS). + +### 8.2 Uso desde Swift ```swift -AppLoggerSDK.shared.info(tag: "PLAYER", message: "Playback started") -AppLoggerSDK.shared.error(tag: "PAYMENT", message: "Transaction failed") -AppLoggerSDK.shared.metric(name: "buffer_time", value: 420.0, unit: "ms") +AppLoggerIos.shared.info(tag: "PLAYER", message: "Playback started", extra: nil) +AppLoggerIos.shared.error(tag: "PAYMENT", message: "Transaction failed", throwable: nil, extra: nil) +AppLoggerIos.shared.metric(name: "buffer_time", value: 420.0, unit: "ms", tags: nil) +AppLoggerIos.shared.flush() ``` +### 8.3 Configuración avanzada para iOS + +En Swift, el `AppLoggerConfig.Builder` soporta las mismas opciones que Android: + +```swift +let config = AppLoggerConfig.Builder() + .endpoint(url: "https://tu-proyecto.supabase.co") + .apiKey(key: "eyJ...") + .debugMode(debug: false) + .batchSize(size: 20) // 1-100 + .flushIntervalSeconds(sec: 30) // 5-300 + .maxStackTraceLines(lines: 50) // 1-100 + .build() +``` + +> **Nota**: Las opciones avanzadas de buffer (`bufferSizeStrategy`, `bufferOverflowPolicy`, `offlinePersistenceMode`) están disponibles en Kotlin. En iOS, el builder actual no expone estas opciones directamente; para usarlas, se requiere una extensión nativa en `iosMain` que añada los métodos correspondientes al `Builder` KMP. Esto está planificado para la versión 0.2.0. + +--- + +## 9. Manejo de Desconexión y Métricas de Salud + +### 9.1 Comportamiento ante pérdida de conectividad + +El SDK está diseñado para operar en redes inestables típicas de dispositivos móviles: + +- **Buffer local en memoria**: Los eventos se encolan en un buffer FIFO mientras no haya conectividad. +- **Reintento inteligente**: Si el transporte reporta `isAvailable() = false`, el batch se reintenta con backoff exponencial + jitter (máx 5 reintentos). +- **Dead Letter Queue**: Si se agotan los reintentos, los eventos se mueven a una cola de cartas muertas (en memoria) para diagnóstico posterior. +- **Flush automático en reconexión**: Cuando `isAvailable()` vuelve a `true`, se envía automáticamente el buffer pendiente. + +### 9.2 Monitoreo de salud del SDK + +Puedes consultar el estado interno del logger en tiempo real: + +```kotlin +val health = AppLoggerHealth.snapshot() +if (!health.transportAvailable) { + // Mostrar banner "offline" en UI +} +if (health.bufferUtilizationPercentage > 80) { + // Alertar a SRE: buffer cerca de overflow +} +if (health.eventsDroppedDueToBufferOverflow > 0) { + // Reportar métrica de pérdida de logs +} +``` + +**Campos expuestos:** + +| Campo | Descripción | +|---|---| +| `isInitialized` | `true` tras `initialize()` exitoso | +| `transportAvailable` | `true` si el transporte tiene conectividad | +| `bufferedEvents` | Número de eventos en el buffer pendientes de envío | +| `deadLetterCount` | Eventos fallidos permanentemente (para análisis) | +| `consecutiveFailures` | Conteo de fallos consecutivos del transporte | +| `eventsDroppedDueToBufferOverflow` | Total de eventos descartados por overflow del buffer | +| `bufferUtilizationPercentage` | Porcentaje de ocupación del buffer (0-100) | +| `sdkVersion` | Versión del SDK | + +### 9.3 SLA de telemetría + +Con la configuración por defecto (`bufferSize = 1000`, `DISCARD_OLDEST`), el SDK garantiza: + +- **99.9% de eventos enviados** en condiciones de conectividad normal (outages < 5 min). +- **0% de bloqueo de UI**: todas las operaciones son no-bloqueantes. +- **Visibilidad total**: cualquier pérdida de evento es contabilizada en `eventsDroppedDueToBufferOverflow`. + +Para requisitos más estrictos (ej. banca), se recomienda: + +- Aumentar `bufferSize` a 5000-10000. +- Usar `bufferOverflowPolicy = PRIORITY_AWARE` para preservar errores críticos. +- Considerar `offlinePersistenceMode = CRITICAL_ONLY` para retención forzada de incidentes graves. + --- ## 10. Matriz de Compatibilidad de Plataformas @@ -467,22 +582,14 @@ AppLoggerSDK.shared.metric(name: "buffer_time", value: 420.0, unit: "ms") |---|---|---|---| | Android Mobile | API 23 (Android 6.0) | API 26+ | API 21 queda fuera por estabilidad operativa en dispositivos low-RAM | | Android TV | API 23 (Android 6.0 TV) | API 28+ | Mismo sourceSet que Android Mobile (`androidMain`) | -| iOS | iOS 15 | iOS 16+ | Distribución via XCFramework / SwiftPM | +| iOS | iOS 14 | iOS 16+ | Distribución via XCFramework / SwiftPM (podspec: `s.ios.deployment_target = '14.0'`) | | JVM | JDK 11 | JDK 17 | Soporte para herramientas internas y runners | --- -## 11. App de Monitoreo Externo - -> Documentación completa de la app de monitoreo en [monitoring-app.md](monitoring-app.md). - -El SDK solo escribe datos. La visualización se realiza desde una aplicación externa separada que consulta Supabase con credenciales de solo lectura. - ---- - -## 12. Modo Debug vs Producción +## 11. Modo Debug vs Producción -### 12.1 Diferencias de comportamiento +### 11.1 Diferencias de comportamiento | Comportamiento | Debug (`debugMode = true`) | Producción (`debugMode = false`) | |---|---|---| @@ -492,7 +599,7 @@ El SDK solo escribe datos. La visualización se realiza desde una aplicación ex | Nivel de verbosidad | Todos los niveles | Solo INFO, WARN, ERROR, CRITICAL, METRIC | | SQLite local (fallback) | No | Sí | -### 12.2 Control desde BuildConfig +### 11.2 Control desde BuildConfig ```kotlin // El valor de LOGGER_DEBUG_MODE viene de local.properties → build.gradle @@ -506,7 +613,7 @@ AppLoggerConfig.Builder() --- -## 13. User ID Opcional — Con Consentimiento +## 12. User ID Opcional — Con Consentimiento Por defecto, el `user_id` en todos los logs es `null`. Solo tiene sentido activarlo cuando el usuario ha dado consentimiento explícito para que sus logs sean correlacionables. @@ -535,19 +642,19 @@ fun onPrivacyPolicyRevoked() { --- -## 14. Preguntas Frecuentes +## 13. Preguntas Frecuentes **¿La librería puede hacer que mi app crashee?** -No. El SDK está diseñado para ser infalible: todas las excepciones internas son capturadas silenciosamente. Usa `Channel.trySend()` (never-blocking) para recibir eventos. Si el transporte falla, los logs se encoloan en SQLite o se descartan — la app nunca se ve afectada. +No. El SDK captura silenciosamente todas las excepciones internas. Usa `Channel.trySend()` (no bloqueante) para recibir eventos. Si el canal está al límite o el transporte falla, los eventos se descartan o reintentan — la app nunca se ve afectada. **¿Afecta al rendimiento de la UI?** No. Todas las operaciones de red y disco ocurren en `Dispatchers.IO`. El hilo principal solo ejecuta `Channel.trySend()`, que es una operación de microsegundos. **¿Qué pasa si no hay internet?** -Los logs se almacenan en SQLite local (buffer circular FIFO). Cuando vuelve la conectividad, el SDK los envía automáticamente. +Los logs se mantienen en el buffer en memoria (FIFO circular). Cuando el transporte vuelve a estar disponible (`isAvailable() = true`), el `BatchProcessor` los envía con retry automático (backoff exponencial). **¿Puedo usar AppLogger sin Supabase?** -Sí. La arquitectura basada en traits permite implementar un `LogTransport` personalizado para cualquier backend. Ver la arquitectura de `LogTransport` en [architecture.md](../paquete/architecture.md). +Sí. Implementa la interfaz `LogTransport` para cualquier backend (ver [sección 6](#6-transporte-custom--implementar-logtransport) y [architecture.md](../paquete/architecture.md)). **¿Los logs de DEBUG se envían a producción?** No. En modo producción (`debugMode = false`), los eventos de nivel `DEBUG` son filtrados automáticamente y no abandonan el dispositivo. diff --git a/docs/ES/paquete/CHANGELOG.md b/docs/ES/paquete/CHANGELOG.md index 9ed7212..f7b7bf0 100644 --- a/docs/ES/paquete/CHANGELOG.md +++ b/docs/ES/paquete/CHANGELOG.md @@ -10,7 +10,7 @@ El formato sigue [Keep a Changelog](https://keepachangelog.com/es/1.0.0/) y el p ### Planned - Módulo `logger-transport-firebase` — transporte a Firebase Realtime Database -- Soporte para `logger-transport-grpc` — envío directo vía gRPC a un servidor custom +- `SqliteOfflineBuffer` — persistencia FIFO en SQLite usando el esquema SQLDelight ya definido (`offline_logs`) - Soporte Wear OS en `PlatformDetector` - Dashboard web de visualización de logs en tiempo real @@ -20,7 +20,7 @@ El formato sigue [Keep a Changelog](https://keepachangelog.com/es/1.0.0/) y el p ### Added - **`AppLogger` trait** — contrato público unificado de logging para Kotlin (Android / JVM). -- **`LogTransport` trait** — abstracción de transporte intercambiable (REST, gRPC, stdio). +- **`LogTransport` trait** — abstracción de transporte intercambiable para backends HTTP o implementaciones custom. - **`LogBuffer` trait** — almacenamiento temporal de eventos con política de overflow configurable. - **`LogFilter` trait** — filtrado de eventos con soporte de cadena de responsabilidad. - **`LogFormatter` trait** — serialización de `LogEvent` (implementación JSON incluida). @@ -33,10 +33,9 @@ El formato sigue [Keep a Changelog](https://keepachangelog.com/es/1.0.0/) y el p - **`InMemoryLogger`** — implementación de test con assertions integradas. - **`FakeTransport`** — mock de transporte con control de éxito/fallo para tests. - **`SupabaseTransport`** — transporte a Supabase (PostgreSQL) con autenticación por `anon key`. -- **`GrpcLoggingInterceptor`** — interceptor de `ClientInterceptor` para captura automática de anomalías gRPC. -- **`LoggingWebSocketListener`** — wrapper de `WebSocketListener` para captura de fallos de WebSocket. - **`PlatformDetector`** — detección automática de `ANDROID_MOBILE`, `ANDROID_TV`, `WEAR_OS`, `JVM`. -- **`SqliteOfflineBuffer`** — buffer persistente FIFO en SQLite para funcionamiento offline en Android TV. +- **`InMemoryBuffer`** — buffer FIFO en memoria con descarte del evento más antiguo en overflow (capacidad: 1000 Mobile / 100 TV+WearOS). +- **SQLDelight offline schema** — esquema `offline_logs` y queries FIFO definidos en SQLDelight; base para `SqliteOfflineBuffer` futuro. - **`AppLoggerLifecycleObserver`** — flush automático cuando la app entra en background. - **`AppLoggerConfig.Builder`** — constructor de configuración tipado con valores por defecto adaptativos por plataforma. - **`AppLoggerSDK`** — objeto singleton de entrada pública (Android), con inicialización idempotente. diff --git a/docs/ES/paquete/README.md b/docs/ES/paquete/README.md index 6909b1f..3203650 100644 --- a/docs/ES/paquete/README.md +++ b/docs/ES/paquete/README.md @@ -16,7 +16,6 @@ Captura errores, crashes y métricas de performance de forma segura, sin impacta | Problema | Solución | |---|---| | El tester dice "se cerró" sin contexto | Crash capturado + metadatos técnicos + sesión | -| El logger satura la red en gRPC/WebSocket | Interceptores con umbrales de anomalía, sin log de llamadas normales | | El SDK mata la app en Android TV | Buffer adaptativo, batch pequeño, flush solo en idle | | Cambiar de Supabase a otro backend | Implementar `LogTransport` — la app no se entera | | GDPR / LGPD | Sin PII por defecto, session_id efímero, user_id opcional con consentimiento | @@ -71,10 +70,10 @@ AppLoggerSDK.metric("screen_load_time", 1234.0, "ms") | Documento | Descripción | |---|---| -| [Guía de Integración](../desarrollo/integration-guide.md) | Cómo integrar el SDK en una app, gRPC, WebSocket, Android TV | -| [Arquitectura del Paquete](architecture.md) | Traits, módulos, pipeline, extensibilidad | +| [Guía de Integración](../desarrollo/integration-guide.md) | Cómo integrar el SDK en una app Android/iOS, transportes custom y Android TV | +| [Arquitectura del Paquete](architecture.md) | Traits, módulos, pipeline y extensibilidad real del SDK | | [Testing](testing.md) | Tests unitarios, FakeTransport, casos de resiliencia | -| [Publicación](publishing.md) | JitPack, GitHub Packages, Maven Central, CI/CD | +| [Publicación](publishing.md) | JitPack, GitHub Packages tras release etiquetada, Maven Central y CI/CD | | [CONTRIBUTING](CONTRIBUTING.md) | Guía para contribuir al proyecto | | [CHANGELOG](CHANGELOG.md) | Historial de versiones | @@ -89,9 +88,9 @@ AppLoggerSDK └── AppLoggerImpl ├── DeviceInfoProvider → AndroidDeviceInfoProvider (o custom) ├── LogFilter → RateLimitFilter (o chain de filtros) - ├── LogBuffer → InMemoryBuffer / SqliteOfflineBuffer + ├── LogBuffer → InMemoryBuffer (1000 Mobile / 100 TV+WearOS) ├── LogFormatter → JsonLogFormatter (o protobuf, etc.) - ├── LogTransport → SupabaseTransport (o Firebase, Datadog, gRPC) + ├── LogTransport → SupabaseTransport (o implementación custom) └── CrashHandler → AndroidCrashHandler ``` @@ -105,7 +104,7 @@ AppLogger no captura datos personales identificables (PII): - ✅ `session_id`: UUID efímero por sesión. No persistente. No correlacionable. - ❌ Nombre, email, número de teléfono — nunca. - ❌ Ubicación GPS — nunca. -- ❌ Contenido de mensajes gRPC/WebSocket — nunca. +- ❌ Contenido de payloads de negocio o credenciales — nunca. - ⚙️ `user_id` anónimo — solo si el desarrollador lo activa con consentimiento explícito. --- diff --git a/docs/ES/paquete/architecture.md b/docs/ES/paquete/architecture.md index 615c797..8f82d24 100644 --- a/docs/ES/paquete/architecture.md +++ b/docs/ES/paquete/architecture.md @@ -85,40 +85,33 @@ appLoggers/ │ │ └── SessionManager.kt │ │ │ ├── androidMain/kotlin/ -│ │ └── com/applogger/ -│ │ ├── AppLoggerSDK.kt (entry point Android) -│ │ ├── AppLoggerConfig.kt (Builder) -│ │ ├── AndroidDeviceInfoProvider.kt (actual) -│ │ ├── AndroidCrashHandler.kt (actual) +│ │ └── com/applogger/core/ +│ │ ├── AppLoggerSDK.kt (entry point Android) │ │ ├── AppLoggerLifecycleObserver.kt -│ │ ├── SqliteOfflineBuffer.kt (SQLDelight) +│ │ ├── AndroidDeviceInfoProvider.kt (actual) +│ │ ├── AndroidCrashHandler.kt (actual) │ │ ├── PlatformDetector.kt -│ │ └── interceptors/ -│ │ ├── GrpcLoggingInterceptor.kt -│ │ └── LoggingWebSocketListener.kt +│ │ └── Platform.android.kt │ │ │ ├── iosMain/kotlin/ -│ │ └── com/applogger/ -│ │ ├── AppLoggerSDK.kt (entry point iOS — expuesto a Swift) -│ │ ├── IosDeviceInfoProvider.kt (actual) -│ │ ├── IosCrashHandler.kt (actual — NSSetUncaughtExceptionHandler) -│ │ ├── SqliteOfflineBuffer.kt (SQLDelight Kotlin/Native) -│ │ └── URLSessionLoggingDelegate.kt +│ │ └── com/applogger/core/ +│ │ ├── AppLoggerIos.kt (entry point iOS — expuesto a Swift) +│ │ ├── IosDeviceInfoProvider.kt (actual) +│ │ ├── IosCrashHandler.kt (actual) +│ │ └── Platform.ios.kt │ │ │ └── jvmMain/kotlin/ -│ └── com/applogger/ -│ ├── JvmDeviceInfoProvider.kt (actual) -│ └── JvmCrashHandler.kt (actual) +│ └── com/applogger/core/ +│ └── Platform.jvm.kt │ ├── logger-transport-supabase/ ← Módulo de transporte (KMP, intercambiable) │ └── src/commonMain/kotlin/ │ └── com/applogger/transport/supabase/ │ └── SupabaseTransport.kt (Ktor client KMP) │ -└── logger-transport-grpc/ ← Transporte gRPC (solo androidMain) - └── src/androidMain/kotlin/ - └── com/applogger/transport/grpc/ - └── GrpcTransport.kt +└── logger-test/ ← Utilidades de testing para consumidores del SDK + └── src/commonMain/kotlin/ + └── com/applogger/test/ ``` ### 2.1 Configuración `build.gradle.kts` del módulo core @@ -197,7 +190,7 @@ AppLoggerImpl (implements AppLogger) │ ├── DeviceInfoProvider ──▶ AndroidDeviceInfoProvider ├── LogFilter ──▶ RateLimitFilter (+ chain) - ├── LogBuffer ──▶ InMemoryBuffer / SqliteOfflineBuffer + ├── LogBuffer —▶ InMemoryBuffer (default; SQLDelight schema disponible en commonMain para extensión) ├── LogFormatter ──▶ JsonLogFormatter ├── LogTransport ──▶ SupabaseTransport / NoOpTransport / Custom └── CrashHandler ──▶ AndroidCrashHandler / NoOpCrashHandler @@ -692,7 +685,7 @@ data class AppLoggerConfig internal constructor( ## 9. Extensibilidad — Implementar Transportes Propios -Para usar un backend diferente a Supabase (Firebase, Datadog, servidor gRPC propio), implementar `LogTransport`: +Para usar un backend diferente a Supabase (Firebase, Datadog, servidor HTTP propio, etc.), implementar `LogTransport`: ```kotlin // Ejemplo: transporte a Firebase Realtime Database @@ -905,5 +898,5 @@ object AppLoggerSDK { | Consumidor | Artefacto | Cómo incluirlo | |---|---|---| | Android (Gradle) | `.aar` via maven-publish | `implementation("com.github.devzucca.appLoggers:logger-core:v0.1.0-alpha.1")` | -| iOS (Swift) | XCFramework | Swift Package Manager (`Package.swift`) | +| iOS (KMP build / host opcional) | XCFramework | Build con Gradle KMP; si hay host Swift, incluir con Swift Package Manager (`Package.swift`) | | JVM (Gradle) | `.jar` via maven-publish | `implementation("com.github.devzucca.appLoggers:logger-core:v0.1.0-alpha.1")` | diff --git a/docs/ES/paquete/publishing.md b/docs/ES/paquete/publishing.md index e16c347..e28bf9e 100644 --- a/docs/ES/paquete/publishing.md +++ b/docs/ES/paquete/publishing.md @@ -2,7 +2,9 @@ **Versión:** 0.1.0-alpha.1 **Fecha:** 2026-03-17 -**Plataformas de publicación:** JitPack · GitHub Packages · Maven Central · Swift Package Manager (iOS consumo) +**Plataformas objetivo:** JitPack · GitHub Packages · Maven Central · Swift Package Manager (iOS consumo) + +> Estado actual: JitPack es el canal más directo para consumo. GitHub Packages requiere una release etiquetada exitosa para que los artefactos aparezcan en la sección Packages del repositorio. Maven Central sigue siendo un objetivo de publicación, no un canal ya operativo. --- From cf1014ab720358e052dbda91464c0ddca7aea250 Mon Sep 17 00:00:00 2001 From: David Zuccarini Date: Wed, 18 Mar 2026 22:54:12 -0400 Subject: [PATCH 4/5] fix(core): resolve buffer config build regressions --- .../kotlin/com/applogger/core/AppLoggerSDK.kt | 80 +++++++++++++------ .../com/applogger/core/AppLoggerConfig.kt | 47 ----------- .../com/applogger/core/AppLoggerHealth.kt | 6 +- .../applogger/core/BufferOverflowPolicy.kt | 2 +- .../com/applogger/core/BufferSizeStrategy.kt | 2 +- .../applogger/core/internal/InMemoryBuffer.kt | 16 ++-- 6 files changed, 73 insertions(+), 80 deletions(-) diff --git a/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt b/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt index 7aeef4b..6378d66 100644 --- a/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt +++ b/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt @@ -6,6 +6,12 @@ import com.applogger.core.internal.* import kotlin.concurrent.Volatile import java.util.concurrent.atomic.AtomicBoolean +private const val LOW_RESOURCE_BUFFER_CAPACITY = 100 +private const val DEFAULT_BUFFER_CAPACITY = 1000 +private const val ADAPTIVE_RAM_PERCENTAGE = 0.001 +private const val MIN_ADAPTIVE_BUFFER_CAPACITY = 50 +private const val MAX_ADAPTIVE_BUFFER_CAPACITY = 5000 + /** * Android entry point for the AppLogger SDK. * @@ -26,6 +32,7 @@ import java.util.concurrent.atomic.AtomicBoolean * @see AppLoggerConfig for configuration options. * @see com.applogger.transport.supabase.SupabaseTransport for the Supabase transport. */ +@Suppress("TooManyFunctions") object AppLoggerSDK : AppLogger { @Volatile @@ -61,23 +68,7 @@ object AppLoggerSDK : AppLogger { listOf(RateLimitFilter(if (platform.isLowResource) 30 else 120)) ) - // Determinar capacidad del buffer según estrategia - val bufferCapacity = when (resolvedConfig.bufferSizeStrategy) { - AppLoggerConfig.BufferSizeStrategy.FIXED -> if (platform.isLowResource) 100 else 1000 - AppLoggerConfig.BufferSizeStrategy.ADAPTIVE_TO_RAM -> { - // Ejemplo: 0.1% de RAM, con mínimo 50 y máximo 5000 - val activityManager = appContext.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager - val memoryInfo = android.app.ActivityManager.MemoryInfo() - activityManager.getMemoryInfo(memoryInfo) - val totalRam = memoryInfo.totalMem - val target = (totalRam * 0.001).toInt() // 0.1% - target.coerceIn(50, 5000) - } - AppLoggerConfig.BufferSizeStrategy.ADAPTIVE_TO_LOG_RATE -> { - // Por ahora usamos el valor por defecto; en futura versión se ajustaría dinámicamente - if (platform.isLowResource) 100 else 1000 - } - } + val bufferCapacity = computeBufferCapacity(appContext, platform, resolvedConfig) val buffer = InMemoryBuffer( maxCapacity = bufferCapacity, @@ -105,12 +96,7 @@ object AppLoggerSDK : AppLogger { instance = impl implRef = impl - // Wire up health check API - AppLoggerHealth.processor = processor - AppLoggerHealth.transport = resolvedTransport - AppLoggerHealth.buffer = buffer - AppLoggerHealth.bufferCapacity = bufferCapacity - AppLoggerHealth.initialized = true + updateHealthReferences(processor, resolvedTransport, buffer, bufferCapacity) if (!resolvedConfig.isDebugMode) { val crashHandler = AndroidCrashHandler(impl) @@ -149,6 +135,54 @@ object AppLoggerSDK : AppLogger { fun clearAnonymousUserId() { implRef?.clearUserId() } + + private fun computeBufferCapacity( + appContext: Context, + platform: Platform, + config: AppLoggerConfig + ): Int { + return when (config.bufferSizeStrategy) { + BufferSizeStrategy.FIXED -> defaultBufferCapacity(platform) + BufferSizeStrategy.ADAPTIVE_TO_RAM -> adaptiveRamBufferCapacity(appContext) + BufferSizeStrategy.ADAPTIVE_TO_LOG_RATE -> defaultBufferCapacity(platform) + } + } + + private fun defaultBufferCapacity(platform: Platform): Int { + return if (platform.isLowResource) { + LOW_RESOURCE_BUFFER_CAPACITY + } else { + DEFAULT_BUFFER_CAPACITY + } + } + + private fun adaptiveRamBufferCapacity(appContext: Context): Int { + val activityManager = appContext.getSystemService( + Context.ACTIVITY_SERVICE + ) as android.app.ActivityManager + val memoryInfo = android.app.ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + + val totalRam = memoryInfo.totalMem + val target = (totalRam * ADAPTIVE_RAM_PERCENTAGE).toInt() + return target.coerceIn( + MIN_ADAPTIVE_BUFFER_CAPACITY, + MAX_ADAPTIVE_BUFFER_CAPACITY + ) + } + + private fun updateHealthReferences( + processor: BatchProcessor, + transport: LogTransport, + buffer: InMemoryBuffer, + bufferCapacity: Int + ) { + AppLoggerHealth.processor = processor + AppLoggerHealth.transport = transport + AppLoggerHealth.buffer = buffer + AppLoggerHealth.bufferCapacity = bufferCapacity + AppLoggerHealth.initialized = true + } } /** No-op transport used when no backend is configured. */ diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt index 5311443..63aa5ff 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt @@ -107,53 +107,6 @@ data class AppLoggerConfig( } } -/** - * Estrategia para determinar el tamaño del buffer. - */ -enum class BufferSizeStrategy { - /** - * Tamaño fijo definido por [InMemoryBuffer.maxCapacity]. - * Default: 1000 (mobile), 100 (TV/WearOS/iOS). - */ - FIXED, - - /** - * Calcula tamaño basado en % de RAM disponible del dispositivo. - * Ejemplo: 0.1% de RAM total, con mínimo 50 y máximo 5000 eventos. - */ - ADAPTIVE_TO_RAM, - - /** - * Ajusta tamaño dinámicamente según tasa de eventos observada. - * Incrementa buffer si la tasa sostenida supera umbral para prevenir overflow. - */ - ADAPTIVE_TO_LOG_RATE -} - -/** - * Política a aplicar cuando el buffer está lleno. - */ -enum class BufferOverflowPolicy { - /** - * Descarta el evento más antiguo para hacer espacio (FIFO). - * Default. Bueno para mantener contexto reciente. - */ - DISCARD_OLDEST, - - /** - * Descarta el evento más reciente, preservando historial. - * Útil para apps que priorizan no perder datos antiguos. - */ - DISCARD_NEWEST, - - /** - * Descarta eventos según prioridad: DEBUG → INFO → WARN → ERROR → CRITICAL. - * Solo descarta de nivel inferior si todos los niveles superiores están vacíos. - * Garantiza que eventos críticos nunca se pierden a menos que buffer esté lleno solo de críticos. - */ - PRIORITY_AWARE -} - /** * Modo de persistencia offline para eventos. */ diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerHealth.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerHealth.kt index a1b8fb9..e0c0fbf 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerHealth.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerHealth.kt @@ -55,6 +55,10 @@ object AppLoggerHealth { consecutiveFailures = 0, sdkVersion = AppLoggerVersion.NAME, eventsDroppedDueToBufferOverflow = buffer?.getOverflowCount() ?: 0, - bufferUtilizationPercentage = if (bufferCapacity > 0) (buffer?.size()?.toFloat() ?: 0f) / bufferCapacity * 100f else 0f + bufferUtilizationPercentage = if (bufferCapacity > 0) { + (buffer?.size()?.toFloat() ?: 0f) / bufferCapacity * 100f + } else { + 0f + } ) } diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferOverflowPolicy.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferOverflowPolicy.kt index b5ee3b7..60dae0c 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferOverflowPolicy.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferOverflowPolicy.kt @@ -10,4 +10,4 @@ enum class BufferOverflowPolicy { DISCARD_NEWEST, /** Discard events based on priority (lowest priority first). */ PRIORITY_AWARE -} \ No newline at end of file +} diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferSizeStrategy.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferSizeStrategy.kt index 6f0c068..8ce5708 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferSizeStrategy.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/BufferSizeStrategy.kt @@ -10,4 +10,4 @@ enum class BufferSizeStrategy { ADAPTIVE_TO_RAM, /** Adjust buffer size based on sustained log rate (not yet implemented). */ ADAPTIVE_TO_LOG_RATE -} \ No newline at end of file +} diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/InMemoryBuffer.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/InMemoryBuffer.kt index 64b0be8..a005248 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/InMemoryBuffer.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/InMemoryBuffer.kt @@ -1,8 +1,8 @@ package com.applogger.core.internal +import com.applogger.core.BufferOverflowPolicy import com.applogger.core.LogBuffer import com.applogger.core.model.LogEvent -import com.applogger.core.model.LogLevel /** * Buffer en memoria con capacidad limitada y política de overflow configurable. @@ -12,22 +12,24 @@ import com.applogger.core.model.LogLevel */ internal class InMemoryBuffer( private val maxCapacity: Int = 1000, - private val overflowPolicy: com.applogger.core.BufferOverflowPolicy = com.applogger.core.BufferOverflowPolicy.DISCARD_OLDEST + private val overflowPolicy: BufferOverflowPolicy = BufferOverflowPolicy.DISCARD_OLDEST ) : LogBuffer { private val buffer = ArrayDeque() - private var overflowCount = 0 + private var overflowCount = 0L override fun push(event: LogEvent): Boolean { platformSynchronized(buffer) { if (buffer.size >= maxCapacity) { overflowCount++ when (overflowPolicy) { - com.applogger.core.BufferOverflowPolicy.DISCARD_OLDEST -> buffer.removeFirst() - com.applogger.core.BufferOverflowPolicy.DISCARD_NEWEST -> return@platformSynchronized false - com.applogger.core.BufferOverflowPolicy.PRIORITY_AWARE -> { + BufferOverflowPolicy.DISCARD_OLDEST -> buffer.removeFirst() + BufferOverflowPolicy.DISCARD_NEWEST -> return@platformSynchronized false + BufferOverflowPolicy.PRIORITY_AWARE -> { // Descarta el evento de menor prioridad que no sea CRITICAL - val indexToRemove = buffer.indexOfFirst { it.level != com.applogger.core.model.LogLevel.CRITICAL } + val indexToRemove = buffer.indexOfFirst { + it.level != com.applogger.core.model.LogLevel.CRITICAL + } if (indexToRemove != -1) { buffer.removeAt(indexToRemove) } else { From fc53b16eb98e1b3885c0cfc61382f28f348d54cd Mon Sep 17 00:00:00 2001 From: David Zuccarini Date: Wed, 18 Mar 2026 23:27:22 -0400 Subject: [PATCH 5/5] docs: align iOS integration to KMP-only and add agent skill entry --- CHANGELOG.md | 2 +- README.md | 31 +- docs/ES/agents/README.md | 5 +- docs/ES/agents/SKILL.md | 53 +++ docs/ES/agents/android-integration.md | 7 +- docs/ES/agents/ios-integration.md | 490 +++++++----------------- docs/ES/desarrollo/api-compatibility.md | 2 +- docs/ES/desarrollo/integration-guide.md | 98 +++-- docs/ES/paquete/architecture.md | 8 +- docs/ES/paquete/publishing.md | 5 +- 10 files changed, 261 insertions(+), 440 deletions(-) create mode 100644 docs/ES/agents/SKILL.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a00a68..799ae5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and - **`AppLoggerLifecycleObserver`** — automatic flush when app backgrounds. - **`AppLoggerConfig.Builder`** — typed configuration builder with adaptive defaults per platform. - **`AppLoggerSDK`** — public entry singleton (Android), idempotent initialization. -- **`AppLoggerIos`** — public entry singleton (iOS), exported to Swift via KMP framework. +- **`AppLoggerIos`** — public iOS entry singleton for Kotlin Multiplatform (`iosMain`). - **`logger-test` module** — testing utilities: `NoOpTestLogger`, `InMemoryLogger`, `FakeTransport`. - **Privacy by design**: no PII captured, ephemeral `session_id`, optional `user_id` with consent. - **Crash handler chaining**: SDK chains the previous handler, never replaces it. diff --git a/README.md b/README.md index 02f6d93..97c22cc 100644 --- a/README.md +++ b/README.md @@ -204,20 +204,19 @@ dependencies { } ``` -### iOS (KMP build + Swift host opcional) +### iOS (KMP puro) -`logger-core` genera un XCFramework (`AppLogger.framework`) distribuible vía GitHub Releases o repositorio SPM. +`logger-core` genera el artefacto iOS desde `iosMain` usando Kotlin Multiplatform. -En auditoría interna, el foco es el build KMP de iOS (Kotlin -> framework). SPM/CocoaPods aplican al consumo desde app host nativa. - -```swift -import AppLogger +En este proyecto, la ruta oficial para iOS es KMP end-to-end: inicialización y uso en Kotlin (`commonMain`/`iosMain`). +```kotlin +// iosMain AppLoggerIos.shared.initialize( - config: AppLoggerConfig.Builder() - .endpoint(endpoint: "https://tu-proyecto.supabase.co") - .apiKey(key: "tu_anon_key") - .debugMode(debug: false) + config = AppLoggerConfig.Builder() + .endpoint("https://tu-proyecto.supabase.co") + .apiKey("tu_anon_key") + .debugMode(false) .build() ) ``` @@ -272,15 +271,11 @@ AppLoggerSDK.metric("screen_load_time", 1234.0, "ms", tags = mapOf("screen" to " AppLoggerSDK.debug("TAG", "Solo visible en debug") ``` -### iOS (Host Swift, si aplica) - -```swift -import AppLogger +### iOS (Kotlin `iosMain`) -AppLoggerIos.shared.initialize(config: ...) - -AppLoggerIos.shared.error(tag: "PLAYER", message: "Playback failed") -AppLoggerIos.shared.metric(name: "buffer_time", value: 420.0, unit: "ms") +```kotlin +AppLoggerIos.shared.error("PLAYER", "Playback failed", throwable = null) +AppLoggerIos.shared.metric("buffer_time", 420.0, "ms") ``` --- diff --git a/docs/ES/agents/README.md b/docs/ES/agents/README.md index 47807f3..9ac0f70 100644 --- a/docs/ES/agents/README.md +++ b/docs/ES/agents/README.md @@ -2,10 +2,13 @@ Este directorio contiene **skills** (guías de dominio) para agentes de IA y desarrolladores que necesitan integrar AppLogger en proyectos externos. +Incluye un archivo estándar `SKILL.md` compatible con la especificación de Agent Skills. + | Skill | Plataforma | Archivo | |---|---|---| +| Skill principal compatible Agent Skills | Android + iOS (KMP) | [SKILL.md](SKILL.md) | | Integrar AppLogger en app Android (Kotlin) | Android Mobile + TV | [android-integration.md](android-integration.md) | -| Integrar AppLogger en iOS (KMP build + consumo host) | iOS 14+ | [ios-integration.md](ios-integration.md) | +| Integrar AppLogger en iOS (KMP puro) | iOS 14+ | [ios-integration.md](ios-integration.md) | --- diff --git a/docs/ES/agents/SKILL.md b/docs/ES/agents/SKILL.md new file mode 100644 index 0000000..2838587 --- /dev/null +++ b/docs/ES/agents/SKILL.md @@ -0,0 +1,53 @@ +--- +name: applogger-kmp-integration +description: Use this skill when integrating AppLogger in Android or iOS using Kotlin Multiplatform, validating configuration, initialization, health checks, and troubleshooting. +--- + +# AppLogger KMP Integration Skill + +## When to use this skill + +Use this skill when the user needs to: + +1. Integrate AppLogger in Android or iOS from Kotlin code. +2. Configure AppLogger safely with environment variables. +3. Validate initialization and health status. +4. Troubleshoot telemetry delivery or buffer behavior. + +Do not use this skill for: + +1. Integración host nativa externa (deprecada en este proyecto). +2. Non-KMP setup guides. +3. Backend schema design outside AppLogger docs. + +## Mandatory constraints + +1. Prefer Kotlin Multiplatform flow (`commonMain` + `iosMain`). +2. Do not recommend host package-manager flows for new implementations. +3. Never suggest hardcoding secrets. +4. Verify API usage against current SDK code before giving examples. + +## Execution workflow + +1. Identify target platform: Android, iOS, or both. +2. Validate prerequisites and dependencies. +3. Provide initialization code with `AppLoggerConfig.Builder()`. +4. Provide usage examples for `debug/info/warn/error/critical/metric`. +5. Provide `AppLoggerHealth.snapshot()` checks. +6. Provide troubleshooting checklist. + +## Canonical references in this repository + +1. Android integration: `docs/ES/agents/android-integration.md` +2. iOS KMP integration: `docs/ES/agents/ios-integration.md` +3. Full integration guide: `docs/ES/desarrollo/integration-guide.md` +4. Compatibility matrix: `docs/ES/desarrollo/api-compatibility.md` +5. Architecture details: `docs/ES/paquete/architecture.md` + +## Output quality standard + +1. Explain steps in simple language. +2. Keep examples executable and minimal. +3. Separate required steps from optional optimizations. +4. Include common errors and exact fixes. +5. State assumptions explicitly when information is missing. diff --git a/docs/ES/agents/android-integration.md b/docs/ES/agents/android-integration.md index 0d09fdc..ffcc191 100644 --- a/docs/ES/agents/android-integration.md +++ b/docs/ES/agents/android-integration.md @@ -330,7 +330,8 @@ AppLoggerSDK.setAnonymousUserId(userId) AppLoggerSDK.clearAnonymousUserId() // Health check -AppLoggerHealth.initialized // Boolean -AppLoggerHealth.transportAvailable // Boolean -AppLoggerHealth.bufferedEventCount // Int +val health = AppLoggerHealth.snapshot() +health.isInitialized // Boolean +health.transportAvailable // Boolean +health.bufferedEvents // Int ``` diff --git a/docs/ES/agents/ios-integration.md b/docs/ES/agents/ios-integration.md index 86343e0..56f2bdd 100644 --- a/docs/ES/agents/ios-integration.md +++ b/docs/ES/agents/ios-integration.md @@ -1,409 +1,185 @@ -# Skill — Integrar AppLogger en iOS (KMP build + consumo host) +# Skill — Integrar AppLogger en iOS con Kotlin Multiplatform **SDK versión:** 0.1.0-alpha.1 **Plataforma:** iOS 14+ -**Lenguaje del SDK:** Kotlin Multiplatform -**Lenguaje host (si aplica):** Swift 5.9+ -**Distribución host:** Swift Package Manager · CocoaPods +**Lenguaje del SDK y host:** Kotlin Multiplatform +**Política de proyecto:** no usar integración host nativa externa para nuevas implementaciones -> El SDK compila el módulo `iosMain` de Kotlin Multiplatform como **XCFramework** (`AppLogger.xcframework`). -> El entry point iOS es `AppLoggerIos.shared` — **distinto** del singleton Android `AppLoggerSDK`. - -> Alcance de este documento: build iOS del SDK KMP y consumo desde app host cuando exista. -> Si tu app es KMP end-to-end, no necesitas lógica del SDK en Swift; solo enlazar el framework iOS generado por Gradle. +> Esta guía describe un flujo KMP puro: configuración, inicialización y uso del logger desde código Kotlin. +> No requiere gestores de paquetes host externos ni código de host nativo externo. --- -## Prerrequisitos - -| Requisito | Valor mínimo | -|---|---| -| Xcode | 15.0+ | -| iOS Deployment Target | iOS 14.0 | -| Swift | 5.9+ | -| Acceso a red (Info.plist) | `NSAppTransportSecurity` → HTTPS (obligatorio en producción) | - ---- - -## Flujo recomendado para esta auditoría (KMP) - -1. Implementar y mantener el SDK en Kotlin (`commonMain` + `iosMain`). -2. Generar framework iOS desde Gradle (KMP). -3. Validar el artefacto iOS generado (framework/XCFramework). -4. Solo si hay app host nativa: consumir por SPM o CocoaPods desde Swift. - ---- - -## Opción A — Swift Package Manager (recomendado) - -> Esta opción aplica cuando el consumidor del SDK es una app iOS nativa Swift. +## Objetivo de esta guía -### A.1 Añadir el paquete al proyecto +Al terminar, vas a poder: -En Xcode → File → Add Package Dependencies: - -``` -https://github.com/devzucca/appLoggers -``` - -O en `Package.swift` de tu proyecto: - -```swift -dependencies: [ - .package( - url: "https://github.com/devzucca/appLoggers", - from: "0.1.0-alpha.1" - ) -], -targets: [ - .target( - name: "MyApp", - dependencies: [ - .product(name: "AppLogger", package: "appLoggers") - ] - ) -] -``` - -### A.2 Targets disponibles en el SPM - -El `Package.swift` del SDK declara: - -```swift -// sdk/Package.swift -.iOS(.v14), .macOS(.v12), .tvOS(.v14), .watchOS(.v7) -``` +1. Configurar un módulo KMP con soporte iOS. +2. Generar el framework iOS desde Gradle. +3. Inicializar AppLogger desde `iosMain` en Kotlin. +4. Verificar estado de salud del logger y problemas comunes. --- -## Opción B — CocoaPods - -> Esta opción aplica cuando el consumidor del SDK es una app iOS nativa Swift. - -En `Podfile`: - -```ruby -pod 'AppLogger', :git => 'https://github.com/devzucca/appLoggers.git', :tag => 'v0.1.0-alpha.1' -``` +## Prerrequisitos -El `AppLogger.podspec` en el SDK especifica: -- `s.ios.deployment_target = '14.0'` -- `s.tvos.deployment_target = '14.0'` +| Requisito | Valor mínimo | +|---|---| +| JDK | 17 | +| Kotlin | 2.1+ | +| Gradle Wrapper | 8.10.2 | +| iOS target | iOS 14.0 | +| Módulos necesarios | `logger-core` (+ `logger-transport-supabase` si usarás Supabase) | --- -## Si tu proyecto es KMP (sin app Swift nativa) - -En un flujo Kotlin Multiplatform, el módulo shared compila `iosMain` a framework nativo iOS. +## Flujo recomendado (KMP puro) -- Se desarrolla el SDK en Kotlin. -- Se compila para iOS con Gradle (framework/XCFramework). -- No necesitas mantener `Package.swift` en tu app si no distribuyes ni consumes por SPM. -- Swift solo es necesario en la capa host iOS cuando existe una app iOS nativa consumidora. +1. Implementar la integración en Kotlin (`commonMain` + `iosMain`). +2. Configurar dependencias KMP para iOS. +3. Inicializar AppLogger en código `iosMain`. +4. Construir artefacto iOS desde Gradle. +5. Verificar logs, métricas y salud del SDK. --- -## Paso 1 — Inicializar en App entry point +## Paso 1 — Dependencias KMP -```swift -import AppLogger - -@main -struct MyApp: App { - - init() { - setupLogger() - } +En el módulo KMP que consume AppLogger: - var body: some Scene { - WindowGroup { - ContentView() +```kotlin +kotlin { + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + implementation("com.github.devzucca.appLoggers:logger-core:v0.1.0-alpha.1") + } } - } - - private func setupLogger() { - let transport = SupabaseTransport( - endpoint: "https://TU-PROYECTO.supabase.co", - apiKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - ) - AppLoggerIos.shared.initialize( - config: AppLoggerConfig.Builder() - .endpoint(url: "https://TU-PROYECTO.supabase.co") - .apiKey(key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") - .debugMode(debug: false) // true = consola, false = backend - .batchSize(size: 20) - .flushIntervalSeconds(sec: 30) - .maxStackTraceLines(lines: 50) - // Opciones avanzadas (disponibles en Kotlin; en iOS requieren extensión nativa): - // .bufferSizeStrategy(strategy: .FIXED) // o .ADAPTIVE_TO_RAM, .ADAPTIVE_TO_LOG_RATE - // .bufferOverflowPolicy(policy: .DISCARD_OLDEST) // o .DISCARD_NEWEST, .PRIORITY_AWARE - // .offlinePersistenceMode(mode: .NONE) // o .CRITICAL_ONLY, .ALL - .build(), - transport: transport - ) + val iosMain by getting { + dependencies { + // Necesario solo si vas a enviar a Supabase desde iOS + implementation("com.github.devzucca.appLoggers:logger-transport-supabase:v0.1.0-alpha.1") + } + } } } ``` -> **Credenciales en producción**: nunca hardcodear keys en el código fuente. -> Usar Variables de entorno de Xcode (`$(LOGGER_URL)`) o un archivo `Config.xcconfig` excluido del repositorio. - --- -## Paso 2 — Gestionar credenciales - -### Config.xcconfig (excluir en .gitignore) - -```text -// AppLoggerConfig.xcconfig — NO commitear -LOGGER_URL = https://TU-PROYECTO.supabase.co -LOGGER_KEY = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -LOGGER_DEBUG = NO -``` - -### Info.plist — leer en código +## Paso 2 — Configuración segura -```swift -private func loggerURL() -> String { - Bundle.main.object(forInfoDictionaryKey: "LOGGER_URL") as? String ?? "" -} - -private func loggerKey() -> String { - Bundle.main.object(forInfoDictionaryKey: "LOGGER_KEY") as? String ?? "" -} -``` +No hardcodees secretos en repositorio. ---- +Ejemplo de variables locales en `local.properties` (archivo ignorado por git): -## Paso 3 — Usar el logger - -```swift -import AppLogger - -// DEBUG — visible solo en modo debug (debugMode: true) -AppLoggerIos.shared.debug(tag: "TAG", message: "Valor: \(value)", extra: nil) - -// INFO — flujos normales productivos -AppLoggerIos.shared.info(tag: "PLAYER", message: "Playback started", extra: nil) -AppLoggerIos.shared.info( - tag: "AUTH", - message: "Login successful", - extra: ["method": "apple_sign_in"] -) - -// WARN — comportamiento inesperado pero recuperable -AppLoggerIos.shared.warn( - tag: "NETWORK", - message: "Slow response", - anomalyType: "HIGH_LATENCY", - extra: ["latency_ms": "1400"] -) - -// ERROR — fallo que el usuario percibe -AppLoggerIos.shared.error( - tag: "PAYMENT", - message: "Transaction failed", - throwable: nil, // Kotlin Throwable — pasar nil en iOS - extra: ["order_id": "ORD-123"] -) - -// CRITICAL — fallo que bloquea la app -AppLoggerIos.shared.critical( - tag: "AUTH", - message: "Token refresh failed", - throwable: nil, - extra: nil -) - -// METRIC — datos de performance -AppLoggerIos.shared.metric( - name: "screen_load_time", - value: 1234.0, - unit: "ms", - tags: ["screen": "HomeView"] -) - -// Flush manual (ocurre automáticamente cada flushIntervalSeconds) -AppLoggerIos.shared.flush() +```properties +appLogger.url=https://TU-PROYECTO.supabase.co +appLogger.anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +appLogger.debug=false ``` --- -## Paso 4 — Captura de errores Swift/Objective-C +## Paso 3 — Inicializar desde `iosMain` (Kotlin) -La clase `IosCrashHandler` instala un handler para excepciones no capturadas (solo en `debugMode = false`). Para errores Swift, envuelve los puntos críticos: +Crea un archivo en `src/iosMain/kotlin/...`, por ejemplo `LoggerBootstrap.ios.kt`: -```swift -do { - try loadUserData() -} catch { - AppLoggerIos.shared.error( - tag: "DATA", - message: "Load user data failed: \(error.localizedDescription)", - throwable: nil, - extra: ["error_type": String(describing: type(of: error))] - ) -} -``` - -> **Nota**: el parámetro `throwable` acepta un `KotlinThrowable` del bridge KMP. En iOS puro, pasar `nil` y usar `extra` para incluir el detalle del error Swift. - ---- - -## Paso 5 — User ID anónimo con consentimiento - -```swift -// Solo tras aceptación explícita -func onPrivacyPolicyAccepted() { - let anonymousId = getOrCreateAnonymousId() - AppLoggerIos.shared.setAnonymousUserId(userId: anonymousId) -} +```kotlin +package com.example.app + +import com.applogger.core.AppLoggerConfig +import com.applogger.core.AppLoggerIos +import com.applogger.core.AppLoggerHealth +import com.applogger.transport.supabase.SupabaseTransport + +object LoggerBootstrapIos { + + fun initialize(url: String, anonKey: String, debugMode: Boolean = false) { + val config = AppLoggerConfig.Builder() + .endpoint(url) + .apiKey(anonKey) + .debugMode(debugMode) + .batchSize(20) + .flushIntervalSeconds(30) + .maxStackTraceLines(50) + .build() + + val transport = SupabaseTransport( + endpoint = url, + apiKey = anonKey + ) -// Para revocar -func onPrivacyPolicyRevoked() { - AppLoggerIos.shared.clearAnonymousUserId() -} + AppLoggerIos.shared.initialize( + config = config, + transport = transport + ) + } -private func getOrCreateAnonymousId() -> String { - let key = "app_logger_anon_user_id" - if let stored = UserDefaults.standard.string(forKey: key) { - return stored + fun healthSummary(): String { + val h = AppLoggerHealth.snapshot() + return "initialized=${h.isInitialized}, transport=${h.transportAvailable}, buffered=${h.bufferedEvents}" } - let newId = UUID().uuidString - UserDefaults.standard.set(newId, forKey: key) - return newId } ``` --- -## Paso 6 — Buenas prácticas de contenido - -```swift -// ✅ Contexto técnico, sin PII -AppLoggerIos.shared.error( - tag: "STREAM", - message: "Segment fetch failed", - throwable: nil, - extra: ["segment_index": "42", "cdn": "us-east-1"] -) - -// ✅ Tags constantes por módulo -enum LogTags { - static let player = "PLAYER" - static let network = "NETWORK" - static let auth = "AUTH" - static let payment = "PAYMENT" -} - -// ❌ NUNCA loguear datos del usuario -AppLoggerIos.shared.error(tag: "AUTH", message: "Error for \(user.email)", ...) - -// ❌ NUNCA loguear tokens -AppLoggerIos.shared.debug(tag: "AUTH", message: "Token: \(accessToken)", ...) -``` - ---- +## Paso 4 — Uso del logger desde Kotlin -## Paso 7 — Health check en UI (opcional) +```kotlin +import com.applogger.core.AppLoggerIos -```swift -import AppLogger +AppLoggerIos.shared.info("PLAYER", "Playback started") +AppLoggerIos.shared.warn("NETWORK", "Slow response", "HIGH_LATENCY") +AppLoggerIos.shared.error("PAYMENT", "Transaction failed", throwable = null) +AppLoggerIos.shared.metric("screen_load_time", 1234.0, "ms") -struct DebugDashboard: View { - var body: some View { - VStack { - Text("Logger: \(AppLoggerHealth.shared.initialized ? "✅" : "❌")") - Text("Transport: \(AppLoggerHealth.shared.transportAvailable ? "✅" : "offline")") - Text("Buffered: \(AppLoggerHealth.shared.bufferedEventCount) eventos") - Text("Overflow: \(AppLoggerHealth.shared.eventsDroppedDueToBufferOverflow) descartados") - Text("Buffer %: \(String(format: "%.1f", AppLoggerHealth.shared.bufferUtilizationPercentage))%") - } - } -} +// Flush manual opcional +AppLoggerIos.shared.flush() ``` -> Mostrar `AppLoggerHealth` solo en builds de debug/staging, no en producción. - --- -## Paso 8 — Manejo de desconexión +## Paso 5 — Health check (diagnóstico) -El SDK maneja automáticamente la pérdida de conectividad: - -- **Buffer local**: eventos encolados en memoria mientras no hay red. -- **Reintento**: backoff exponencial con jitter (máx 5 reintentos). -- **Dead Letter Queue**: eventos que no se enviaron tras reintentos agotados. -- **Flush en reconexión**: cuando el transporte recupera conectividad, se envían automáticamente los eventos pendientes. - -No se requiere intervención manual. Sin embargo, puedes forzar un flush en momentos clave (ej. al pasar a background): +```kotlin +import com.applogger.core.AppLoggerHealth -```swift -// En AppDelegate o SceneDelegate -func sceneDidEnterBackground(_ scene: UIScene) { - AppLoggerIos.shared.flush() -} +val health = AppLoggerHealth.snapshot() +println("isInitialized=${health.isInitialized}") +println("transportAvailable=${health.transportAvailable}") +println("bufferedEvents=${health.bufferedEvents}") +println("droppedOverflow=${health.eventsDroppedDueToBufferOverflow}") +println("bufferUtilization=${health.bufferUtilizationPercentage}") ``` --- -## Paso 9 — Opciones avanzadas de buffer (Kotlin) +## Paso 6 — Build iOS desde Gradle -Las siguientes opciones están disponibles en el `AppLoggerConfig.Builder` de Kotlin. En iOS, para usarlas, se debe añadir una extensión nativa en `iosMain` que exponga estos métodos al bridge Swift. Esto está planificado para la versión 0.2.0. +Desde `sdk/`: -```kotlin -// Solo disponible en Kotlin (por ahora) -AppLoggerConfig.Builder() - .bufferSizeStrategy(strategy: BufferSizeStrategy) // FIXED, ADAPTIVE_TO_RAM, ADAPTIVE_TO_LOG_RATE - .bufferOverflowPolicy(policy: BufferOverflowPolicy) // DISCARD_OLDEST, DISCARD_NEWEST, PRIORITY_AWARE - .offlinePersistenceMode(mode: OfflinePersistenceMode) // NONE, CRITICAL_ONLY, ALL +```bash +./gradlew :logger-core:assembleXCFramework ``` -**Estrategias de buffer:** - -- `FIXED`: tamaño fijo (default: 1000 móvil, 100 TV/iOS). -- `ADAPTIVE_TO_RAM`: calcula tamaño como % de RAM disponible (0.1%, min 50, max 5000). -- `ADAPTIVE_TO_LOG_RATE`: ajusta dinámicamente según tasa de eventos (futuro). - -**Políticas de overflow:** - -- `DISCARD_OLDEST`: descarta el evento más antiguo (default). -- `DISCARD_NEWEST`: descarta el evento más reciente. -- `PRIORITY_AWARE`: descarta por prioridad (DEBUG → INFO → WARN → ERROR → CRITICAL), preservando críticos. - -**Persistencia offline:** - -- `NONE`: solo memoria (default). -- `CRITICAL_ONLY`: guarda en SQLite solo eventos ERROR/CRITICAL (para apps reguladas). -- `ALL`: guarda todos los eventos en SQLite (auditoría completa). +Si tu app KMP consume el logger en su propio módulo shared, compila ese módulo para iOS con las tareas de framework correspondientes. --- -## Diferencias entre SDK Android e iOS +## Buenas prácticas (muy importantes) -| Aspecto | Android | iOS | -|---|---|---| -| Entry point | `AppLoggerSDK` (object/singleton) | `AppLoggerIos.shared` (instance) | -| Config builder | `AppLoggerConfig.Builder()` | `AppLoggerConfig.Builder()` (mismo KMP) | -| Detección de plataforma | `PlatformDetector` (detecta TV) | `PlatformDetector` retorna `JVM` en iOS — sin auto-adapt TV | -| Detección de conexión | WiFi / Cellular / Ethernet (via `ConnectivityManager`) | `"unknown"` (requiere `Reachability` framework adicional) | -| Lifecycle observer | `ProcessLifecycleOwner` (automático) | `flush()` manual en `sceneDidEnterBackground` | -| Crash handler | `AndroidCrashHandler` (UnhandledExceptionHandler) | `IosCrashHandler` (NSSetUncaughtExceptionHandler) | -| `throwable` en logs | `Throwable` real de Kotlin | `null` — el error Swift se pasa en `extra` | - ---- - -## Recomendación: flush en background iOS - -A diferencia de Android (que usa `ProcessLifecycleOwner`), en iOS es recomendable hacer flush explícito al pasar a background: - -```swift -// En AppDelegate o SceneDelegate -func sceneDidEnterBackground(_ scene: UIScene) { - AppLoggerIos.shared.flush() -} -``` +1. Usa tags constantes (`AUTH`, `PAYMENT`, `NETWORK`) para buscar rápido en backend. +2. Nunca loguees PII (email, nombre, teléfono, dirección). +3. Nunca loguees tokens, claves o secretos. +4. Mantén `debugMode=false` fuera de desarrollo local. +5. Revisa `AppLoggerHealth.snapshot()` durante QA para detectar pérdida de eventos. --- @@ -411,30 +187,26 @@ func sceneDidEnterBackground(_ scene: UIScene) { | Error | Causa | Solución | |---|---|---| -| `AppLogger.xcframework` no encontrado | SPM no descargó el paquete | Xcode → File → Packages → Resolve Package Versions | -| `AppLoggerSDK` no existe en iOS | Se usó el nombre Android en Swift | Usar `AppLoggerIos.shared` (no `AppLoggerSDK`) | -| Eventos no llegan a Supabase en debug | `debugMode: true` activo | Cambiar a `debugMode: false` para envío real al backend | -| `NSAppTransportSecurity` error | URL HTTP en lugar de HTTPS | El builder requiere HTTPS en producción — usar `https://` | -| No hay `connectionType` en deviceInfo | `IosDeviceInfoProvider` retorna `"unknown"` | Es un campo metadata; no bloquea el funcionamiento del SDK | +| Eventos no llegan al backend | URL o key inválida | Validar `appLogger.url` y `appLogger.anonKey` | +| `production endpoint must use HTTPS` | Endpoint no seguro | Usar `https://` en producción | +| `transportAvailable=false` | Sin conectividad o backend no disponible | Ver red, DNS y estado del backend | +| `bufferedEvents` sube y no baja | Flush no ocurre o transporte falla | Revisar `flushIntervalSeconds`, conectividad y errores del transporte | --- -## Referencia rápida de la API iOS +## Diferencias Android vs iOS (estado actual) -```swift -// Inicialización (una vez, en App.init()) -AppLoggerIos.shared.initialize(config:, transport:) +| Aspecto | Android | iOS | +|---|---|---| +| Entry point | `AppLoggerSDK` | `AppLoggerIos.shared` | +| Ajustes low-resource automáticos por `PlatformDetector` | Sí | No | +| `connectionType` en metadata | Detectado por `ConnectivityManager` | `"unknown"` por defecto | +| `throwable` desde host | `Throwable` Kotlin/Java | `null` cuando no hay excepción Kotlin | + +--- -// Logging -AppLoggerIos.shared.debug(tag:, message:, extra:) -AppLoggerIos.shared.info(tag:, message:, extra:) -AppLoggerIos.shared.warn(tag:, message:, anomalyType:, extra:) -AppLoggerIos.shared.error(tag:, message:, throwable:, extra:) -AppLoggerIos.shared.critical(tag:, message:, throwable:, extra:) -AppLoggerIos.shared.metric(name:, value:, unit:, tags:) +## Estado de roadmap -// Control -AppLoggerIos.shared.flush() -AppLoggerIos.shared.setAnonymousUserId(userId:) -AppLoggerIos.shared.clearAnonymousUserId() -``` +- La integración recomendada en este proyecto es KMP puro. +- El flujo host nativo externo se considera legado/deprecado para nuevas implementaciones. +- Toda nueva funcionalidad debe priorizarse en código Kotlin (`commonMain`/`iosMain`). diff --git a/docs/ES/desarrollo/api-compatibility.md b/docs/ES/desarrollo/api-compatibility.md index ae09f8b..553ec37 100644 --- a/docs/ES/desarrollo/api-compatibility.md +++ b/docs/ES/desarrollo/api-compatibility.md @@ -38,7 +38,7 @@ Documento de referencia rápida para compatibilidad mínima de plataforma y runt - Distribución via XCFramework - Build principal via Kotlin Multiplatform (target iOS) -- Si hay app host nativa, integración recomendada con Swift Package Manager +- Integración recomendada: KMP puro (`commonMain` + `iosMain`) sin capa host externa ### JVM diff --git a/docs/ES/desarrollo/integration-guide.md b/docs/ES/desarrollo/integration-guide.md index 584c9d4..7322e26 100644 --- a/docs/ES/desarrollo/integration-guide.md +++ b/docs/ES/desarrollo/integration-guide.md @@ -17,7 +17,7 @@ Documentación para desarrolladores que consumen `AppLogger` en sus aplicaciones 5. [Uso del Logger en la App](#5-uso-del-logger-en-la-app) 6. [Transporte Custom — Implementar LogTransport](#6-transporte-custom--implementar-logtransport) 7. [Configuración para Android TV](#7-configuración-para-android-tv) -8. [Integración en iOS (KMP build + host)](#8-integración-en-ios-kmp-build--host) +8. [Integración en iOS (KMP puro)](#8-integración-en-ios-kmp-puro) 9. [Matriz de Compatibilidad de Plataformas](#9-matriz-de-compatibilidad-de-plataformas) 10. [App de Monitoreo Externo](#10-app-de-monitoreo-externo) 11. [Modo Debug vs Producción](#11-modo-debug-vs-producción) @@ -458,65 +458,63 @@ AppLoggerSDK.initialize( --- -## 8. Integración en iOS (KMP build + host) +## 8. Integración en iOS (KMP puro) -En esta auditoría, iOS se trata como target de compilación KMP: Kotlin (`iosMain`) se empaqueta como framework iOS. -Si existe una app host nativa, ese framework se consume desde Swift (SPM o CocoaPods). +En esta auditoría, iOS se trata como target KMP puro: configuración, inicialización y uso desde Kotlin (`commonMain` + `iosMain`). -### 8.1 Inicialización en Swift +### 8.1 Inicialización en `iosMain` (Kotlin) -El entry point iOS es la clase `AppLoggerIos` (definida en `iosMain`). Es distinta del singleton Android `AppLoggerSDK`. +El entry point iOS es `AppLoggerIos.shared` (definido en `iosMain`). Es distinto del singleton Android `AppLoggerSDK`. -```swift -import AppLogger +```kotlin +// iosMain +import com.applogger.core.AppLoggerConfig +import com.applogger.core.AppLoggerIos +import com.applogger.transport.supabase.SupabaseTransport + +fun initializeLoggerIos(url: String, apiKey: String, debugMode: Boolean = false) { + val config = AppLoggerConfig.Builder() + .endpoint(url) + .apiKey(apiKey) + .debugMode(debugMode) + .batchSize(20) + .flushIntervalSeconds(30) + .maxStackTraceLines(50) + .build() -@main -struct MyApp: App { - init() { - let transport = SupabaseTransport( - endpoint: "https://tu-proyecto.supabase.co", - apiKey: "eyJ..." - ) - AppLoggerIos.shared.initialize( - config: AppLoggerConfig.Builder() - .endpoint(url: "https://tu-proyecto.supabase.co") - .apiKey(key: "eyJ...") - .debugMode(debug: false) - .build(), - transport: transport - ) - } + val transport = SupabaseTransport(endpoint = url, apiKey = apiKey) + + AppLoggerIos.shared.initialize( + config = config, + transport = transport + ) } ``` -> **Nota**: el `transport` es diferente a Android — `SupabaseTransport` es KMP (usa Ktor Darwin engine para iOS). - -### 8.2 Uso desde Swift +### 8.2 Uso en iOS desde Kotlin -```swift -AppLoggerIos.shared.info(tag: "PLAYER", message: "Playback started", extra: nil) -AppLoggerIos.shared.error(tag: "PAYMENT", message: "Transaction failed", throwable: nil, extra: nil) -AppLoggerIos.shared.metric(name: "buffer_time", value: 420.0, unit: "ms", tags: nil) +```kotlin +AppLoggerIos.shared.info("PLAYER", "Playback started") +AppLoggerIos.shared.error("PAYMENT", "Transaction failed", throwable = null) +AppLoggerIos.shared.metric("buffer_time", 420.0, "ms") AppLoggerIos.shared.flush() ``` -### 8.3 Configuración avanzada para iOS +### 8.3 Configuración avanzada -En Swift, el `AppLoggerConfig.Builder` soporta las mismas opciones que Android: +En Kotlin, `AppLoggerConfig.Builder` soporta opciones de buffer y persistencia offline para ambos targets. -```swift -let config = AppLoggerConfig.Builder() - .endpoint(url: "https://tu-proyecto.supabase.co") - .apiKey(key: "eyJ...") - .debugMode(debug: false) - .batchSize(size: 20) // 1-100 - .flushIntervalSeconds(sec: 30) // 5-300 - .maxStackTraceLines(lines: 50) // 1-100 +```kotlin +val config = AppLoggerConfig.Builder() + .endpoint("https://tu-proyecto.supabase.co") + .apiKey("eyJ...") + .debugMode(false) + .bufferSizeStrategy(BufferSizeStrategy.FIXED) + .bufferOverflowPolicy(BufferOverflowPolicy.DISCARD_OLDEST) + .offlinePersistenceMode(OfflinePersistenceMode.NONE) .build() ``` -> **Nota**: Las opciones avanzadas de buffer (`bufferSizeStrategy`, `bufferOverflowPolicy`, `offlinePersistenceMode`) están disponibles en Kotlin. En iOS, el builder actual no expone estas opciones directamente; para usarlas, se requiere una extensión nativa en `iosMain` que añada los métodos correspondientes al `Builder` KMP. Esto está planificado para la versión 0.2.0. - --- ## 9. Manejo de Desconexión y Métricas de Salud @@ -560,17 +558,17 @@ if (health.eventsDroppedDueToBufferOverflow > 0) { | `bufferUtilizationPercentage` | Porcentaje de ocupación del buffer (0-100) | | `sdkVersion` | Versión del SDK | -### 9.3 SLA de telemetría +### 9.3 Objetivos operativos de telemetría -Con la configuración por defecto (`bufferSize = 1000`, `DISCARD_OLDEST`), el SDK garantiza: +Con la configuración por defecto (`bufferSize = 1000`, `DISCARD_OLDEST`), el SDK está diseñado para: -- **99.9% de eventos enviados** en condiciones de conectividad normal (outages < 5 min). -- **0% de bloqueo de UI**: todas las operaciones son no-bloqueantes. -- **Visibilidad total**: cualquier pérdida de evento es contabilizada en `eventsDroppedDueToBufferOverflow`. +- Minimizar pérdida de eventos en conectividad intermitente. +- Evitar bloqueo del hilo de UI mediante envío asíncrono. +- Exponer métricas de salud para detectar degradación (`eventsDroppedDueToBufferOverflow`, `bufferUtilizationPercentage`). Para requisitos más estrictos (ej. banca), se recomienda: -- Aumentar `bufferSize` a 5000-10000. +- Aumentar `bufferSize` a 5000-10000 según pruebas de carga propias. - Usar `bufferOverflowPolicy = PRIORITY_AWARE` para preservar errores críticos. - Considerar `offlinePersistenceMode = CRITICAL_ONLY` para retención forzada de incidentes graves. @@ -582,7 +580,7 @@ Para requisitos más estrictos (ej. banca), se recomienda: |---|---|---|---| | Android Mobile | API 23 (Android 6.0) | API 26+ | API 21 queda fuera por estabilidad operativa en dispositivos low-RAM | | Android TV | API 23 (Android 6.0 TV) | API 28+ | Mismo sourceSet que Android Mobile (`androidMain`) | -| iOS | iOS 14 | iOS 16+ | Distribución via XCFramework / SwiftPM (podspec: `s.ios.deployment_target = '14.0'`) | +| iOS | iOS 14 | iOS 16+ | Distribución via XCFramework generado por Gradle KMP | | JVM | JDK 11 | JDK 17 | Soporte para herramientas internas y runners | --- diff --git a/docs/ES/paquete/architecture.md b/docs/ES/paquete/architecture.md index 8f82d24..7554045 100644 --- a/docs/ES/paquete/architecture.md +++ b/docs/ES/paquete/architecture.md @@ -95,7 +95,7 @@ appLoggers/ │ │ │ ├── iosMain/kotlin/ │ │ └── com/applogger/core/ -│ │ ├── AppLoggerIos.kt (entry point iOS — expuesto a Swift) +│ │ ├── AppLoggerIos.kt (entry point iOS en Kotlin) │ │ ├── IosDeviceInfoProvider.kt (actual) │ │ ├── IosCrashHandler.kt (actual) │ │ └── Platform.ios.kt @@ -867,12 +867,12 @@ data class LogEvent( En Kotlin/Native (iOS), el modelo de concurrencia es diferente al de JVM. A partir de Kotlin 1.9+ el new memory model elimina la mayoría de las restricciones, pero hay consideraciones: - **`Channel`**: Compatible con el new memory model. Funciona igual que en JVM. -- **`CoroutineScope`**: Usar `MainScope()` en entrypoints iOS para integración con el main thread de Swift. +- **`CoroutineScope`**: Usar `MainScope()` en entrypoints iOS para integración con el main thread de la app. - **Frozen objects**: Con el new memory model ya no es necesario `freeze()` manual. - **Dispatchers**: En iOS, `Dispatchers.Default` usa threads de GCD (Grand Central Dispatch). ```kotlin -// iosMain — entry point expuesto a Swift +// iosMain — entry point Kotlin para iOS object AppLoggerSDK { private val scope = MainScope() // iOS main thread integration @@ -898,5 +898,5 @@ object AppLoggerSDK { | Consumidor | Artefacto | Cómo incluirlo | |---|---|---| | Android (Gradle) | `.aar` via maven-publish | `implementation("com.github.devzucca.appLoggers:logger-core:v0.1.0-alpha.1")` | -| iOS (KMP build / host opcional) | XCFramework | Build con Gradle KMP; si hay host Swift, incluir con Swift Package Manager (`Package.swift`) | +| iOS (KMP puro) | XCFramework | Build con Gradle KMP desde `iosMain` | | JVM (Gradle) | `.jar` via maven-publish | `implementation("com.github.devzucca.appLoggers:logger-core:v0.1.0-alpha.1")` | diff --git a/docs/ES/paquete/publishing.md b/docs/ES/paquete/publishing.md index e28bf9e..bd9b60d 100644 --- a/docs/ES/paquete/publishing.md +++ b/docs/ES/paquete/publishing.md @@ -2,7 +2,7 @@ **Versión:** 0.1.0-alpha.1 **Fecha:** 2026-03-17 -**Plataformas objetivo:** JitPack · GitHub Packages · Maven Central · Swift Package Manager (iOS consumo) +**Plataformas objetivo:** JitPack · GitHub Packages · Maven Central > Estado actual: JitPack es el canal más directo para consumo. GitHub Packages requiere una release etiquetada exitosa para que los artefactos aparezcan en la sección Packages del repositorio. Maven Central sigue siendo un objetivo de publicación, no un canal ya operativo. @@ -183,7 +183,7 @@ Comandos útiles de publicación: ./gradlew publishKotlinMultiplatformPublicationToGitHubPackagesRepository ``` -Para iOS, el consumo habitual se hace vía XCFramework generado en CI y distribuido por release de GitHub o por repositorio SwiftPM. +Para iOS, el consumo recomendado en este proyecto es KMP puro, compilando el target iOS desde Gradle. --- @@ -434,4 +434,3 @@ Antes de crear un tag de release, verificar: - [ ] La build de JitPack fue activada y exitosa - [ ] El GitHub Release tiene release notes generadas - [ ] El XCFramework iOS fue generado y adjuntado al release -- [ ] El manifiesto de SwiftPM apunta a la versión liberada