feature: integrate Sentry SDK for crash reporting on Android and iOS
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -20,11 +20,6 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</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>
|
||||
|
||||
</manifest>
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
+10
-10
@@ -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" }
|
||||
@@ -76,3 +75,4 @@ googleServices = { id = "com.google.gms.google-services", version.ref = "googleS
|
||||
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"}
|
||||
sentry = { id = "io.sentry.kotlin.multiplatform.gradle", version.ref = "sentry" }
|
||||
@@ -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 */;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import SwiftUI
|
||||
|
||||
@main
|
||||
struct iOSApp: App {
|
||||
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self)
|
||||
private var appDelegate
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
|
||||
@@ -2,6 +2,8 @@ plugins {
|
||||
id("gwenedeg.kmp.library")
|
||||
id("gwenedeg.compose")
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
id("gwenedeg.secrets")
|
||||
alias(libs.plugins.sentry)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user