Skip to content
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

feat: add fragment lifecycle breadcrumb logger #1522

Merged
merged 18 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from 15 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* Feat: Automatic breadcrumbs logging for fragment lifecycle (#1522)

## 5.0.1

* Fix: Sources and Javadoc artifacts were mixed up (#1515)
Expand Down
3 changes: 3 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ object Config {

val minSdkVersion = 14
val minSdkVersionOkHttp = 21
val minSdkVersionFragment = 21
val minSdkVersionNdk = 16
val targetSdkVersion = sdkVersion
val compileSdkVersion = sdkVersion
Expand Down Expand Up @@ -82,6 +83,8 @@ object Config {
val retrofit2Gson = "$retrofit2Group:converter-gson:$retrofit2Version"

val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"

val fragment = "androidx.fragment:fragment-ktx:1.3.4"
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
}

object AnnotationProcessors {
Expand Down
1 change: 1 addition & 0 deletions sentry-android-fragment/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
38 changes: 38 additions & 0 deletions sentry-android-fragment/api/sentry-android-fragment.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
public final class io/sentry/android/fragment/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public static final field VERSION_NAME Ljava/lang/String;
public fun <init> ()V
}

public final class io/sentry/android/fragment/FragmentLifecycleIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable {
public fun <init> (Landroid/app/Application;)V
public fun close ()V
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityDestroyed (Landroid/app/Activity;)V
public fun onActivityPaused (Landroid/app/Activity;)V
public fun onActivityResumed (Landroid/app/Activity;)V
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityStarted (Landroid/app/Activity;)V
public fun onActivityStopped (Landroid/app/Activity;)V
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/fragment/SentryFragmentLifecycleCallbacks : androidx/fragment/app/FragmentManager$FragmentLifecycleCallbacks {
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;)V
public synthetic fun <init> (Lio/sentry/IHub;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun onFragmentAttached (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/content/Context;)V
public fun onFragmentCreated (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/os/Bundle;)V
public fun onFragmentDestroyed (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V
public fun onFragmentDetached (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V
public fun onFragmentPaused (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V
public fun onFragmentResumed (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V
public fun onFragmentSaveInstanceState (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/os/Bundle;)V
public fun onFragmentStarted (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V
public fun onFragmentStopped (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V
public fun onFragmentViewCreated (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/view/View;Landroid/os/Bundle;)V
public fun onFragmentViewDestroyed (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V
}

83 changes: 83 additions & 0 deletions sentry-android-fragment/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import io.gitlab.arturbosch.detekt.Detekt
import io.gitlab.arturbosch.detekt.extensions.DetektExtension

plugins {
id("com.android.library")
kotlin("android")
jacoco
id(Config.QualityPlugins.gradleVersions)
id(Config.QualityPlugins.detektPlugin)
}

android {
compileSdkVersion(Config.Android.compileSdkVersion)

defaultConfig {
targetSdkVersion(Config.Android.targetSdkVersion)
minSdkVersion(Config.Android.minSdkVersionFragment)

versionName = project.version.toString()
versionCode = project.properties[Config.Sentry.buildVersionCodeProp].toString().toInt()

// for AGP 4.1
buildConfigField("String", "VERSION_NAME", "\"$versionName\"")
}

buildTypes {
getByName("debug")
getByName("release") {
consumerProguardFiles("proguard-rules.pro")
}
}

marandaneto marked this conversation as resolved.
Show resolved Hide resolved
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}

testOptions {
animationsDisabled = true
unitTests.apply {
isReturnDefaultValues = true
isIncludeAndroidResources = true
}
}

lintOptions {
isWarningsAsErrors = true
isCheckDependencies = true

// We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks.
isCheckReleaseBuilds = false
}
}

tasks.withType<Test> {
configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = false
}
}

kotlin {
explicitApi()
}

dependencies {
api(project(":sentry"))

implementation(Config.Libs.fragment)

// tests
testImplementation(Config.TestLibs.kotlinTestJunit)
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.mockitoInline)
}

tasks.withType<Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = JavaVersion.VERSION_1_8.toString()
}

configure<DetektExtension> {
buildUponDefaultConfig = true
allRules = true
}
1 change: 1 addition & 0 deletions sentry-android-fragment/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-keep class io.sentry.android.fragment.** { *; }
2 changes: 2 additions & 0 deletions sentry-android-fragment/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="io.sentry.android.fragment"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.sentry.android.fragment

import android.app.Activity
import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import io.sentry.IHub
import io.sentry.ILogger
import io.sentry.Integration
import io.sentry.SentryLevel.DEBUG
import io.sentry.SentryOptions
import java.io.Closeable

class FragmentLifecycleIntegration(private val application: Application) :
ActivityLifecycleCallbacks,
Integration,
Closeable {

private lateinit var hub: IHub
private lateinit var logger: ILogger

override fun register(hub: IHub, options: SentryOptions) {
this.hub = hub
this.logger = options.logger

application.registerActivityLifecycleCallbacks(this)
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
logger.log(DEBUG, "FragmentLifecycleIntegration installed.")
}

override fun close() {
application.unregisterActivityLifecycleCallbacks(this)
logger.log(DEBUG, "FragmentLifecycleIntegration removed.")
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
(activity as? FragmentActivity)
?.supportFragmentManager
?.registerFragmentLifecycleCallbacks(
SentryFragmentLifecycleCallbacks(hub),
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
true
)
}

override fun onActivityStarted(activity: Activity) {
// no-op
}

override fun onActivityResumed(activity: Activity) {
// no-op
}

override fun onActivityPaused(activity: Activity) {
// no-op
}

override fun onActivityStopped(activity: Activity) {
// no-op
}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
// no-op
}

override fun onActivityDestroyed(activity: Activity) {
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
/**
* It is not needed to unregister [SentryFragmentLifecycleCallbacks] as
* [androidx.fragment.app.FragmentManager] will do this on its own when it's destroyed.
*
* @see [androidx.fragment.app.FragmentManager.registerFragmentLifecycleCallbacks]
*/
// no-op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package io.sentry.android.fragment

import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
import io.sentry.Breadcrumb
import io.sentry.HubAdapter
import io.sentry.IHub
import io.sentry.SentryLevel.INFO

@Suppress("TooManyFunctions")
class SentryFragmentLifecycleCallbacks(
private val hub: IHub = HubAdapter.getInstance()
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
) : FragmentLifecycleCallbacks() {

override fun onFragmentAttached(
fragmentManager: FragmentManager,
fragment: Fragment,
context: Context
) {
addBreadcrumb(fragment, "attached")
}

override fun onFragmentSaveInstanceState(
fragmentManager: FragmentManager,
fragment: Fragment,
outState: Bundle
) {
addBreadcrumb(fragment, "save instance state")
}

override fun onFragmentCreated(
fragmentManager: FragmentManager,
fragment: Fragment,
savedInstanceState: Bundle?
) {
addBreadcrumb(fragment, "created")
}

override fun onFragmentViewCreated(
fragmentManager: FragmentManager,
fragment: Fragment,
view: View,
savedInstanceState: Bundle?
) {
addBreadcrumb(fragment, "view created")
}

override fun onFragmentStarted(fragmentManager: FragmentManager, fragment: Fragment) {
addBreadcrumb(fragment, "started")
}

override fun onFragmentResumed(fragmentManager: FragmentManager, fragment: Fragment) {
addBreadcrumb(fragment, "resumed")
}

override fun onFragmentPaused(fragmentManager: FragmentManager, fragment: Fragment) {
addBreadcrumb(fragment, "paused")
}

override fun onFragmentStopped(fragmentManager: FragmentManager, fragment: Fragment) {
addBreadcrumb(fragment, "stopped")
}

override fun onFragmentViewDestroyed(fragmentManager: FragmentManager, fragment: Fragment) {
addBreadcrumb(fragment, "view destroyed")
}

override fun onFragmentDestroyed(fragmentManager: FragmentManager, fragment: Fragment) {
addBreadcrumb(fragment, "destroyed")
}

override fun onFragmentDetached(fragmentManager: FragmentManager, fragment: Fragment) {
addBreadcrumb(fragment, "detached")
}

private fun addBreadcrumb(fragment: Fragment, state: String) {
val breadcrumb = Breadcrumb().apply {
type = "navigation"
setData("state", state)
setData("screen", getFragmentName(fragment))
category = "ui.fragment.lifecycle"
level = INFO
}
hub.addBreadcrumb(breadcrumb)
}

private fun getFragmentName(fragment: Fragment): String {
return fragment.javaClass.simpleName
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.sentry.android.fragment

import android.app.Activity
import android.app.Application
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.nhaarman.mockitokotlin2.check
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import io.sentry.SentryOptions
import org.junit.Test

class FragmentLifecycleIntegrationTest {

private class Fixture {
val application = mock<Application>()

fun getSut(): FragmentLifecycleIntegration {
return FragmentLifecycleIntegration(application)
}
}

private val fixture = Fixture()

@Test
fun `When register, it should register activity lifecycle callbacks`() {
val sut = fixture.getSut()

sut.register(mock(), SentryOptions())

verify(fixture.application).registerActivityLifecycleCallbacks(sut)
}

@Test
fun `When close, it should unregister lifecycle callbacks`() {
val sut = fixture.getSut()

sut.register(mock(), SentryOptions())
sut.close()

verify(fixture.application).unregisterActivityLifecycleCallbacks(sut)
}

@Test
fun `When FragmentActivity is created, it should register fragment lifecycle callbacks`() {
val sut = fixture.getSut()
val fragmentManager = mock<FragmentManager>()
val fragmentActivity = mock<FragmentActivity> {
on { supportFragmentManager } doReturn fragmentManager
}

sut.register(mock(), SentryOptions())
sut.onActivityCreated(fragmentActivity, savedInstanceState = null)

verify(fragmentManager).registerFragmentLifecycleCallbacks(check { fragmentCallbacks ->
fragmentCallbacks is SentryFragmentLifecycleCallbacks
}, eq(true))
}

@Test
fun `When not a FragmentActivity is created, it should not crash`() {
val sut = fixture.getSut()
val activity = mock<Activity>()

sut.register(mock(), SentryOptions())
sut.onActivityCreated(activity, savedInstanceState = null)
}
}
Loading