diff --git a/.gitignore b/.gitignore index 2f80f28..fbfb549 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ data/resources/src/commonMain/composeResources/files/records.json keystore.properties *.jks *.keystore + +# Secrets — see docs/sentry-glitchtip.md (the .example template stays versioned) +.sentryclirc diff --git a/.sentryclirc.example b/.sentryclirc.example new file mode 100644 index 0000000..93e32b9 --- /dev/null +++ b/.sentryclirc.example @@ -0,0 +1,17 @@ +; Template for the local `.sentryclirc` used by sentry-cli to upload debug symbols +; to GlitchTip. Copy it to `.sentryclirc` at the repository root and fill in the token: +; +; cp .sentryclirc.example .sentryclirc +; chmod 600 .sentryclirc +; +; `.sentryclirc` is git-ignored and must never be committed — it holds a credential. +; See docs/sentry-glitchtip.md for the full procedure. + +[auth] +; Created in GlitchTip under your user menu > Profile > Auth Tokens. +token= + +[defaults] +url=https://eu.glitchtip.com/ +org=kerlenn-sten-kidna +project=chom-bev-e-brezhoneg diff --git a/README.md b/README.md index 6a007f8..bb79e1f 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,18 @@ See [Resource's README.md](data/resources/README.md) to handle the resource file audio files, and subtitles). +## Documentation + +- [Crash reporting — Sentry SDK on GlitchTip](./docs/sentry-glitchtip.md): DSN configuration, + `.sentryclirc` setup, and the iOS Archive procedure for uploading dSYM files. + +## Crash reporting + +Crashes are reported with the Sentry Kotlin Multiplatform SDK to a [GlitchTip](https://glitchtip.com) +instance. The DSN is read from `local.properties` and never committed, so a fresh clone builds and +runs with crash reporting disabled until you configure it. See +[docs/sentry-glitchtip.md](./docs/sentry-glitchtip.md).R + ## License This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. diff --git a/androidApp/README.md b/androidApp/README.md index fe30b45..3e6dcaf 100644 --- a/androidApp/README.md +++ b/androidApp/README.md @@ -1,10 +1,5 @@ # Android app -## Crash logging - -GlitchTip is configured in the project. It uses the open source solution Senstry to log crashes -generated by the app and send them to GlitchTip on a European server. - ## StrictMode (debug builds) The app enables Android [StrictMode](https://developer.android.com/reference/android/os/StrictMode) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index c751ee4..21ccc0e 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -31,9 +31,6 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.compose.uiToolingPreview) debugImplementation(libs.compose.uiTooling) - - // Crash logging - implementation(libs.glitchtips) } android { @@ -55,10 +52,6 @@ android { .toInt() versionCode = 1 versionName = "1.0.0-alpha02" - - // Crash-reporting DSN, kept out of the public repo: read from local.properties - // (glitchtip.dsn=...). Empty when absent, which disables the Sentry SDK. - manifestPlaceholders["glitchtipDsn"] = localProperty("glitchtip.dsn") ?: "" } packaging { resources { @@ -76,12 +69,3 @@ android { targetCompatibility = JavaVersion.VERSION_11 } } - -/** Reads a key from the root local.properties, or null when the file or key is absent. */ -fun localProperty(key: String): String? { - val file = rootProject.file("local.properties") - if (!file.exists()) return null - return Properties() - .apply { file.inputStream().use { load(it) } } - .getProperty(key) -} diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 5d94f36..4ef8eef 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -20,11 +20,6 @@ - - - - - \ No newline at end of file diff --git a/androidApp/src/main/kotlin/bzh/ajaury/chombev/ChomBevApp.kt b/androidApp/src/main/kotlin/bzh/ajaury/chombev/ChomBevApp.kt index 8dca7e3..f334338 100644 --- a/androidApp/src/main/kotlin/bzh/ajaury/chombev/ChomBevApp.kt +++ b/androidApp/src/main/kotlin/bzh/ajaury/chombev/ChomBevApp.kt @@ -10,10 +10,11 @@ class ChomBevApp : Application() { if (isDebuggable()) { enableStrictMode() } + + SentryHelper.initializeSentry() } - private fun isDebuggable(): Boolean = - applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 + private fun isDebuggable(): Boolean = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 /** * Enables StrictMode for debug builds only. All violations are logged diff --git a/docs/sentry-glitchtip.md b/docs/sentry-glitchtip.md new file mode 100644 index 0000000..6268359 --- /dev/null +++ b/docs/sentry-glitchtip.md @@ -0,0 +1,164 @@ +# Crash reporting — Sentry SDK on GlitchTip + +Chom Bev reports crashes and handled errors with the +[Sentry Kotlin Multiplatform SDK](https://docs.sentry.io/platforms/kotlin/guides/kotlin-multiplatform/), +but sends them to [GlitchTip](https://glitchtip.com) instead of Sentry. GlitchTip is an +open-source, Sentry-API-compatible backend, so the Sentry SDKs and `glitchtip-cli` work against +it unchanged — only the DSN (and the CLI's `url`) point somewhere else. + +Our instance is the hosted EU one: . + +| | | +|-----------------------|------------------------------------------------------------------------------------------------------| +| SDK | `io.sentry.kotlin.multiplatform` (see `sentry` in `gradle/libs.versions.toml`) | +| Gradle plugin | `io.sentry.kotlin.multiplatform.gradle`, applied in `shared/build.gradle.kts` | +| iOS native dependency | `sentry-cocoa` via Swift Package Manager, declared in the Xcode project | +| Initialisation | `initializeSentry()` in `shared/src/commonMain/kotlin/bzh/ajaury/chombev/SentryHelper.kt` | +| Called from | `ChomBevApp.onCreate()` (Android), `AppDelegate.application(_:didFinishLaunchingWithOptions:)` (iOS) | + +There are two distinct pieces of configuration, and they are easy to confuse: + +1. **The DSN** — where the *app* sends events at runtime. Injected at build time from + `local.properties`. See [Runtime configuration](#1-runtime-configuration--the-dsn). +2. **The auth token** — how *your machine or CI* uploads debug symbols to GlitchTip. + Stored in `.sentryclirc`. See [CLI configuration](#2-cli-configuration--sentryclirc). + +Neither is committed. + +--- + +## 1. Runtime configuration — the DSN + +The DSN is a write-only endpoint URL identifying the GlitchTip project. It is kept out of +version control by the `gwenedeg.secrets` convention plugin +(`build-logic/src/main/kotlin/gwenedeg.secrets.gradle.kts`), which reads it at build +time and generates an internal `BuildSecrets` object into `commonMain`. + +### Setup + +Add the DSN to `local.properties` at the repository root (this file is git-ignored): + +```properties +glitchtip.dsn=https://@eu.glitchtip.com/ +``` + +Find the value in GlitchTip under **Settings → Projects → _Chom Bev e Brezhoneg_ → Client Keys (DSN)**. + +On CI, set the `GLITCHTIP_DSN` environment variable instead — it takes precedence over +`local.properties`. + +### Behaviour when it is missing + +`initializeSentry()` returns early when the DSN is blank, so a fresh clone builds and runs +with crash reporting simply disabled. No placeholder or dummy DSN is needed. + +### Adding other secrets + +Extend the `secret(environmentVariable, localPropertyKey)` helper in the convention plugin +rather than hardcoding values in Kotlin sources. + +--- + +## 2. CLI configuration — `.sentryclirc` + +`sentry-cli` needs an auth token to upload debug symbols. It reads its settings from a +`.sentryclirc` INI file in the current working directory (and from `~/.sentryclirc`), +or from environment variables. + +> **`.sentryclirc` contains a credential and must never be committed.** +> It is listed in `.gitignore`. Only `.sentryclirc.example` is versioned. + +### Setup + +```bash +cp .sentryclirc.example .sentryclirc +chmod 600 .sentryclirc +``` + +Then edit `.sentryclirc` and replace the token placeholder: + +```ini +[auth] +token= + +[defaults] +url=https://eu.glitchtip.com/ +org=kerlenn-sten-kidna +project=chom-bev-e-brezhoneg +``` + +- `url` is mandatory: without it `glitchtip-cli` talks to sentry.io, not GlitchTip. +- `org` and `project` are the *slugs* shown in the GlitchTip URLs, not the display names. + +### Creating the token + +In GlitchTip: **user menu → Profile → Auth Tokens → Create New Token**. Grant it at least +`project:read`, `project:write` and `project:releases`. Copy the token immediately — it is +shown only once. + +### On CI + +Do not write the file. Export the equivalent environment variables, sourced from your CI +secret store: + +```bash +export SENTRY_URL=https://eu.glitchtip.com/ +export SENTRY_ORG=kerlenn-sten-kidna +export SENTRY_PROJECT=chom-bev-e-brezhoneg +export SENTRY_AUTH_TOKEN=*** +``` + +### Installing glitchtip-cli + +```bash +curl -fsSL https://glitchtip.com/install.sh | sh +``` + +Check that the configuration resolves: + +```bash +glitchtip-cli info +``` + +It should print the GlitchTip URL and report the token as valid. + +--- + +## 3. iOS Archive — uploading dSYM files + +### Why this is needed + +A release iOS binary ships without symbol names. Without the matching dSYM, GlitchTip shows +crash frames as raw memory addresses. Uploading the dSYM lets it resolve them back to +function names and line numbers. + +The shared Kotlin framework is linked **statically** (`isStatic = true` in +`shared/build.gradle.kts`), so Kotlin/Native frames end up in the app's own dSYM — there is +no separate framework dSYM to upload. + +### Prerequisites + +You need `glitchtip-cli` installed and `.sentryclirc` filled in ([section 2](#2-cli-configuration--sentryclirc)). + +### Upload after an archive + +1. **Product → Archive** in Xcode. +2. In the Organizer, right-click the archive → **Show in Finder**. +3. Right-click the `.xcarchive` → **Show Package Contents**. +4. Upload the `dSYMs` folder: + +```bash +cd /path/to/Gwenedeg +glitchtip-cli debug-files upload /path/to/Gwenedeg.xcarchive/dSYMs +``` + +Add `--include-sources` if you also want source context attached to native frames. + +## 4. References + +- [Sentry — Kotlin Multiplatform SDK](https://docs.sentry.io/platforms/kotlin/guides/kotlin-multiplatform/) +- [Sentry — Uploading debug symbols for Apple platforms](https://docs.sentry.io/platforms/apple/guides/ios/dsym/) +- [Sentry — data collected by the SDK](https://docs.sentry.io/platforms/kotlin/guides/kotlin-multiplatform/data-management/data-collected/) +- [GlitchTip documentation](https://glitchtip.com/documentation) +- [GlitchTip CLI documentation](https://glitchtip.com/documentation/cli) +- [Kotlin — symbolicating iOS crash reports](https://kotlinlang.org/docs/native-ios-symbolication.html) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a06a169..a2d471f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,24 +6,24 @@ androidTargetSdk = "37" androidxActivity = "1.13.0" androidxLifecycle = "2.11.0" androidxWebkit = "1.16.0" -media3 = "1.11.0" -navigation3 = "1.1.1" -composeMultiplatform = "1.11.1" composeIconsExtended = "1.7.3" -glitchtips = "8.52.0" +composeMultiplatform = "1.11.1" +datastore = "1.2.1" # https://developer.android.com/jetpack/androidx/releases/datastore googleServices = "4.5.0" +kermit = "2.1.0" +koin = "4.2.2" kotlin = "2.4.10" +kotlinSerialization = "2.4.10" kotlinWrappers = "2026.8.0" kotlinxBrowser = "0.5.0" kotlinxCoroutines = "1.11.0" -kotlinSerialization = "2.4.10" kotlinxSerializationJson = "1.11.0" material3 = "1.11.0-alpha07" -koin = "4.2.2" -kermit = "2.1.0" -vorbisspi = "1.0.3.3" -datastore = "1.2.1" # https://developer.android.com/jetpack/androidx/releases/datastore +media3 = "1.11.0" +navigation3 = "1.1.1" okio = "3.18.1" +sentry = "0.27.0" +vorbisspi = "1.0.3.3" [libraries] # Gradle plugin artifacts, used by build-logic convention plugins only @@ -44,7 +44,6 @@ compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.re compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } -glitchtips = { module = "io.sentry:sentry-android", version.ref = "glitchtips" } koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } @@ -75,4 +74,5 @@ composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "k googleServices = { id = "com.google.gms.google-services", version.ref = "googleServices" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization"} \ No newline at end of file +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization"} +sentry = { id = "io.sentry.kotlin.multiplatform.gradle", version.ref = "sentry" } \ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 99b29ef..4afb7b6 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -6,6 +6,10 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + F36B7319302CAEC900B3E490 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = F36B7318302CAEC900B3E490 /* Sentry */; }; +/* End PBXBuildFile section */ + /* Begin PBXFileReference section */ 9DA43AABF87DF46F5E794E72 /* Chom Bev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chom Bev.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -41,6 +45,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F36B7319302CAEC900B3E490 /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -85,6 +90,7 @@ ); name = iosApp; packageProductDependencies = ( + F36B7318302CAEC900B3E490 /* Sentry */, ); productName = iosApp; productReference = 9DA43AABF87DF46F5E794E72 /* Chom Bev.app */; @@ -114,6 +120,9 @@ ); mainGroup = A562F15E4A0F12F420BB5B12; minimizedProjectReferenceProxies = 1; + packageReferences = ( + F36B7317302CAEC900B3E490 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 47B0C8770C82BEADFE13EF77 /* Products */; projectDirPath = ""; @@ -176,7 +185,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "$(TEAM_ID)"; + DEVELOPMENT_TEAM = 492RZGQ5SU; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = iosApp/Info.plist; @@ -269,7 +278,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "$(TEAM_ID)"; + DEVELOPMENT_TEAM = 492RZGQ5SU; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = iosApp/Info.plist; @@ -368,6 +377,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + F36B7317302CAEC900B3E490 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/getsentry/sentry-cocoa"; + requirement = { + kind = exactVersion; + version = 8.58.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + F36B7318302CAEC900B3E490 /* Sentry */ = { + isa = XCSwiftPackageProductDependency; + package = F36B7317302CAEC900B3E490 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 50584B9465876AB65BD227D3 /* Project object */; } diff --git a/iosApp/iosApp/AppDelegate.swift b/iosApp/iosApp/AppDelegate.swift new file mode 100644 index 0000000..818b0ab --- /dev/null +++ b/iosApp/iosApp/AppDelegate.swift @@ -0,0 +1,18 @@ +// +// AppDelegate.swift +// iosApp +// +// Created by Antoine Jaury on 12/08/2026. +// +import UIKit +import Shared + +final class AppDelegate: NSObject, UIApplicationDelegate { + + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + SentryHelper().initializeSentry() + return true + } +} diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index d83dca6..ce67523 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -2,6 +2,10 @@ import SwiftUI @main struct iOSApp: App { + + @UIApplicationDelegateAdaptor(AppDelegate.self) + private var appDelegate + var body: some Scene { WindowGroup { ContentView() diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 7d6919f..9462145 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -2,6 +2,8 @@ plugins { id("gwenedeg.kmp.library") id("gwenedeg.compose") alias(libs.plugins.kotlinSerialization) + id("gwenedeg.secrets") + alias(libs.plugins.sentry) } kotlin { diff --git a/shared/src/commonMain/kotlin/bzh/ajaury/chombev/SentryHelper.kt b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/SentryHelper.kt new file mode 100644 index 0000000..7e2ee30 --- /dev/null +++ b/shared/src/commonMain/kotlin/bzh/ajaury/chombev/SentryHelper.kt @@ -0,0 +1,20 @@ +package bzh.ajaury.chombev + +import io.sentry.kotlin.multiplatform.Sentry + +object SentryHelper { + fun initializeSentry() { + // The DSN is injected at build time from local.properties (or the GLITCHTIP_DSN + // environment variable) so it never lands in version control. Without it, crash + // reporting is simply disabled. + val dsn = BuildSecrets.GLITCHTIP_DSN + if (dsn.isBlank()) return + + Sentry.init { options -> + options.dsn = dsn + options.debug = true + options.sendDefaultPii = false + options.enableAutoSessionTracking = false + } + } +}