Skip to content

Commit

Permalink
Add sentry for crash reporting (#71)
Browse files Browse the repository at this point in the history
* Add Sentry dependency

* Add BuildKonfig dependency

* Provide Sentry DSN as build config field

* Add `Initializer` interface

* Add platform specific Sentry intializer

* Provide `SentryInitializer` as part of DI graph

* Initialise initializers on app create

* Add Sentry dependency to `androidApp` module

* Capture handled errors using Sentry

* Report feed URL when adding a feed throws error

* Add Sentry Android Gradle plugin

This will provide the functionality to upload proguard/r8 mappings

* Add sentry environment variables when creating Android production build

* Pass sentry DSN as Gradle build parameter when creating production Android build
  • Loading branch information
msasikanth authored Aug 16, 2023
1 parent f17f669 commit 26ba23e
Show file tree
Hide file tree
Showing 22 changed files with 229 additions and 2 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/android_prod_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
TERM: dumb
ORG_GRADLE_PROJECT_READER_KEYSTORE_PASSWORD: ${{ secrets.READER_KEYSTORE_PASSWORD }}
ORG_GRADLE_PROJECT_READER_KEY_PASSWORD: ${{ secrets.READER_KEY_PASSWORD }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
steps:
- name: Setup versionName regardless of how this action is triggered
id: version_name
Expand Down Expand Up @@ -47,7 +50,7 @@ jobs:
run: gpg --batch --yes --quiet --decrypt --passphrase=${{ secrets.KEYSTORE_ENCRYPT_KEY }} --output release/reader.jks release/reader.gpg

- name: Build release artifacts
run: ./gradlew --no-daemon androidApp:bundleRelease
run: ./gradlew --no-daemon androidApp:bundleRelease -Psentry.dsn=${{ secrets.SENTRY_DSN }}

- name: Upload Release Bundle
uses: actions/upload-artifact@v3
Expand Down
4 changes: 4 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.sentry.android)
}

sentry { tracingInstrumentation { enabled = false } }

kotlin { android() }

android {
Expand Down Expand Up @@ -77,4 +80,5 @@ dependencies {
implementation(libs.kotlininject.runtime)
ksp(libs.kotlininject.compiler)
implementation(libs.androidx.work)
implementation(libs.sentry)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import dev.sasikanth.rss.reader.repository.RssRepository
import io.sentry.kotlin.multiplatform.Sentry
import java.lang.Exception
import java.time.Duration

Expand All @@ -45,6 +46,7 @@ class FeedsRefreshWorker(
rssRepository.updateFeeds()
Result.success()
} catch (e: Exception) {
Sentry.captureException(e)
Result.failure()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class ReaderApplication : Application(), Configuration.Provider {
super.onCreate()
initialiseLogging()
enqueuePeriodicFeedsRefresh()

appComponent.initializers.forEach { it.initialize() }
}

override fun getWorkManagerConfiguration(): Configuration {
Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ plugins {
alias(libs.plugins.android.library).apply(false)
alias(libs.plugins.compose).apply(false)
alias(libs.plugins.spotless).apply(false)
alias(libs.plugins.buildKonfig).apply(false)
alias(libs.plugins.sentry.android).apply(false)
}

allprojects {
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ android.useAndroidX=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
#Sentry
# do_not_change_here
sentry.dsn=
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ kotlininject = "0.6.1"
ksp = "1.8.22-1.0.11"
material_color_utilities = "1.0.0-alpha01"
ksoup = "0.1.4"
sentry = "0.2.1"
sentry_android = "3.12.0"
buildKonfig = "0.13.3"

[libraries]
compose_runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" }
Expand Down Expand Up @@ -69,6 +72,7 @@ kotlininject-compiler = { module = 'me.tatarka.inject:kotlin-inject-compiler-ksp
kotlininject-runtime = { module = 'me.tatarka.inject:kotlin-inject-runtime', version.ref = 'kotlininject' }
material_color_utilities = { module = "dev.sasikanth:material-color-utilities", version.ref = "material_color_utilities" }
ksoup = { module = "com.mohamedrejeb.ksoup:ksoup-html", version.ref = "ksoup" }
sentry = { module = "io.sentry:sentry-kotlin-multiplatform", version.ref = "sentry" }

[plugins]
android_application = { id = "com.android.application", version.ref = "android_gradle_plugin" }
Expand All @@ -81,6 +85,8 @@ sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
moko_resources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko_resources" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
ksp = { id = 'com.google.devtools.ksp', version.ref = 'ksp' }
buildKonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildKonfig" }
sentry_android = { id = "io.sentry.android.gradle", version.ref = "sentry_android" }

[bundles]
compose = [ "compose-runtime", "compose-foundation", "compose_material", "compose_material3", "compose_resources" ]
Expand Down
18 changes: 18 additions & 0 deletions iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
F85CB1118929364A9C6EFABC /* Frameworks */,
2134C13603D0B299603D9F49 /* [CP] Copy Pods Resources */,
22C8302A2A0C849A007E0725 /* Copy Moko Resources */,
0E668490121D39D1C4498DE3 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
Expand Down Expand Up @@ -178,6 +179,23 @@
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
0E668490121D39D1C4498DE3 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
2134C13603D0B299603D9F49 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down
6 changes: 6 additions & 0 deletions iosApp/iosApp/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ class AppDelegate: NSObject, UIApplicationDelegate {
lazy var applicationComponent: InjectApplicationComponent = InjectApplicationComponent()

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

applicationComponent.initializers
.compactMap { $0 as! any Initializer }
.forEach { initializer in
initializer.initialize()
}

BGTaskScheduler.shared.register(forTaskWithIdentifier: "dev.sasikanth.reader.feeds_refresh", using: DispatchQueue.main) { (task) in
self.refreshFeeds(task: task as! BGAppRefreshTask)
Expand Down
12 changes: 12 additions & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/
import com.android.build.api.dsl.ManagedVirtualDevice
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
import dev.icerock.gradle.MRVisibility.Internal

@Suppress("DSL_SCOPE_VIOLATION")
Expand All @@ -25,6 +26,14 @@ plugins {
alias(libs.plugins.sqldelight)
alias(libs.plugins.moko.resources)
alias(libs.plugins.ksp)
alias(libs.plugins.buildKonfig)
}

buildkonfig {
packageName = "dev.sasikanth.reader"
defaultConfigs {
buildConfigField(STRING, "SENTRY_DSN", project.properties["sentry.dsn"] as? String)
}
}

multiplatformResources {
Expand Down Expand Up @@ -64,6 +73,8 @@ kotlin {
homepage = "https://github.com/msasikanth/rss_reader"
ios.deploymentTarget = "14.1"
podfile = project.file("../iosApp/Podfile")
pod("Sentry", "~> 8.4.0")

framework {
baseName = "shared"
isStatic = true
Expand Down Expand Up @@ -91,6 +102,7 @@ kotlin {
implementation(libs.androidx.collection)
implementation(libs.material.color.utilities)
implementation(libs.ksoup)
implementation(libs.sentry)
}
}
val commonTest by getting {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlParser
import dev.sasikanth.rss.reader.models.FeedPayload
import dev.sasikanth.rss.reader.models.PostPayload
import io.github.aakira.napier.Napier
import io.sentry.kotlin.multiplatform.Sentry
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import org.xmlpull.v1.XmlPullParser
Expand Down Expand Up @@ -105,6 +106,7 @@ internal class AndroidAtomParser(private val parser: XmlPullParser, private val
try {
ZonedDateTime.parse(date, atomDateFormat).toEpochSecond() * 1000
} catch (e: Throwable) {
Sentry.captureException(e)
Napier.e("Parse date error: ${e.message}")
null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlParser
import dev.sasikanth.rss.reader.models.FeedPayload
import dev.sasikanth.rss.reader.models.PostPayload
import io.github.aakira.napier.Napier
import io.sentry.kotlin.multiplatform.Sentry
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatterBuilder
import java.util.Locale
Expand Down Expand Up @@ -104,6 +105,7 @@ internal class AndroidRssParser(private val parser: XmlPullParser, private val f
try {
ZonedDateTime.parse(date, this.rssDateFormat).toEpochSecond() * 1000
} catch (e: Throwable) {
Sentry.captureException(e)
Napier.e("Parse date error: ${e.message}")
null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2023 Sasikanth Miriyampalli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.sasikanth.rss.reader.sentry

import dev.sasikanth.rss.reader.di.scopes.AppScope
import dev.sasikanth.rss.reader.initializers.Initializer
import me.tatarka.inject.annotations.IntoSet
import me.tatarka.inject.annotations.Provides

actual interface SentryComponent {

@IntoSet
@Provides
@AppScope
fun providesSentryInitializer(bind: SentryInitializer): Initializer = bind
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2023 Sasikanth Miriyampalli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.sasikanth.rss.reader.sentry

import android.content.Context
import dev.sasikanth.reader.BuildKonfig
import dev.sasikanth.rss.reader.initializers.Initializer
import io.sentry.kotlin.multiplatform.Sentry
import me.tatarka.inject.annotations.Inject

@Inject
class SentryInitializer(private val context: Context) : Initializer {

override fun initialize() {
Sentry.init(context) { options -> options.dsn = BuildKonfig.SENTRY_DSN }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ package dev.sasikanth.rss.reader.di

import dev.sasikanth.rss.reader.components.ImageLoader
import dev.sasikanth.rss.reader.di.scopes.AppScope
import dev.sasikanth.rss.reader.initializers.Initializer
import dev.sasikanth.rss.reader.sentry.SentryComponent
import dev.sasikanth.rss.reader.utils.DefaultDispatchersProvider
import dev.sasikanth.rss.reader.utils.DispatchersProvider
import me.tatarka.inject.annotations.Provides

abstract class SharedApplicationComponent : DataComponent, ImageLoaderComponent {
abstract class SharedApplicationComponent : DataComponent, ImageLoaderComponent, SentryComponent {

abstract val imageLoader: ImageLoader

abstract val initializers: Set<Initializer>

@Provides @AppScope fun DefaultDispatchersProvider.bind(): DispatchersProvider = this
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import dev.sasikanth.rss.reader.database.PostWithMetadata
import dev.sasikanth.rss.reader.repository.RssRepository
import dev.sasikanth.rss.reader.utils.DispatchersProvider
import dev.sasikanth.rss.reader.utils.ObservableSelectedFeed
import io.sentry.kotlin.multiplatform.Sentry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
Expand Down Expand Up @@ -118,6 +119,7 @@ internal class HomeViewModel(
try {
rssRepository.addFeed(feedLink)
} catch (e: Exception) {
Sentry.captureException(e) { scope -> scope.setContext("feed_url", feedLink) }
_effects.emit(HomeEffect.ShowError(e.message))
} finally {
_state.update { it.copy(feedFetchingState = FeedFetchingState.Idle) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2023 Sasikanth Miriyampalli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.sasikanth.rss.reader.initializers

interface Initializer {
fun initialize()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2023 Sasikanth Miriyampalli
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.sasikanth.rss.reader.sentry

expect interface SentryComponent
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import dev.sasikanth.rss.reader.models.FeedPayload
import dev.sasikanth.rss.reader.models.PostPayload
import io.github.aakira.napier.Napier
import io.ktor.http.Url
import io.sentry.kotlin.multiplatform.Sentry
import platform.Foundation.NSDateFormatter
import platform.Foundation.timeIntervalSince1970

Expand Down Expand Up @@ -82,6 +83,7 @@ private fun String?.atomDateStringToEpochSeconds(): Long {
try {
atomDateFormatter.dateFromString(this.trim())
} catch (e: Exception) {
Sentry.captureException(e)
Napier.e("Parse date error: ${e.message}")
null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import dev.sasikanth.rss.reader.models.FeedPayload
import dev.sasikanth.rss.reader.models.PostPayload
import io.github.aakira.napier.Napier
import io.ktor.http.Url
import io.sentry.kotlin.multiplatform.Sentry
import platform.Foundation.NSDateFormatter
import platform.Foundation.timeIntervalSince1970

Expand Down Expand Up @@ -94,6 +95,7 @@ private fun String?.rssDateStringToEpochSeconds(): Long {
try {
abbrevTimezoneDateFormatter.dateFromString(this.trim())
} catch (e: Exception) {
Sentry.captureException(e)
Napier.e("Parse date error: ${e.message}")
null
}
Expand Down
Loading

0 comments on commit 26ba23e

Please sign in to comment.