feature: integrate Sentry SDK for crash reporting on Android and iOS

This commit is contained in:
Antoine Jaury
2026-09-30 11:18:54 +02:00
parent 87d4f3ce5d
commit 48a78e5743
14 changed files with 284 additions and 41 deletions
+3
View File
@@ -32,3 +32,6 @@ data/resources/src/commonMain/composeResources/files/records.json
keystore.properties keystore.properties
*.jks *.jks
*.keystore *.keystore
# Secrets — see docs/sentry-glitchtip.md (the .example template stays versioned)
.sentryclirc
+17
View File
@@ -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=<your-glitchtip-auth-token>
[defaults]
url=https://eu.glitchtip.com/
org=kerlenn-sten-kidna
project=chom-bev-e-brezhoneg
+12
View File
@@ -93,6 +93,18 @@ See [Resource's README.md](data/resources/README.md) to handle the resource file
audio files, audio files,
and subtitles). 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 ## License
This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.
-5
View File
@@ -1,10 +1,5 @@
# Android app # 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) ## StrictMode (debug builds)
The app enables Android [StrictMode](https://developer.android.com/reference/android/os/StrictMode) The app enables Android [StrictMode](https://developer.android.com/reference/android/os/StrictMode)
-16
View File
@@ -31,9 +31,6 @@ dependencies {
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.compose.uiToolingPreview) implementation(libs.compose.uiToolingPreview)
debugImplementation(libs.compose.uiTooling) debugImplementation(libs.compose.uiTooling)
// Crash logging
implementation(libs.glitchtips)
} }
android { android {
@@ -55,10 +52,6 @@ android {
.toInt() .toInt()
versionCode = 1 versionCode = 1
versionName = "1.0.0-alpha02" 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 { packaging {
resources { resources {
@@ -76,12 +69,3 @@ android {
targetCompatibility = JavaVersion.VERSION_11 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)
}
-5
View File
@@ -20,11 +20,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Injected from local.properties (glitchtip.dsn); an empty DSN disables the SDK. -->
<meta-data android:name="io.sentry.dsn" android:value="${glitchtipDsn}" />
<meta-data android:name="io.sentry.traces.sample-rate" android:value="0.01" />
</application> </application>
</manifest> </manifest>
@@ -10,10 +10,11 @@ class ChomBevApp : Application() {
if (isDebuggable()) { if (isDebuggable()) {
enableStrictMode() enableStrictMode()
} }
SentryHelper.initializeSentry()
} }
private fun isDebuggable(): Boolean = private fun isDebuggable(): Boolean = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
/** /**
* Enables StrictMode for debug builds only. All violations are logged * Enables StrictMode for debug builds only. All violations are logged
+164
View File
@@ -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: <https://eu.glitchtip.com>.
| | |
|-----------------------|------------------------------------------------------------------------------------------------------|
| 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://<key>@eu.glitchtip.com/<project-id>
```
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=<your-glitchtip-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)
+11 -11
View File
@@ -6,24 +6,24 @@ androidTargetSdk = "37"
androidxActivity = "1.13.0" androidxActivity = "1.13.0"
androidxLifecycle = "2.11.0" androidxLifecycle = "2.11.0"
androidxWebkit = "1.16.0" androidxWebkit = "1.16.0"
media3 = "1.11.0"
navigation3 = "1.1.1"
composeMultiplatform = "1.11.1"
composeIconsExtended = "1.7.3" 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" googleServices = "4.5.0"
kermit = "2.1.0"
koin = "4.2.2"
kotlin = "2.4.10" kotlin = "2.4.10"
kotlinSerialization = "2.4.10"
kotlinWrappers = "2026.8.0" kotlinWrappers = "2026.8.0"
kotlinxBrowser = "0.5.0" kotlinxBrowser = "0.5.0"
kotlinxCoroutines = "1.11.0" kotlinxCoroutines = "1.11.0"
kotlinSerialization = "2.4.10"
kotlinxSerializationJson = "1.11.0" kotlinxSerializationJson = "1.11.0"
material3 = "1.11.0-alpha07" material3 = "1.11.0-alpha07"
koin = "4.2.2" media3 = "1.11.0"
kermit = "2.1.0" navigation3 = "1.1.1"
vorbisspi = "1.0.3.3"
datastore = "1.2.1" # https://developer.android.com/jetpack/androidx/releases/datastore
okio = "3.18.1" okio = "3.18.1"
sentry = "0.27.0"
vorbisspi = "1.0.3.3"
[libraries] [libraries]
# Gradle plugin artifacts, used by build-logic convention plugins only # 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-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", 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" } 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-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose" } koin-compose = { module = "io.insert-koin:koin-compose" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } 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" } googleServices = { id = "com.google.gms.google-services", version.ref = "googleServices" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization"} kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization"}
sentry = { id = "io.sentry.kotlin.multiplatform.gradle", version.ref = "sentry" }
+30 -2
View File
@@ -6,6 +6,10 @@
objectVersion = 77; objectVersion = 77;
objects = { objects = {
/* Begin PBXBuildFile section */
F36B7319302CAEC900B3E490 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = F36B7318302CAEC900B3E490 /* Sentry */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
9DA43AABF87DF46F5E794E72 /* Chom Bev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chom Bev.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 9DA43AABF87DF46F5E794E72 /* Chom Bev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chom Bev.app"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -41,6 +45,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
F36B7319302CAEC900B3E490 /* Sentry in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -85,6 +90,7 @@
); );
name = iosApp; name = iosApp;
packageProductDependencies = ( packageProductDependencies = (
F36B7318302CAEC900B3E490 /* Sentry */,
); );
productName = iosApp; productName = iosApp;
productReference = 9DA43AABF87DF46F5E794E72 /* Chom Bev.app */; productReference = 9DA43AABF87DF46F5E794E72 /* Chom Bev.app */;
@@ -114,6 +120,9 @@
); );
mainGroup = A562F15E4A0F12F420BB5B12; mainGroup = A562F15E4A0F12F420BB5B12;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = (
F36B7317302CAEC900B3E490 /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
);
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = 47B0C8770C82BEADFE13EF77 /* Products */; productRefGroup = 47B0C8770C82BEADFE13EF77 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -176,7 +185,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "$(TEAM_ID)"; DEVELOPMENT_TEAM = 492RZGQ5SU;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_FILE = iosApp/Info.plist;
@@ -269,7 +278,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "$(TEAM_ID)"; DEVELOPMENT_TEAM = 492RZGQ5SU;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_FILE = iosApp/Info.plist;
@@ -368,6 +377,25 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* 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 */; rootObject = 50584B9465876AB65BD227D3 /* Project object */;
} }
+18
View File
@@ -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
}
}
+4
View File
@@ -2,6 +2,10 @@ import SwiftUI
@main @main
struct iOSApp: App { struct iOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self)
private var appDelegate
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ContentView()
+2
View File
@@ -2,6 +2,8 @@ plugins {
id("gwenedeg.kmp.library") id("gwenedeg.kmp.library")
id("gwenedeg.compose") id("gwenedeg.compose")
alias(libs.plugins.kotlinSerialization) alias(libs.plugins.kotlinSerialization)
id("gwenedeg.secrets")
alias(libs.plugins.sentry)
} }
kotlin { kotlin {
@@ -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
}
}
}