feat(analytics): Integrate Datadog for RUM and Logging (#2578)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2025-08-01 16:54:46 -05:00
committed by GitHub
parent f5478b42c3
commit ab22a655c4
18 changed files with 348 additions and 194 deletions

View File

@@ -21,11 +21,11 @@ import android.os.Debug
import com.geeksville.mesh.android.AppPrefs
import com.geeksville.mesh.android.BuildUtils.isEmulator
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.util.Exceptions
import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.Firebase
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.setCustomKeys
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
@HiltAndroidApp
class MeshUtilApplication : GeeksvilleApplication() {
@@ -33,30 +33,15 @@ class MeshUtilApplication : GeeksvilleApplication() {
override fun onCreate() {
super.onCreate()
Logging.showLogs = BuildConfig.DEBUG
// We default to off in the manifest - we turn on here if the user approves
// leave off when running in the debugger
if (!isEmulator && (!BuildConfig.DEBUG || !Debug.isDebuggerConnected())) {
val crashlytics = Firebase.crashlytics
crashlytics.setCrashlyticsCollectionEnabled(isAnalyticsAllowed)
crashlytics.setCustomKey("debug_build", BuildConfig.DEBUG)
val crashlytics = FirebaseCrashlytics.getInstance()
val pref = AppPrefs(this)
crashlytics.setUserId(pref.getInstallId()) // be able to group all bugs per anonymous user
// We always send our log messages to the crashlytics lib, but they only get sent to the server if we report an exception
// This makes log messages work properly if someone turns on analytics just before they click report bug.
// send all log messages through crashyltics, so if we do crash we'll have those in the report
val standardLogger = Logging.printlog
Logging.printlog = { level, tag, message ->
crashlytics.log("$tag: $message")
standardLogger(level, tag, message)
}
fun sendCrashReports() {
if (isAnalyticsAllowed)
crashlytics.sendUnsentReports()
if (isAnalyticsAllowed) crashlytics.sendUnsentReports()
}
// Send any old reports if user approves
@@ -67,6 +52,30 @@ class MeshUtilApplication : GeeksvilleApplication() {
crashlytics.recordException(exception)
sendCrashReports() // Send the new report
}
Timber.plant(CrashlyticsTree())
}
}
}
class CrashlyticsTree : Timber.Tree() {
companion object {
private const val KEY_PRIORITY = "priority"
private const val KEY_TAG = "tag"
private const val KEY_MESSAGE = "message"
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
FirebaseCrashlytics.getInstance().setCustomKeys {
key(KEY_PRIORITY, priority)
key(KEY_TAG, tag ?: "No Tag")
key(KEY_MESSAGE, message)
}
if (t == null) {
FirebaseCrashlytics.getInstance().recordException(Exception(message))
} else {
FirebaseCrashlytics.getInstance().recordException(t)
}
}
}

View File

@@ -21,44 +21,54 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.provider.Settings
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import com.datadog.android.Datadog
import com.datadog.android.DatadogSite
import com.datadog.android.compose.enableComposeActionTracking
import com.datadog.android.core.configuration.Configuration
import com.datadog.android.log.Logger
import com.datadog.android.log.Logs
import com.datadog.android.log.LogsConfiguration
import com.datadog.android.privacy.TrackingConsent
import com.datadog.android.rum.GlobalRumMonitor
import com.datadog.android.rum.Rum
import com.datadog.android.rum.RumConfiguration
import com.datadog.android.timber.DatadogTree
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.analytics.AnalyticsProvider
import com.geeksville.mesh.analytics.FirebaseAnalytics
import com.geeksville.mesh.model.DeviceHardware
import com.geeksville.mesh.util.exceptionReporter
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailabilityLight
import com.suddenh4x.ratingdialog.AppRating
import timber.log.Timber
/**
* Created by kevinh on 1/4/15.
*/
open class GeeksvilleApplication : Application(), Logging {
/** Created by kevinh on 1/4/15. */
open class GeeksvilleApplication :
Application(),
Logging {
companion object {
lateinit var analytics: AnalyticsProvider
}
/// Are we running inside the testlab?
// / Are we running inside the testlab?
val isInTestLab: Boolean
get() {
val testLabSetting =
Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
if(testLabSetting != null)
info("Testlab is $testLabSetting")
val testLabSetting = Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
if (testLabSetting != null) info("Testlab is $testLabSetting")
return "true" == testLabSetting
}
private val analyticsPrefs: SharedPreferences by lazy {
getSharedPreferences("analytics-prefs", Context.MODE_PRIVATE)
}
private val analyticsPrefs: SharedPreferences by lazy { getSharedPreferences("analytics-prefs", MODE_PRIVATE) }
var isAnalyticsAllowed: Boolean
get() = analyticsPrefs.getBoolean("allowed", true)
set(value) {
analyticsPrefs.edit {
putBoolean("allowed", value)
}
analyticsPrefs.edit { putBoolean("allowed", value) }
// Change the flag with the providers
analytics.setEnabled(value && !isInTestLab) // Never do analytics in the test lab
@@ -68,12 +78,20 @@ open class GeeksvilleApplication : Application(), Logging {
fun askToRate(activity: AppCompatActivity) {
if (!isGooglePlayAvailable()) return
exceptionReporter { // we don't want to crash our app because of bugs in this optional feature
exceptionReporter {
// we don't want to crash our app because of bugs in this optional feature
AppRating.Builder(activity)
.setMinimumLaunchTimes(10) // default is 5, 3 means app is launched 3 or more times
.setMinimumDays(10) // default is 5, 0 means install day, 10 means app is launched 10 or more days later than installation
.setMinimumLaunchTimesToShowAgain(5) // default is 5, 1 means app is launched 1 or more times after neutral button clicked
.setMinimumDaysToShowAgain(14) // default is 14, 1 means app is launched 1 or more days after neutral button clicked
.setMinimumDays(10) // default is 5, 0 means install day, 10 means app is launched 10 or more days
// later than installation
.setMinimumLaunchTimesToShowAgain(
5,
) // default is 5, 1 means app is launched 1 or more times after neutral button
// clicked
.setMinimumDaysToShowAgain(
14,
) // default is 14, 1 means app is launched 1 or more days after neutral button
// clicked
.showIfMeetsConditions()
}
}
@@ -81,19 +99,64 @@ open class GeeksvilleApplication : Application(), Logging {
override fun onCreate() {
super.onCreate()
val firebaseAnalytics = com.geeksville.mesh.analytics.FirebaseAnalytics(this)
val logger =
Logger.Builder()
.setNetworkInfoEnabled(true)
.setLogcatLogsEnabled(true)
.setRemoteSampleRate(100f)
.setBundleWithTraceEnabled(true)
.setName("TimberLogger")
.build()
val firebaseAnalytics = FirebaseAnalytics(this)
analytics = firebaseAnalytics
// Set analytics per prefs
isAnalyticsAllowed = isAnalyticsAllowed
if (isAnalyticsAllowed || BuildConfig.DEBUG) {
// datadog analytics
val configuration =
Configuration.Builder(
clientToken = BuildConfig.datadogClientToken,
env = if (BuildConfig.DEBUG || true) "debug" else "release",
variant = BuildConfig.FLAVOR,
)
.useSite(DatadogSite.US5)
.setCrashReportsEnabled(true)
.setUseDeveloperModeWhenDebuggable(true)
.build()
val consent =
if (isAnalyticsAllowed) {
TrackingConsent.GRANTED
} else {
TrackingConsent.NOT_GRANTED
}
Datadog.initialize(this, configuration, consent)
Datadog.setVerbosity(Log.VERBOSE)
val rumConfiguration =
RumConfiguration.Builder(BuildConfig.datadogApplicationId)
.trackUserInteractions()
.trackLongTasks()
.trackBackgroundEvents(true)
.enableComposeActionTracking()
.build()
Rum.enable(rumConfiguration)
val logsConfig = LogsConfiguration.Builder().build()
Logs.enable(logsConfig)
Timber.plant(Timber.DebugTree(), DatadogTree(logger))
}
}
}
fun Context.isGooglePlayAvailable(): Boolean {
return GoogleApiAvailabilityLight.getInstance()
.isGooglePlayServicesAvailable(this)
.let {
it != ConnectionResult.SERVICE_MISSING &&
it != ConnectionResult.SERVICE_INVALID
}
}
fun setAttributes(firmwareVersion: String, deviceHardware: DeviceHardware) {
GlobalRumMonitor.get().addAttribute("firmware_version", firmwareVersion)
GlobalRumMonitor.get().addAttribute("device_hardware", deviceHardware.hwModelSlug)
}
fun Context.isGooglePlayAvailable(): Boolean =
GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(this).let {
it != ConnectionResult.SERVICE_MISSING && it != ConnectionResult.SERVICE_INVALID
}