Skip to content

Commit

Permalink
feat(android): track network info and network change event
Browse files Browse the repository at this point in the history
* Introduce SystemServiceProvider to abstract access to system services.
* Add network_type, network_provider and network_generation in Resource.
* Introduce new event network_change event - tracks vpn, cellular, wifi and no internet.
* Track network_type, network_provider and network_generation for ANRs and Exceptions.
  • Loading branch information
abhaysood committed Nov 17, 2023
1 parent 173a73f commit d170129
Show file tree
Hide file tree
Showing 30 changed files with 1,328 additions and 50 deletions.
2 changes: 1 addition & 1 deletion measure-android/measure/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ dependencies {
testImplementation("androidx.test.ext:junit-ktx:1.1.5")
testImplementation("org.robolectric:robolectric:4.9.2")
testImplementation("androidx.fragment:fragment-testing:1.2.5")
testImplementation("androidx.test:rules:1.5.0")

androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test:rules:1.5.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import sh.measure.android.gestures.ScrollEvent
import sh.measure.android.lifecycle.ActivityLifecycleEvent
import sh.measure.android.lifecycle.ApplicationLifecycleEvent
import sh.measure.android.lifecycle.FragmentLifecycleEvent
import sh.measure.android.network_change.NetworkChangeEvent

@Suppress("MemberVisibilityCanBePrivate")
internal class FakeEventTracker: EventTracker {
Expand All @@ -25,6 +26,7 @@ internal class FakeEventTracker: EventTracker {
val trackedApplicationLifecycleEvents = mutableListOf<ApplicationLifecycleEvent>()
val trackedColdLaunchEvents = mutableListOf<ColdLaunchEvent>()
val trackedWarmLaunchEvents = mutableListOf<WarmLaunchEvent>()
val trackedNetworkChangeEvents = mutableListOf<NetworkChangeEvent>()
val trackedHotLaunchEvents = mutableListOf<HotLaunchEvent>()
val trackedAttachments = mutableListOf<AttachmentInfo>()

Expand Down Expand Up @@ -72,6 +74,10 @@ internal class FakeEventTracker: EventTracker {
trackedHotLaunchEvents.add(event)
}

override fun trackNetworkChange(event: NetworkChangeEvent) {
trackedNetworkChangeEvents.add(event)
}

override fun storeAttachment(attachmentInfo: AttachmentInfo) {
trackedAttachments.add(attachmentInfo)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import sh.measure.android.network.HttpClient
import sh.measure.android.network.HttpClientOkHttp
import sh.measure.android.network.Transport
import sh.measure.android.network.TransportImpl
import sh.measure.android.network_change.NetworkChangesCollector
import sh.measure.android.network_change.NetworkInfoProvider
import sh.measure.android.network_change.NetworkInfoProviderImpl
import sh.measure.android.session.ResourceFactoryImpl
import sh.measure.android.session.SessionController
import sh.measure.android.session.SessionControllerImpl
Expand All @@ -30,6 +33,8 @@ import sh.measure.android.utils.AndroidTimeProvider
import sh.measure.android.utils.CurrentThread
import sh.measure.android.utils.PidProvider
import sh.measure.android.utils.PidProviderImpl
import sh.measure.android.utils.SystemServiceProvider
import sh.measure.android.utils.SystemServiceProviderImpl
import sh.measure.android.utils.UUIDProvider

object Measure {
Expand All @@ -46,9 +51,13 @@ object Measure {
val timeProvider = AndroidTimeProvider()
val idProvider = UUIDProvider()
val config = Config
val resourceFactory = ResourceFactoryImpl(logger, context, config)
val systemServiceProvider: SystemServiceProvider = SystemServiceProviderImpl(context)
val networkInfoProvider: NetworkInfoProvider =
NetworkInfoProviderImpl(context, logger, systemServiceProvider)
val resourceFactory = ResourceFactoryImpl(logger, context, config, networkInfoProvider)
val currentThread = CurrentThread()
val appExitProvider: AppExitProvider = AppExitProviderImpl(context, logger, currentThread)
val appExitProvider: AppExitProvider =
AppExitProviderImpl(logger, currentThread, systemServiceProvider)
val pidProvider: PidProvider = PidProviderImpl()
val sessionReportGenerator = SessionReportGenerator(logger, storage, appExitProvider)
val sessionProvider =
Expand All @@ -67,13 +76,18 @@ object Measure {
).apply { start() }

// Register data collectors
UnhandledExceptionCollector(logger, eventTracker, timeProvider).register()
AnrCollector(logger, context, timeProvider, eventTracker).register()
UnhandledExceptionCollector(logger, eventTracker, timeProvider, networkInfoProvider)
.register()
AnrCollector(logger, systemServiceProvider, networkInfoProvider, timeProvider, eventTracker)
.register()
AppLaunchCollector(
logger, application, timeProvider, coldLaunchTrace, eventTracker,
coldLaunchListener = {
LifecycleCollector(context, eventTracker, timeProvider, currentThread).register()
GestureCollector(logger, eventTracker, timeProvider, currentThread).register()
NetworkChangesCollector(
context, systemServiceProvider, logger, eventTracker, timeProvider, currentThread
).register()
sessionController.syncAllSessions()
},
).register()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,18 @@ package sh.measure.android.anr

import android.app.ActivityManager
import android.app.ActivityManager.ProcessErrorStateInfo
import android.content.Context
import android.os.Debug
import android.os.Handler
import android.os.Looper
import android.os.Process
import sh.measure.android.utils.SystemServiceProvider
import sh.measure.android.utils.TimeProvider

/**
* A watchdog timer thread that detects when the UI thread has frozen.
*/
internal class ANRWatchDog(
private val context: Context,
private val systemServiceProvider: SystemServiceProvider,
private val timeoutInterval: Int,
private val timeProvider: TimeProvider,
private val anrListener: ANRListener,
Expand Down Expand Up @@ -85,8 +85,7 @@ internal class ANRWatchDog(

// Verify ANR state by checking activity manager ProcessErrorStateInfo.
// Don't report an ANR if the process error state is not ANR.
val activityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
val activityManager = systemServiceProvider.activityManager
val pid = Process.myPid()
val processErrorState = captureProcessErrorState(activityManager, pid)
if (processErrorState != null && processErrorState.condition != ProcessErrorStateInfo.NOT_RESPONDING) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
package sh.measure.android.anr

import android.content.Context
import sh.measure.android.events.EventTracker
import sh.measure.android.exceptions.ExceptionFactory
import sh.measure.android.exceptions.MeasureException
import sh.measure.android.logger.LogLevel
import sh.measure.android.logger.Logger
import sh.measure.android.network_change.NetworkInfoProvider
import sh.measure.android.utils.SystemServiceProvider
import sh.measure.android.utils.TimeProvider

private const val ANR_TIMEOUT_MILLIS = 5000

internal class AnrCollector(
private val logger: Logger,
private val context: Context,
private val systemServiceProvider: SystemServiceProvider,
private val networkInfoProvider: NetworkInfoProvider,
private val timeProvider: TimeProvider,
private val tracker: EventTracker
) : ANRWatchDog.ANRListener {
fun register() {
ANRWatchDog(
context = context,
systemServiceProvider = systemServiceProvider,
timeoutInterval = ANR_TIMEOUT_MILLIS,
timeProvider = timeProvider,
anrListener = this
Expand All @@ -31,11 +33,15 @@ internal class AnrCollector(
}

private fun toMeasureException(anr: AnrError): MeasureException {
val networkType = networkInfoProvider.getNetworkType()
return ExceptionFactory.createMeasureException(
throwable = anr,
handled = false,
timestamp = anr.timestamp,
thread = anr.thread,
networkType = networkType,
networkGeneration = networkInfoProvider.getNetworkGeneration(networkType),
networkProvider = networkInfoProvider.getNetworkProvider(networkType),
isAnr = true
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package sh.measure.android.appexit

import android.app.ActivityManager
import android.app.ApplicationExitInfo
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import okio.Buffer
Expand All @@ -12,6 +11,7 @@ import okio.source
import sh.measure.android.logger.LogLevel
import sh.measure.android.logger.Logger
import sh.measure.android.utils.CurrentThread
import sh.measure.android.utils.SystemServiceProvider
import sh.measure.android.utils.iso8601Timestamp
import java.io.InputStream

Expand All @@ -20,23 +20,19 @@ internal interface AppExitProvider {
}

internal class AppExitProviderImpl(
private val context: Context, private val logger: Logger, val currentThread: CurrentThread
private val logger: Logger,
private val currentThread: CurrentThread,
private val systemServiceProvider: SystemServiceProvider
) : AppExitProvider {

override fun get(pid: Int): AppExit? {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
return null
}
return try {
val activityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val historicalExitReason =
activityManager.getHistoricalProcessExitReasons(null, pid, 1).firstOrNull()
historicalExitReason?.toAppExit(currentThread.name)
} catch (e: Exception) {
logger.log(LogLevel.Error, "Failed to get exit info for pid: $pid", e)
null
}
return systemServiceProvider.activityManager?.runCatching {
getHistoricalProcessExitReasons(null, pid, 1).firstOrNull()
?.toAppExit(currentThread.name)
}?.getOrNull()
}

@RequiresApi(Build.VERSION_CODES.R)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import sh.measure.android.app_launch.ColdLaunchEvent
import sh.measure.android.app_launch.HotLaunchEvent
import sh.measure.android.app_launch.WarmLaunchEvent
import sh.measure.android.appexit.AppExit
import sh.measure.android.network_change.NetworkChangeEvent
import sh.measure.android.exceptions.MeasureException
import sh.measure.android.gestures.ClickEvent
import sh.measure.android.gestures.LongClickEvent
Expand Down Expand Up @@ -139,4 +140,12 @@ internal fun HotLaunchEvent.toEvent(): Event {
data = Json.encodeToJsonElement(HotLaunchEvent.serializer(), this),
thread_name = thread_name
)
}
internal fun NetworkChangeEvent.toEvent() : Event {
return Event(
type = EventType.NETWORK_CHANGE,
timestamp = timestamp.iso8601Timestamp(),
data = Json.encodeToJsonElement(NetworkChangeEvent.serializer(), this),
thread_name = thread_name
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import sh.measure.android.lifecycle.ApplicationLifecycleEvent
import sh.measure.android.lifecycle.FragmentLifecycleEvent
import sh.measure.android.logger.LogLevel
import sh.measure.android.logger.Logger
import sh.measure.android.network_change.NetworkChangeEvent
import sh.measure.android.session.SessionController

internal interface EventTracker {
Expand All @@ -27,6 +28,7 @@ internal interface EventTracker {
fun trackColdLaunch(event: ColdLaunchEvent)
fun trackWarmLaunchEvent(event: WarmLaunchEvent)
fun trackHotLaunchEvent(event: HotLaunchEvent)
fun trackNetworkChange(event: NetworkChangeEvent)
fun storeAttachment(attachmentInfo: AttachmentInfo)
}

Expand Down Expand Up @@ -98,6 +100,11 @@ internal class MeasureEventTracker(
sessionController.storeEvent(event.toEvent())
}

override fun trackNetworkChange(event: NetworkChangeEvent) {
logger.log(LogLevel.Error, "Tracking network change ${event.network_type}")
sessionController.storeEvent(event.toEvent())
}

override fun storeAttachment(attachmentInfo: AttachmentInfo) {
logger.log(LogLevel.Debug, "Storing attachment ${attachmentInfo.name}")
sessionController.storeAttachment(attachmentInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ object EventType {
const val COLD_LAUNCH: String = "cold_launch"
const val WARM_LAUNCH: String = "warm_launch"
const val HOT_LAUNCH: String = "hot_launch"
const val NETWORK_CHANGE: String = "network_change"
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ internal object ExceptionFactory {
handled: Boolean,
timestamp: Long,
thread: Thread,
isAnr: Boolean = false
networkType: String?,
networkGeneration: String?,
networkProvider: String?,
isAnr: Boolean = false,
): MeasureException {
val exceptions = mutableListOf<ExceptionUnit>()
var error: Throwable? = throwable
Expand Down Expand Up @@ -42,17 +45,25 @@ internal object ExceptionFactory {
val measureThread = MeasureThread(
name = t.name,
frames = stackTrace.trimStackTrace().map { stackTraceElement ->
Frame(
class_name = stackTraceElement.className,
method_name = stackTraceElement.methodName,
file_name = stackTraceElement.fileName,
line_num = stackTraceElement.lineNumber,
)
})
Frame(
class_name = stackTraceElement.className,
method_name = stackTraceElement.methodName,
file_name = stackTraceElement.fileName,
line_num = stackTraceElement.lineNumber,
)
})
threads.add(measureThread)
}
count++
}
return MeasureException(timestamp, thread.name, exceptions, threads, handled, isAnr)
return MeasureException(
timestamp, thread.name, exceptions,
threads = threads,
handled = handled,
network_type = networkType,
network_provider = networkProvider,
network_generation = networkGeneration,
isAnr = isAnr,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package sh.measure.android.exceptions

import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import sh.measure.android.network_change.NetworkType
import sh.measure.android.network_change.NetworkGeneration

/**
* Represents an exception in Measure. This is used to track handled and unhandled exceptions.
Expand Down Expand Up @@ -34,6 +36,23 @@ internal data class MeasureException(
*/
val handled: Boolean,

/**
* The [NetworkType] that was active when the exception occurred.
*/
val network_type: String?,

/**
* The network provider that was active when the exception occurred. Only set for cellular
* networks.
*/
val network_provider: String?,

/**
* The [NetworkGeneration] that was active when the exception occurred. Only set for cellular
* networks.
*/
val network_generation: String?,

/**
* Whether is exception represents an ANR or not.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sh.measure.android.exceptions
import sh.measure.android.events.EventTracker
import sh.measure.android.logger.LogLevel
import sh.measure.android.logger.Logger
import sh.measure.android.network_change.NetworkInfoProvider
import sh.measure.android.utils.TimeProvider
import java.lang.Thread.UncaughtExceptionHandler

Expand All @@ -19,6 +20,7 @@ internal class UnhandledExceptionCollector(
private val logger: Logger,
private val eventTracker: EventTracker,
private val timeProvider: TimeProvider,
private val networkInfoProvider: NetworkInfoProvider
) : UncaughtExceptionHandler {

private val originalHandler: UncaughtExceptionHandler? =
Expand All @@ -35,11 +37,15 @@ internal class UnhandledExceptionCollector(
override fun uncaughtException(thread: Thread, throwable: Throwable) {
logger.log(LogLevel.Debug, "Unhandled exception received")
try {
val networkType = networkInfoProvider.getNetworkType()
val measureException = ExceptionFactory.createMeasureException(
throwable,
handled = false,
timestamp = timeProvider.currentTimeSinceEpochInMillis,
thread = thread,
networkType = networkType,
networkGeneration = networkInfoProvider.getNetworkGeneration(networkType),
networkProvider = networkInfoProvider.getNetworkProvider(networkType)
)
eventTracker.trackUnhandledException(measureException)
} catch (e: Throwable) {
Expand Down
Loading

0 comments on commit d170129

Please sign in to comment.