Skip to content

Commit

Permalink
feat(android): implement bug reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
abhaysood committed Feb 3, 2025
1 parent 522a593 commit a8385f4
Show file tree
Hide file tree
Showing 75 changed files with 2,927 additions and 204 deletions.
2 changes: 1 addition & 1 deletion android/benchmarks/app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="msr_white">#FFFFFFFF</color>
</resources>
4 changes: 2 additions & 2 deletions android/benchmarks/app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<!-- Base application theme. -->
<style name="Theme.Measure" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorOnPrimary">@color/msr_white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
Expand Down
2 changes: 1 addition & 1 deletion android/measure-android-gradle/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ MEASURE_PLUGIN_GROUP_ID=sh.measure
MEASURE_PLUGIN_ARTIFACT_ID=sh.measure.android.gradle.gradle.plugin
MEASURE_PLUGIN_VERSION_NAME=0.8.0-SNAPSHOT

RELEASE_SIGNING_ENABLED = true
RELEASE_SIGNING_ENABLED = false
14 changes: 14 additions & 0 deletions android/measure/api/measure.api
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,40 @@ public final class sh/measure/android/BuildConfig {
public final class sh/measure/android/Measure {
public static final field $stable I
public static final field INSTANCE Lsh/measure/android/Measure;
public final fun captureLayoutSnapshot (Landroid/app/Activity;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)V
public final fun captureScreenshot (Landroid/app/Activity;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)V
public static final fun clearUserId ()V
public final fun createSpanBuilder (Ljava/lang/String;)Lsh/measure/android/tracing/SpanBuilder;
public final fun getCurrentTime ()J
public final fun getSessionId ()Ljava/lang/String;
public final fun getTraceParentHeaderKey ()Ljava/lang/String;
public final fun getTraceParentHeaderValue (Lsh/measure/android/tracing/Span;)Ljava/lang/String;
public final fun imageUriToAttachment (Landroid/content/Context;Landroid/net/Uri;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)V
public static final fun init (Landroid/content/Context;)V
public static final fun init (Landroid/content/Context;Lsh/measure/android/config/MeasureConfig;)V
public static synthetic fun init$default (Landroid/content/Context;Lsh/measure/android/config/MeasureConfig;ILjava/lang/Object;)V
public final fun launchBugReportActivity (Landroid/app/Activity;Z)V
public static synthetic fun launchBugReportActivity$default (Lsh/measure/android/Measure;Landroid/app/Activity;ZILjava/lang/Object;)V
public static final fun setUserId (Ljava/lang/String;)V
public final fun start ()V
public final fun startSpan (Ljava/lang/String;)Lsh/measure/android/tracing/Span;
public final fun startSpan (Ljava/lang/String;J)Lsh/measure/android/tracing/Span;
public final fun stop ()V
public final fun trackBugReport (Ljava/lang/String;Ljava/util/List;)V
public static synthetic fun trackBugReport$default (Lsh/measure/android/Measure;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)V
public final fun trackEvent (Ljava/lang/String;Ljava/util/Map;Ljava/lang/Long;)V
public static synthetic fun trackEvent$default (Lsh/measure/android/Measure;Ljava/lang/String;Ljava/util/Map;Ljava/lang/Long;ILjava/lang/Object;)V
public static final fun trackHandledException (Ljava/lang/Throwable;)V
public static final fun trackScreenView (Ljava/lang/String;)V
}

public final class sh/measure/android/MsrAttachment {
public static final field $stable I
public final fun getBytes ()[B
public final fun getName ()Ljava/lang/String;
public final fun getType ()Ljava/lang/String;
}

public abstract interface class sh/measure/android/attributes/AttributeValue {
public static final field Companion Lsh/measure/android/attributes/AttributeValue$Companion;
public abstract fun getValue ()Ljava/lang/Object;
Expand Down
2 changes: 1 addition & 1 deletion android/measure/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />

<!--
Permissions required to ensure the tests do not get blocked by
Permissions required to ensure the android tests do not crash
-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,23 @@ class EventsTest {
}
}

@Test
fun tracksBugReportEvent() {
// Given
robot.initializeMeasure(MeasureConfig(enableLogging = true))
ActivityScenario.launch(TestActivity::class.java).use {
// When
it.moveToState(Lifecycle.State.RESUMED)
it.onActivity {
robot.trackBugReport("description", listOf())
}
triggerExport()

// Then
assertEventTracked(EventType.BUG_REPORT)
}
}

private fun String.containsEvent(eventType: String): Boolean {
return contains("\"type\":\"$eventType\"")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,9 @@ class EventsTestRobot {
}.build(),
)
}

fun trackBugReport(description: String, attachments: List<MsrAttachment>) {
Measure.trackBugReport(description, attachments)
device.waitForIdle()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ class FakeConfigProvider : ConfigProvider {
override val maxCheckpointsPerSpan: Int = 100
override val maxInMemorySignalsQueueSize: Int = 30
override val inMemorySignalsQueueFlushRateMs: Long = 3000
override val maxAttachmentsInBugReport: Int = 5
override val maxDescriptionLengthInBugReport: Int = 15
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package sh.measure.android

import sh.measure.android.events.Event
import sh.measure.android.storage.SignalStore
import sh.measure.android.tracing.SpanData

internal class FakeSignalStore : SignalStore {
val trackedEvents = mutableListOf<Event<*>>()
val trackedSpans = mutableListOf<SpanData>()

override fun <T> store(event: Event<T>) {
trackedEvents.add(event)
}

override fun store(spanData: SpanData) {
trackedSpans.add(spanData)
}

override fun flush() {
// No-op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package sh.measure.android

import android.app.Application
import android.view.View
import android.view.ViewGroup
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import org.hamcrest.core.AllOf.allOf
import org.junit.Assert
import sh.measure.android.config.MeasureConfig
import sh.measure.android.events.EventType

internal class MsrBugReportActivityRobot {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val context = instrumentation.context.applicationContext
private val device = UiDevice.getInstance(instrumentation)
private val signalStore = FakeSignalStore()

fun initializeMeasure(config: MeasureConfig = MeasureConfig()): TestMeasureInitializer {
val initializer = TestMeasureInitializer(
application = context as Application,
inputConfig = config,
signalStore = signalStore,
)
Measure.initForInstrumentationTest(initializer)
return initializer
}

fun assertSendCtaEnabled(enabled: Boolean) {
onView(withId(R.id.tv_send)).check(matches(isDisplayed())).check { view, _ ->
Assert.assertEquals(enabled, view.isEnabled)
}
}

fun enterDescription(length: Int = 100) {
onView(withId(R.id.et_description))
.perform(replaceText("a".repeat(length)))
.perform(closeSoftKeyboard())
device.waitForIdle()
}

fun assertBugReportActivityLaunched() {
device.waitForIdle()
onView(withId(R.id.tv_title)).check(matches(isDisplayed()))
}

fun assertBugReportActivityNotVisible() {
device.waitForIdle()
onView(withId(R.id.tv_title)).check(doesNotExist())
}

fun assertTotalScreenshots(value: Int) {
onView(withId(R.id.sl_screenshots_container)).check { view, _ ->
val viewGroup = view as ViewGroup
Assert.assertEquals(value, viewGroup.childCount)
}
}

fun clickCloseButton() {
onView(withId(R.id.btn_close)).perform(click())
}

fun removeScreenshot(index: Int) {
onView(
allOf(
withId(R.id.closeButton),
isDescendantOfA(nthChildOf(withId(R.id.sl_screenshots_container), index)),
),
).perform(click())
}

fun clickAddImagesCTA() {
onView(withId(R.id.tv_choose_image)).perform(click())
}

private fun nthChildOf(parentMatcher: Matcher<View>, childPosition: Int): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("with $childPosition child view of type parentMatcher")
}

override fun matchesSafely(view: View): Boolean {
if (view.parent !is ViewGroup) return false
val parent = view.parent as ViewGroup

return parentMatcher.matches(parent) && parent.getChildAt(childPosition) == view
}
}
}

fun clickSendCTA() {
onView(withId(R.id.tv_send)).perform(click())
}

fun assetBugReportTracked(attachmentCount: Int) {
device.waitForIdle()
val event = signalStore.trackedEvents.find {
it.type == EventType.BUG_REPORT
}
Assert.assertNotNull(event)
Assert.assertEquals(attachmentCount, event?.attachments?.size)
}
}
Loading

0 comments on commit a8385f4

Please sign in to comment.