mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
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:
parent
f5478b42c3
commit
ab22a655c4
18 changed files with 348 additions and 194 deletions
|
|
@ -17,73 +17,32 @@
|
|||
|
||||
package com.geeksville.mesh.android
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.util.Exceptions
|
||||
|
||||
/**
|
||||
* Created by kevinh on 12/24/14.
|
||||
*/
|
||||
|
||||
typealias LogPrinter = (Int, String, String) -> Unit
|
||||
import timber.log.Timber
|
||||
|
||||
interface Logging {
|
||||
|
||||
companion object {
|
||||
/** Some vendors strip log messages unless the severity is super high.
|
||||
*
|
||||
* alps == Soyes
|
||||
* HMD Global == mfg of the Nokia 7.2
|
||||
*/
|
||||
private val badVendors = setOf("OnePlus", "alps", "HMD Global", "Sony")
|
||||
private fun tag(): String = this.javaClass.name
|
||||
|
||||
/// if false NO logs will be shown, set this in the application based on BuildConfig.DEBUG
|
||||
var showLogs = true
|
||||
fun info(msg: String) = Timber.tag(tag()).i(msg)
|
||||
|
||||
/** if true, all logs will be printed at error level. Sometimes necessary for buggy ROMs
|
||||
* that filter logcat output below this level.
|
||||
*
|
||||
* Since there are so many bad vendors, we just always lie if we are a release build
|
||||
*/
|
||||
var forceErrorLevel = !BuildConfig.DEBUG || badVendors.contains(Build.MANUFACTURER)
|
||||
fun debug(msg: String) = Timber.tag(tag()).d(msg)
|
||||
|
||||
/// If false debug logs will not be shown (but others might)
|
||||
var showDebug = true
|
||||
fun warn(msg: String) = Timber.tag(tag()).w(msg)
|
||||
|
||||
/**
|
||||
* By default all logs are printed using the standard android Log class. But clients
|
||||
* can change printlog to a different implementation (for logging to files or via
|
||||
* google crashlytics)
|
||||
*/
|
||||
var printlog: LogPrinter = { level, tag, message ->
|
||||
if (showLogs) {
|
||||
if (showDebug || level > Log.DEBUG) {
|
||||
Log.println(if (forceErrorLevel) Log.ERROR else level, tag, message)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Log an error message, note - we call this errormsg rather than error because error() is a stdlib function in
|
||||
* kotlin in the global namespace and we don't want users to accidentally call that.
|
||||
*/
|
||||
fun errormsg(msg: String, ex: Throwable? = null) {
|
||||
if (ex?.message != null) {
|
||||
Timber.tag(tag()).e(ex, msg)
|
||||
} else {
|
||||
Timber.tag(tag()).e(msg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tag(): String = this.javaClass.getName()
|
||||
|
||||
fun info(msg: String) = printlog(Log.INFO, tag(), msg)
|
||||
fun verbose(msg: String) = printlog(Log.VERBOSE, tag(), msg)
|
||||
fun debug(msg: String) = printlog(Log.DEBUG, tag(), msg)
|
||||
fun warn(msg: String) = printlog(Log.WARN, tag(), msg)
|
||||
|
||||
/**
|
||||
* Log an error message, note - we call this errormsg rather than error because error() is
|
||||
* a stdlib function in kotlin in the global namespace and we don't want users to accidentally call that.
|
||||
*/
|
||||
fun errormsg(msg: String, ex: Throwable? = null) {
|
||||
if (ex?.message != null)
|
||||
printlog(Log.ERROR, tag(), "$msg (exception ${ex.message})")
|
||||
else
|
||||
printlog(Log.ERROR, tag(), "$msg")
|
||||
}
|
||||
|
||||
/// Kotlin assertions are disabled on android, so instead we use this assert helper
|
||||
// / Kotlin assertions are disabled on android, so instead we use this assert helper
|
||||
fun logAssert(f: Boolean) {
|
||||
if (!f) {
|
||||
val ex = AssertionError("Assertion failed")
|
||||
|
|
@ -93,8 +52,8 @@ interface Logging {
|
|||
}
|
||||
}
|
||||
|
||||
/// Report an error (including messaging our crash reporter service if allowed
|
||||
// / Report an error (including messaging our crash reporter service if allowed
|
||||
fun reportError(s: String) {
|
||||
Exceptions.report(Exception("logging reportError: $s"), s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import com.geeksville.mesh.database.entity.MyNodeEntity
|
|||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.database.entity.asDeviceVersion
|
||||
import com.geeksville.mesh.repository.api.DeviceHardwareRepository
|
||||
import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.repository.location.LocationRepository
|
||||
|
|
@ -192,6 +193,7 @@ constructor(
|
|||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val packetRepository: PacketRepository,
|
||||
private val quickChatActionRepository: QuickChatActionRepository,
|
||||
private val locationRepository: LocationRepository,
|
||||
|
|
@ -219,8 +221,17 @@ constructor(
|
|||
viewModelScope.launch { _excludedModulesUnlocked.value = true }
|
||||
}
|
||||
|
||||
val firmwareVersion = myNodeInfo.mapNotNull { nodeInfo -> nodeInfo?.firmwareVersion }
|
||||
|
||||
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmwareEdition }
|
||||
|
||||
val deviceHardware: StateFlow<DeviceHardware?> =
|
||||
ourNodeInfo
|
||||
.mapNotNull { nodeInfo ->
|
||||
nodeInfo?.user?.hwModel?.let { deviceHardwareRepository.getDeviceHardwareByModel(it.number) }
|
||||
}
|
||||
.stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = null)
|
||||
|
||||
val clientNotification: StateFlow<MeshProtos.ClientNotification?> = radioConfigRepository.clientNotification
|
||||
|
||||
fun clearClientNotification(notification: MeshProtos.ClientNotification) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ import kotlinx.coroutines.withContext
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeviceHardwareRepository @Inject constructor(
|
||||
class DeviceHardwareRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val apiDataSource: DeviceHardwareRemoteDataSource,
|
||||
private val localDataSource: DeviceHardwareLocalDataSource,
|
||||
private val jsonDataSource: DeviceHardwareJsonDataSource,
|
||||
|
|
@ -45,15 +47,13 @@ class DeviceHardwareRepository @Inject constructor(
|
|||
} else {
|
||||
val cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
if (cachedHardware != null && !isCacheExpired(cachedHardware.lastUpdated)) {
|
||||
debug("Using recent cached device hardware")
|
||||
val externalModel = cachedHardware.asExternalModel()
|
||||
return@withContext externalModel
|
||||
}
|
||||
}
|
||||
try {
|
||||
debug("Fetching device hardware from server")
|
||||
val deviceHardware = apiDataSource.getAllDeviceHardware()
|
||||
?: throw IOException("empty response from server")
|
||||
val deviceHardware =
|
||||
apiDataSource.getAllDeviceHardware() ?: throw IOException("empty response from server")
|
||||
localDataSource.insertAllDeviceHardware(deviceHardware)
|
||||
val cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
val externalModel = cachedHardware?.asExternalModel()
|
||||
|
|
@ -65,7 +65,6 @@ class DeviceHardwareRepository @Inject constructor(
|
|||
debug("Using stale cached device hardware")
|
||||
return@withContext cachedHardware.asExternalModel()
|
||||
}
|
||||
debug("Loading and caching device hardware from local JSON asset")
|
||||
localDataSource.insertAllDeviceHardware(jsonDataSource.loadDeviceHardwareFromJsonAsset())
|
||||
cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
val externalModel = cachedHardware?.asExternalModel()
|
||||
|
|
@ -78,10 +77,7 @@ class DeviceHardwareRepository @Inject constructor(
|
|||
localDataSource.deleteAllDeviceHardware()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cache is expired
|
||||
*/
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean {
|
||||
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
/** Check if the cache is expired */
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean =
|
||||
System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package com.geeksville.mesh.repository.api
|
||||
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.warn
|
||||
import com.geeksville.mesh.database.entity.FirmwareRelease
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseType
|
||||
|
|
@ -28,7 +27,9 @@ import kotlinx.coroutines.flow.flow
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class FirmwareReleaseRepository @Inject constructor(
|
||||
class FirmwareReleaseRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val apiDataSource: FirmwareReleaseRemoteDataSource,
|
||||
private val localDataSource: FirmwareReleaseLocalDataSource,
|
||||
private val jsonDataSource: FirmwareReleaseJsonDataSource,
|
||||
|
|
@ -43,61 +44,50 @@ class FirmwareReleaseRepository @Inject constructor(
|
|||
|
||||
val alphaRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.ALPHA)
|
||||
|
||||
private fun getLatestFirmware(
|
||||
releaseType: FirmwareReleaseType,
|
||||
refresh: Boolean = false
|
||||
): Flow<FirmwareRelease?> = flow {
|
||||
if (refresh) {
|
||||
invalidateCache()
|
||||
} else {
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
if (cachedRelease != null && !isCacheExpired(cachedRelease.lastUpdated)) {
|
||||
debug("Using recent cached firmware release")
|
||||
val externalModel = cachedRelease.asExternalModel()
|
||||
private fun getLatestFirmware(releaseType: FirmwareReleaseType, refresh: Boolean = false): Flow<FirmwareRelease?> =
|
||||
flow {
|
||||
if (refresh) {
|
||||
invalidateCache()
|
||||
} else {
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
if (cachedRelease != null && !isCacheExpired(cachedRelease.lastUpdated)) {
|
||||
val externalModel = cachedRelease.asExternalModel()
|
||||
emit(externalModel)
|
||||
return@flow
|
||||
}
|
||||
}
|
||||
try {
|
||||
val networkFirmwareReleases =
|
||||
apiDataSource.getFirmwareReleases() ?: throw IOException("empty response from server")
|
||||
val releases =
|
||||
when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> networkFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> networkFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(releases, releaseType)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
} catch (e: IOException) {
|
||||
warn("Failed to fetch firmware releases from server: ${e.message}")
|
||||
val jsonFirmwareReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
|
||||
val releases =
|
||||
when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> jsonFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> jsonFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(releases, releaseType)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
return@flow
|
||||
}
|
||||
}
|
||||
try {
|
||||
debug("Fetching firmware releases from server")
|
||||
val networkFirmwareReleases = apiDataSource.getFirmwareReleases()
|
||||
?: throw IOException("empty response from server")
|
||||
val releases = when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> networkFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> networkFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(
|
||||
releases,
|
||||
releaseType
|
||||
)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
} catch (e: IOException) {
|
||||
warn("Failed to fetch firmware releases from server: ${e.message}")
|
||||
val jsonFirmwareReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
|
||||
val releases = when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> jsonFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> jsonFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(
|
||||
releases,
|
||||
releaseType
|
||||
)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun invalidateCache() {
|
||||
localDataSource.deleteAllFirmwareReleases()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cache is expired
|
||||
*/
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean {
|
||||
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
/** Check if the cache is expired */
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean =
|
||||
System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ import com.geeksville.mesh.BuildConfig
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.setAttributes
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.Node
|
||||
|
|
@ -306,6 +307,7 @@ fun MainScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun VersionChecks(viewModel: UIViewModel) {
|
||||
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle()
|
||||
|
|
@ -313,6 +315,10 @@ private fun VersionChecks(viewModel: UIViewModel) {
|
|||
|
||||
val firmwareEdition by viewModel.firmwareEdition.collectAsStateWithLifecycle(null)
|
||||
|
||||
val currentFirmwareVersion by viewModel.firmwareVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
val currentDeviceHardware by viewModel.deviceHardware.collectAsStateWithLifecycle(null)
|
||||
|
||||
val latestStableFirmwareRelease by viewModel.latestStableFirmwareRelease.collectAsState(DeviceVersion("2.6.4"))
|
||||
LaunchedEffect(connectionState, firmwareEdition) {
|
||||
if (connectionState == MeshService.ConnectionState.CONNECTED) {
|
||||
|
|
@ -330,6 +336,15 @@ private fun VersionChecks(viewModel: UIViewModel) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(connectionState, currentFirmwareVersion, currentDeviceHardware) {
|
||||
if (connectionState == MeshService.ConnectionState.CONNECTED) {
|
||||
if (currentDeviceHardware != null && currentFirmwareVersion != null) {
|
||||
setAttributes(currentFirmwareVersion!!, currentDeviceHardware!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the device is running an old app version or firmware version
|
||||
LaunchedEffect(connectionState, myNodeInfo) {
|
||||
if (connectionState == MeshService.ConnectionState.CONNECTED) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue