Skip to content

[#117] Send Sentry report if file path given for logs is not valid #122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,6 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.6.1'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
implementation "androidx.datastore:datastore-preferences:${versions.dataStore}"
}
25 changes: 18 additions & 7 deletions app/src/main/java/com/steamclock/steamclogsample/App.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.steamclock.steamclogsample

import android.app.Application
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import com.steamclock.steamclog.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
* steamclog
Expand All @@ -10,13 +18,16 @@ import com.steamclock.steamclog.*
class App : Application() {
override fun onCreate() {
super.onCreate()
clog.initWith(Config(
isDebug = BuildConfig.DEBUG,
fileWritePath = externalCacheDir,
autoRotateConfig = AutoRotateConfig(10L), // Short rotate so we can more easily test
filtering = appFiltering,
detailedLogsOnUserReports = true
))
clog.initWith(
application = this,
config = Config(
isDebug = BuildConfig.DEBUG,
fileWritePath = externalCacheDir,
autoRotateConfig = AutoRotateConfig(10L), // Short rotate so we can more easily test
filtering = App.appFiltering,
detailedLogsOnUserReports = true
)
)
}

companion object {
Expand Down
77 changes: 74 additions & 3 deletions app/src/main/java/com/steamclock/steamclogsample/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.steamclock.steamclogsample

import android.app.Application
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
Expand All @@ -8,14 +9,25 @@ import android.view.View
import android.widget.AdapterView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.steamclock.steamclog.*
import com.steamclock.steamclogsample.databinding.ActivityMainBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date

import java.util.*

class MainActivity : AppCompatActivity() {

Expand Down Expand Up @@ -60,6 +72,14 @@ class MainActivity : AppCompatActivity() {
binding.enableExtraConfigInfo.setOnCheckedChangeListener { _, checked ->
clog.config.extraInfo = if (checked) { this::getExtraInfo } else null
}
binding.forceInvalidPathCheck.setOnCheckedChangeListener { _, checked ->
val filePath = if (checked) File("Idontexist") else externalCacheDir
val updatedConfig = updateConfigFilePath(filePath)
clog.initWith(
application = application,
config = updatedConfig
)
}

binding.addUserId.setOnClickListener { clog.setUserId("1234") }

Expand Down Expand Up @@ -106,13 +126,32 @@ class MainActivity : AppCompatActivity() {
clog.warn("LogLevel changed to ${clog.config.logLevel.title}")
}
}

// Testing app DataStore to make sure having a Steamclog datastore won't interfere with an
// app's local DataStore; this test won't be visible in app, devs can verify datastore read/write
// in console logs after the app is launched.
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
// Doesn't need to be lifecycle aware for this test
val timestamp =
AppDataStore(application).apply {
var testValueBefore = getTestValue.firstOrNull()
setTestValue("UpdatedValue @ ${Date().time}")
var testValueAfter = getTestValue.firstOrNull()
clog.debug("Testing app DataStore functionality")
clog.debug("Before: $testValueBefore, After: $testValueAfter")
}
}
}
}
}

private fun showMessageIfCrashReportingNotEnabled() {
if (clog.config.logLevel.remote == LogLevel.None) {
Toast.makeText(applicationContext,
"Set Log Level to Release or Release Advanced to enable crash reporting",
Toast.LENGTH_LONG).show()

}
}

Expand Down Expand Up @@ -245,6 +284,21 @@ class MainActivity : AppCompatActivity() {
Toast.makeText(applicationContext, "Copied to clipboard", Toast.LENGTH_LONG).show()
}

private fun updateConfigFilePath(newFileWritePath: File?): Config {
// Only the fileWrite path is changed, everything else retains current config values.
return Config(
isDebug = clog.config.isDebug,
fileWritePath = newFileWritePath,
keepLogsForDays = clog.config.keepLogsForDays,
autoRotateConfig = clog.config.autoRotateConfig,
requireRedacted = clog.config.requireRedacted,
filtering = clog.config.filtering,
logLevel = clog.config.logLevel,
detailedLogsOnUserReports = clog.config.detailedLogsOnUserReports,
extraInfo = clog.config.extraInfo
)
}

// Test logging objects
class RedactableParent : Any(), Redactable {
val safeProp = "name"
Expand Down Expand Up @@ -272,4 +326,21 @@ class MainActivity : AppCompatActivity() {
object TestButtonPressed: AnalyticEvent("test_button_pressed", mapOf())
object TestButtonPressedWithRedactable: AnalyticEvent("test_button_pressed", mapOf("redactableObject" to RedactableChild()))
}
}

/**
* Verifying that we can have a DataStore in the app "separate" from the Steamclog DataStore
*/
private class AppDataStore(private val application: Application) {
companion object {
private val Context.AppDataStore: DataStore<Preferences> by preferencesDataStore(name = "AppDataStore")
private val testKey = stringPreferencesKey("testKey")
}
val getTestValue: Flow<String>
get() = application.AppDataStore.data.map {
it[testKey] ?: "DefaultText"
}
suspend fun setTestValue(value: String) {
application.AppDataStore.edit { it[testKey] = value }
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<CheckBox
android:text="Force invalid file write path"
android:id="@+id/force_invalid_path_check"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<View
android:layout_width="match_parent"
android:layout_height="20dp" />
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ buildscript {
"compileSdk": 33,
"kotlin": "1.8.10",
"timber": "5.0.1",
"sentry": "6.23.0"
"sentry": "6.23.0",
"dataStore" : "1.0.0"
]

repositories {
Expand Down
1 change: 1 addition & 0 deletions steamclog/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,5 @@ dependencies {
implementation "com.jakewharton.timber:timber:${versions.timber}"
// https://github.com/getsentry/sentry-java/releases
implementation "io.sentry:sentry-android:${versions.sentry}"
implementation "androidx.datastore:datastore-preferences:${versions.dataStore}"
}
33 changes: 33 additions & 0 deletions steamclog/src/main/java/com/steamclock/steamclog/SClogDataStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.steamclock.steamclog

import android.app.Application
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.preferencesDataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
* https://developer.android.com/topic/libraries/architecture/datastore
*/
class SClogDataStore(private val application: Application) {
companion object {
private val Context.SClogDataStore: DataStore<Preferences> by preferencesDataStore(name = "SClogDataStore")
private val hasReportedFilepathErrorKey = booleanPreferencesKey("has_logged_file_creation_failure")
}

/**
* hasReportedFilepathError indicates if Steamclog has reported a Sentry error regarding
* it's inability to use the given filePath to store logs.
*/
val getHasReportedFilepathError: Flow<Boolean>
get() = application.SClogDataStore.data.map {
it[hasReportedFilepathErrorKey] ?: false
}
suspend fun setHasReportedFilepathError(value: Boolean) {
application.SClogDataStore.edit { it[hasReportedFilepathErrorKey] = value }
}
}
55 changes: 51 additions & 4 deletions steamclog/src/main/java/com/steamclock/steamclog/Steamclog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

package com.steamclock.steamclog

import android.app.Application
import android.content.Context
import io.sentry.Sentry
import io.sentry.protocol.User
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import org.jetbrains.annotations.NonNls
import timber.log.Timber
import java.io.File
Expand All @@ -27,6 +32,7 @@ object SteamcLog {
private var customDebugTree: ConsoleDestination
private var sentryTree: SentryDestination
private var externalLogFileTree: ExternalLogFileDestination
private var coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

//---------------------------------------------
// Public properties
Expand All @@ -48,16 +54,57 @@ object SteamcLog {
// Don't plant yet; fileWritePath required before we can start writing to ExternalLogFileDestination
}

fun initWith(config: Config) {
fun initWith(application: Application, config: Config) {
this.config = config
this.config.fileWritePath?.let {

// Setup ExternalLogFileDestination
if (this.config.fileWritePath != null && this.config.fileWritePath!!.exists()) {
updateTree(externalLogFileTree, true)
} ?: run {
logInternal(LogLevel.Warn, "fileWritePath given was null; cannot log to external file")
} else {
// We have seen issues where some devices are failing to support some of the default file
// paths (ie. externalCacheDir); the code below attempts to catch that case and report it
// proactively to Sentry as an error once per app install.
checkForLogAccessError(application, this.config.fileWritePath)
}
logInternal(LogLevel.Info, "Steamclog initialized:\n$this")
}

/**
* We have seen issues where some devices are failing to support some of the default file
* paths (ie. externalCacheDir); the code below attempts to catch that case and report it
* proactively to Sentry as an error once per app install.
*/
private fun checkForLogAccessError(application: Application, fileWritePath: File?) {
coroutineScope.launch {
// We have seen issues where some devices are failing to support some of the default file
// paths (ie. externalCacheDir); the code below attempts to catch that case and report it
// proactively to Sentry as an error once per app install.
val sentryErrorTitle = "Steamclog could not create external logs"
logInternal(LogLevel.Info, "File path $fileWritePath is invalid")

try {
// Attempt to log error only once to avoid overwhelming Sentry.
// runBlocking usage was recommended in the google docs as the way to synchronously
// call the datastore methods; we need to use this with caution
// https://developer.android.com/topic/libraries/architecture/datastore#synchronous
val dataStore = SClogDataStore(application)
val alreadyReportedFailure = dataStore.getHasReportedFilepathError.firstOrNull()
val isSentryEnabled = clog.config.logLevel.remote != LogLevel.None

if (isSentryEnabled && alreadyReportedFailure == false) {
logInternal(LogLevel.Error, sentryErrorTitle)
dataStore.setHasReportedFilepathError(true)
} else {
logInternal(LogLevel.Warn, sentryErrorTitle)
}
} catch (e: Exception) {
logInternal(LogLevel.Info, "Could not read local DataStore")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safeguarding against any error that might occur with DataStore; since this is the first I've worked with it I want to make sure that failures with it do cause the app to crash.

logInternal(LogLevel.Info, e.message ?: "No error message given")
logInternal(LogLevel.Error, sentryErrorTitle)
}
}
}

//---------------------------------------------
// Public Logging <level> calls
//
Expand Down