Skip to content

Commit d76ba7e

Browse files
authored
Merge pull request #129 from ooni/crash-reporting
feat: Configure Sentry crash reporting
2 parents 534bdf0 + 7b53a64 commit d76ba7e

File tree

19 files changed

+540
-48
lines changed

19 files changed

+540
-48
lines changed

composeApp/build.gradle.kts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ kotlin {
8888
iosSimulatorArm64()
8989

9090
cocoapods {
91-
ios.deploymentTarget = "12.0"
91+
ios.deploymentTarget = "15.3"
9292

9393
version = "1.0"
9494
summary = "Compose App"
@@ -100,6 +100,11 @@ kotlin {
100100
binaryOption("bundleId", "composeApp")
101101
}
102102

103+
pod("Sentry") {
104+
version = "~> 8.25"
105+
extraOpts += listOf("-compiler-option", "-fmodules")
106+
}
107+
103108
podfile = project.file("../iosApp/Podfile")
104109
}
105110

@@ -178,7 +183,11 @@ android {
178183
versionName = "1.0"
179184
resValue("string", "app_name", config.appName)
180185
resValue("string", "ooni_run_enabled", config.supportsOoniRun.toString())
181-
resValue("string", "supported_languages", config.supportedLanguages.joinToString(separator = ","))
186+
resValue(
187+
"string",
188+
"supported_languages",
189+
config.supportedLanguages.joinToString(separator = ","),
190+
)
182191
resourceConfigurations += config.supportedLanguages
183192
}
184193
packaging {

composeApp/composeApp.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ Pod::Spec.new do |spec|
88
spec.summary = 'Compose App'
99
spec.vendored_frameworks = 'build/cocoapods/framework/composeApp.framework'
1010
spec.libraries = 'c++'
11-
spec.ios.deployment_target = '12.0'
12-
11+
spec.ios.deployment_target = '15.3'
12+
spec.dependency 'Sentry', '~> 8.25'
1313

1414
if !Dir.exist?('build/cocoapods/framework/composeApp.framework') || Dir.empty?('build/cocoapods/framework/composeApp.framework')
1515
raise "

composeApp/src/commonMain/composeResources/values/strings-common.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
<string name="Settings_Advanced_RecentLogs">See recent logs</string>
101101
<string name="Settings_Advanced_LanguageSettings_Title">Language Setting</string>
102102
<string name="Settings_Storage_Label">Storage usage</string>
103+
<string name="Settings_Storage_Delete">Delete</string>
104+
<string name="Settings_Storage_Clear">Clear</string>
103105
<string name="Settings_WarmVPNInUse_Label">Warn when VPN is in use</string>
104106

105107
<string name="CategoryCode_ALDR_Name">Drugs &amp; Alcohol</string>
@@ -263,4 +265,7 @@
263265
<string name="measurement_anomaly">Anomaly</string>
264266
<string name="quiz_answer_correct">Correct answer</string>
265267
<string name="quiz_answer_incorrect">Incorrect answer</string>
268+
<string name="logs">Logs</string>
269+
<string name="share_logs">Share Logs</string>
270+
<string name="filter_logs">Filter Logs</string>
266271
</resources>

composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import co.touchlab.kermit.Logger
2222
import org.jetbrains.compose.ui.tooling.preview.Preview
2323
import org.ooni.probe.data.models.DeepLink
2424
import org.ooni.probe.di.Dependencies
25+
import org.ooni.probe.shared.PlatformInfo
2526
import org.ooni.probe.ui.navigation.BottomNavigationBar
2627
import org.ooni.probe.ui.navigation.Navigation
2728
import org.ooni.probe.ui.navigation.Screen
@@ -82,7 +83,12 @@ fun App(
8283
}
8384

8485
LaunchedEffect(Unit) {
85-
logAppStart(dependencies)
86+
Logger.addLogWriter(dependencies.crashMonitoring.logWriter)
87+
Logger.addLogWriter(dependencies.appLogger.logWriter)
88+
logAppStart(dependencies.platformInfo)
89+
}
90+
LaunchedEffect(Unit) {
91+
dependencies.crashMonitoring.setup()
8692
}
8793
LaunchedEffect(Unit) {
8894
dependencies.bootstrapTestDescriptors()
@@ -110,9 +116,9 @@ fun App(
110116
}
111117
}
112118

113-
private fun logAppStart(dependencies: Dependencies) {
114-
with(dependencies.platformInfo) {
115-
Logger.i(
119+
private fun logAppStart(platformInfo: PlatformInfo) {
120+
with(platformInfo) {
121+
Logger.v(
116122
"""
117123
---APP START---
118124
Platform: $platform ($osVersion)"

composeApp/src/commonMain/kotlin/org/ooni/probe/data/disk/ReadFile.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ReadFileOkio(
2020
readUtf8()
2121
}
2222
} catch (e: IOException) {
23-
Logger.w("Could not read $path", e)
23+
Logger.v("Could not read $path", e)
2424
null
2525
}
2626
}

composeApp/src/commonMain/kotlin/org/ooni/probe/data/disk/WriteFile.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class WriteFileOkio(
3030
try {
3131
absolutePath.parent?.let { fileSystem.createDirectories(it) }
3232
} catch (e: IOException) {
33-
Logger.w("Could not create file $path", e)
33+
Logger.v("Could not create file $path", e)
3434
return
3535
}
3636

composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,13 @@ import org.ooni.probe.domain.ShouldShowVpnWarning
6868
import org.ooni.probe.domain.TestRunStateManager
6969
import org.ooni.probe.domain.UploadMissingMeasurements
7070
import org.ooni.probe.shared.PlatformInfo
71+
import org.ooni.probe.shared.monitoring.AppLogger
72+
import org.ooni.probe.shared.monitoring.CrashMonitoring
7173
import org.ooni.probe.ui.dashboard.DashboardViewModel
7274
import org.ooni.probe.ui.descriptor.DescriptorViewModel
7375
import org.ooni.probe.ui.descriptor.add.AddDescriptorViewModel
7476
import org.ooni.probe.ui.descriptor.review.ReviewUpdatesViewModel
77+
import org.ooni.probe.ui.log.LogViewModel
7578
import org.ooni.probe.ui.onboarding.OnboardingViewModel
7679
import org.ooni.probe.ui.result.ResultViewModel
7780
import org.ooni.probe.ui.results.ResultsViewModel
@@ -125,6 +128,18 @@ class Dependencies(
125128
private val writeFile: WriteFile by lazy { WriteFileOkio(FileSystem.SYSTEM, baseFileDir) }
126129
private val deleteFiles: DeleteFiles by lazy { DeleteFilesOkio(FileSystem.SYSTEM, baseFileDir) }
127130

131+
// Monitoring
132+
133+
val crashMonitoring by lazy { CrashMonitoring(preferenceRepository) }
134+
val appLogger by lazy {
135+
AppLogger(
136+
readFile = readFile,
137+
writeFile = writeFile,
138+
deleteFiles = deleteFiles,
139+
backgroundDispatcher = backgroundDispatcher,
140+
)
141+
}
142+
128143
// Engine
129144

130145
private val taskEventMapper by lazy { TaskEventMapper(networkTypeFinder, json) }
@@ -346,6 +361,13 @@ class Dependencies(
346361
descriptorUpdates = getDescriptorUpdate::observeAvailableUpdatesState,
347362
)
348363

364+
fun logViewModel(onBack: () -> Unit) =
365+
LogViewModel(
366+
onBack = onBack,
367+
readLog = appLogger::read,
368+
clearLog = appLogger::clear,
369+
)
370+
349371
fun onboardingViewModel(
350372
goToDashboard: () -> Unit,
351373
goToSettings: () -> Unit,
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.ooni.probe.shared.monitoring
2+
3+
import co.touchlab.kermit.LogWriter
4+
import co.touchlab.kermit.Severity
5+
import kotlinx.coroutines.CoroutineDispatcher
6+
import kotlinx.coroutines.CoroutineScope
7+
import kotlinx.coroutines.flow.Flow
8+
import kotlinx.coroutines.flow.MutableStateFlow
9+
import kotlinx.coroutines.flow.map
10+
import kotlinx.coroutines.flow.onStart
11+
import kotlinx.coroutines.flow.update
12+
import kotlinx.coroutines.launch
13+
import kotlinx.coroutines.withContext
14+
import kotlinx.datetime.LocalDateTime
15+
import okio.Path.Companion.toPath
16+
import org.ooni.probe.data.disk.DeleteFiles
17+
import org.ooni.probe.data.disk.ReadFile
18+
import org.ooni.probe.data.disk.WriteFile
19+
import org.ooni.probe.shared.now
20+
import org.ooni.probe.ui.shared.logFormat
21+
22+
class AppLogger(
23+
private val readFile: ReadFile,
24+
private val writeFile: WriteFile,
25+
private val deleteFiles: DeleteFiles,
26+
private val backgroundDispatcher: CoroutineDispatcher,
27+
) {
28+
private val log = MutableStateFlow(emptyList<String>())
29+
30+
fun read(severity: Severity?): Flow<List<String>> =
31+
log
32+
.onStart {
33+
if (log.value.isEmpty()) {
34+
log.value = readFile(FILE_PATH).orEmpty().lines()
35+
}
36+
}
37+
.map { lines ->
38+
if (severity == null) {
39+
lines
40+
} else {
41+
lines.filter { line ->
42+
line.contains(": ${severity.name.uppercase()} :")
43+
}
44+
}
45+
}
46+
47+
suspend fun clear() {
48+
withContext(backgroundDispatcher) {
49+
log.value = emptyList()
50+
deleteFiles(FILE_PATH)
51+
}
52+
}
53+
54+
val logWriter = object : LogWriter() {
55+
override fun isLoggable(
56+
tag: String,
57+
severity: Severity,
58+
): Boolean = severity != Severity.Verbose
59+
60+
override fun log(
61+
severity: Severity,
62+
message: String,
63+
tag: String,
64+
throwable: Throwable?,
65+
) {
66+
CoroutineScope(backgroundDispatcher).launch {
67+
val logMessage =
68+
"${LocalDateTime.now().logFormat()} : ${severity.name.uppercase()} : $message"
69+
log.update { lines ->
70+
val newLines = (lines + logMessage).takeLast(MAX_LINES)
71+
writeFile(FILE_PATH, newLines.joinToString("\n"), append = false)
72+
newLines
73+
}
74+
}
75+
}
76+
}
77+
78+
companion object {
79+
private val FILE_PATH = "Log/logger.txt".toPath()
80+
private const val MAX_LINES = 1000
81+
}
82+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package org.ooni.probe.shared.monitoring
2+
3+
import co.touchlab.kermit.LogWriter
4+
import co.touchlab.kermit.Severity
5+
import io.sentry.kotlin.multiplatform.Sentry
6+
import io.sentry.kotlin.multiplatform.SentryLevel
7+
import io.sentry.kotlin.multiplatform.protocol.Breadcrumb
8+
import kotlinx.coroutines.flow.collect
9+
import kotlinx.coroutines.flow.onEach
10+
import org.ooni.probe.data.models.SettingsKey
11+
import org.ooni.probe.data.repositories.PreferenceRepository
12+
13+
class CrashMonitoring(
14+
private val preferencesRepository: PreferenceRepository,
15+
) {
16+
private var isEnabled = false
17+
18+
suspend fun setup() {
19+
preferencesRepository.getValueByKey(SettingsKey.SEND_CRASH)
20+
.onEach { sendCrash ->
21+
if (sendCrash == true) {
22+
Sentry.init {
23+
it.dsn = SENTRY_DSN
24+
}
25+
isEnabled = true
26+
} else {
27+
isEnabled = false
28+
Sentry.close()
29+
}
30+
}
31+
.collect()
32+
}
33+
34+
val logWriter = object : LogWriter() {
35+
override fun isLoggable(
36+
tag: String,
37+
severity: Severity,
38+
): Boolean = isEnabled && severity != Severity.Verbose
39+
40+
override fun log(
41+
severity: Severity,
42+
message: String,
43+
tag: String,
44+
throwable: Throwable?,
45+
) {
46+
if (!isEnabled) return
47+
48+
if (severity == Severity.Error) {
49+
if (throwable != null) {
50+
addBreadcrumb(severity, message, tag)
51+
Sentry.captureException(throwable)
52+
} else {
53+
Sentry.captureMessage(message)
54+
}
55+
} else {
56+
addBreadcrumb(severity, message, tag)
57+
}
58+
}
59+
60+
private fun addBreadcrumb(
61+
severity: Severity,
62+
message: String,
63+
tag: String,
64+
) {
65+
Sentry.addBreadcrumb(
66+
Breadcrumb(
67+
level = when (severity) {
68+
Severity.Verbose,
69+
Severity.Debug,
70+
-> SentryLevel.DEBUG
71+
72+
Severity.Info -> SentryLevel.INFO
73+
Severity.Warn -> SentryLevel.WARNING
74+
Severity.Error -> SentryLevel.ERROR
75+
Severity.Assert -> SentryLevel.ERROR
76+
},
77+
type = when (severity) {
78+
Severity.Debug -> "debug"
79+
Severity.Error -> "error"
80+
else -> "default"
81+
},
82+
message = message,
83+
category = tag,
84+
),
85+
)
86+
}
87+
}
88+
89+
companion object {
90+
private const val SENTRY_DSN =
91+
"https://9dcd83d9519844188803aa817cdcd416@o155150.ingest.sentry.io/5619989"
92+
}
93+
}

0 commit comments

Comments
 (0)