Skip to content

Commit

Permalink
Merge 9596bb9 into 7823d87
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn committed May 3, 2024
2 parents 7823d87 + 9596bb9 commit 2c855ae
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 45 deletions.
2 changes: 2 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,11 @@ public final class io/sentry/android/core/CurrentActivityIntegration : android/a
public final class io/sentry/android/core/DeviceInfoUtil {
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;)V
public fun collectDeviceInformation (ZZ)Lio/sentry/protocol/Device;
public static fun getBatteryLevel (Landroid/content/Intent;Lio/sentry/SentryOptions;)Ljava/lang/Float;
public static fun getInstance (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;)Lio/sentry/android/core/DeviceInfoUtil;
public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem;
public fun getSideLoadedInfo ()Lio/sentry/android/core/ContextUtils$SideLoadedInfo;
public static fun isCharging (Landroid/content/Intent;Lio/sentry/SentryOptions;)Ljava/lang/Boolean;
public static fun resetInstance ()V
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import android.util.DisplayMetrics;
import io.sentry.DateUtils;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.internal.util.CpuInfoUtils;
import io.sentry.android.core.internal.util.DeviceOrientations;
import io.sentry.android.core.internal.util.RootChecker;
Expand Down Expand Up @@ -184,8 +185,8 @@ public ContextUtils.SideLoadedInfo getSideLoadedInfo() {
private void setDeviceIO(final @NotNull Device device, final boolean includeDynamicData) {
final Intent batteryIntent = getBatteryIntent();
if (batteryIntent != null) {
device.setBatteryLevel(getBatteryLevel(batteryIntent));
device.setCharging(isCharging(batteryIntent));
device.setBatteryLevel(getBatteryLevel(batteryIntent, options));
device.setCharging(isCharging(batteryIntent, options));
device.setBatteryTemperature(getBatteryTemperature(batteryIntent));
}

Expand Down Expand Up @@ -270,7 +271,8 @@ private Intent getBatteryIntent() {
* @return the device's current battery level (as a percentage of total), or null if unknown
*/
@Nullable
private Float getBatteryLevel(final @NotNull Intent batteryIntent) {
public static Float getBatteryLevel(
final @NotNull Intent batteryIntent, final @NotNull SentryOptions options) {
try {
int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
Expand All @@ -294,7 +296,8 @@ private Float getBatteryLevel(final @NotNull Intent batteryIntent) {
* @return whether or not the device is currently plugged in and charging, or null if unknown
*/
@Nullable
private Boolean isCharging(final @NotNull Intent batteryIntent) {
public static Boolean isCharging(
final @NotNull Intent batteryIntent, final @NotNull SentryOptions options) {
try {
int plugged = batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
return plugged == BatteryManager.BATTERY_PLUGGED_AC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE;
import static android.content.Intent.ACTION_AIRPLANE_MODE_CHANGED;
import static android.content.Intent.ACTION_APP_ERROR;
import static android.content.Intent.ACTION_BATTERY_CHANGED;
import static android.content.Intent.ACTION_BATTERY_LOW;
import static android.content.Intent.ACTION_BATTERY_OKAY;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
Expand Down Expand Up @@ -41,10 +42,11 @@
import io.sentry.Breadcrumb;
import io.sentry.Hint;
import io.sentry.IHub;
import io.sentry.ILogger;
import io.sentry.Integration;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
import io.sentry.android.core.internal.util.Debouncer;
import io.sentry.util.Objects;
import io.sentry.util.StringUtils;
import java.io.Closeable;
Expand Down Expand Up @@ -120,7 +122,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio

private void startSystemEventsReceiver(
final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) {
receiver = new SystemEventsBroadcastReceiver(hub, options.getLogger());
receiver = new SystemEventsBroadcastReceiver(hub, options);
final IntentFilter filter = new IntentFilter();
for (String item : actions) {
filter.addAction(item);
Expand Down Expand Up @@ -154,6 +156,7 @@ private void startSystemEventsReceiver(
actions.add(ACTION_AIRPLANE_MODE_CHANGED);
actions.add(ACTION_BATTERY_LOW);
actions.add(ACTION_BATTERY_OKAY);
actions.add(ACTION_BATTERY_CHANGED);
actions.add(ACTION_BOOT_COMPLETED);
actions.add(ACTION_CAMERA_BUTTON);
actions.add(ACTION_CONFIGURATION_CHANGED);
Expand Down Expand Up @@ -204,45 +207,69 @@ public void close() throws IOException {

static final class SystemEventsBroadcastReceiver extends BroadcastReceiver {

private static final long DEBOUNCE_WAIT_TIME_MS = 60 * 1000;
private final @NotNull IHub hub;
private final @NotNull ILogger logger;
private final @NotNull SentryAndroidOptions options;
private final @NotNull Debouncer debouncer =
new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS, 0);

SystemEventsBroadcastReceiver(final @NotNull IHub hub, final @NotNull ILogger logger) {
SystemEventsBroadcastReceiver(
final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) {
this.hub = hub;
this.logger = logger;
this.options = options;
}

@Override
public void onReceive(Context context, Intent intent) {
final boolean shouldDebounce = debouncer.checkForDebounce();
final String action = intent.getAction();
final boolean isBatteryChanged = ACTION_BATTERY_CHANGED.equals(action);
if (isBatteryChanged && shouldDebounce) {
// aligning with iOS which only captures battery status changes every minute at maximum
return;
}

final Breadcrumb breadcrumb = new Breadcrumb();
breadcrumb.setType("system");
breadcrumb.setCategory("device.event");
final String action = intent.getAction();
String shortAction = StringUtils.getStringAfterDot(action);
if (shortAction != null) {
breadcrumb.setData("action", shortAction);
}

final Bundle extras = intent.getExtras();
final Map<String, String> newExtras = new HashMap<>();
if (extras != null && !extras.isEmpty()) {
for (String item : extras.keySet()) {
try {
@SuppressWarnings("deprecation")
Object value = extras.get(item);
if (value != null) {
newExtras.put(item, value.toString());
if (isBatteryChanged) {
final Float batteryLevel = DeviceInfoUtil.getBatteryLevel(intent, options);
if (batteryLevel != null) {
breadcrumb.setData("level", batteryLevel);
}
final Boolean isCharging = DeviceInfoUtil.isCharging(intent, options);
if (isCharging != null) {
breadcrumb.setData("charging", isCharging);
}
} else {
final Bundle extras = intent.getExtras();
final Map<String, String> newExtras = new HashMap<>();
if (extras != null && !extras.isEmpty()) {
for (String item : extras.keySet()) {
try {
@SuppressWarnings("deprecation")
Object value = extras.get(item);
if (value != null) {
newExtras.put(item, value.toString());
}
} catch (Throwable exception) {
options
.getLogger()
.log(
SentryLevel.ERROR,
exception,
"%s key of the %s action threw an error.",
item,
action);
}
} catch (Throwable exception) {
logger.log(
SentryLevel.ERROR,
exception,
"%s key of the %s action threw an error.",
item,
action);
}
breadcrumb.setData("extras", newExtras);
}
breadcrumb.setData("extras", newExtras);
}
breadcrumb.setLevel(SentryLevel.INFO);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@ package io.sentry.android.core

import android.content.Context
import android.content.Intent
import android.os.BatteryManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.Breadcrumb
import io.sentry.IHub
import io.sentry.ISentryExecutorService
import io.sentry.SentryLevel
import io.sentry.test.DeferredExecutorService
import io.sentry.test.ImmediateExecutorService
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.check
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull

@RunWith(AndroidJUnit4::class)
class SystemEventsBreadcrumbsIntegrationTest {

private class Fixture {
Expand Down Expand Up @@ -111,6 +116,61 @@ class SystemEventsBreadcrumbsIntegrationTest {
)
}

@Test
fun `handles battery changes`() {
val sut = fixture.getSut()

sut.register(fixture.hub, fixture.options)
val intent = Intent().apply {
action = Intent.ACTION_BATTERY_CHANGED
putExtra(BatteryManager.EXTRA_LEVEL, 75)
putExtra(BatteryManager.EXTRA_SCALE, 100)
putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB)
}
sut.receiver!!.onReceive(fixture.context, intent)

verify(fixture.hub).addBreadcrumb(
check<Breadcrumb> {
assertEquals("device.event", it.category)
assertEquals("system", it.type)
assertEquals(SentryLevel.INFO, it.level)
assertEquals(it.data["level"], 75f)
assertEquals(it.data["charging"], true)
},
anyOrNull()
)
}

@Test
fun `battery changes are debounced`() {
val sut = fixture.getSut()

sut.register(fixture.hub, fixture.options)
val intent1 = Intent().apply {
action = Intent.ACTION_BATTERY_CHANGED
putExtra(BatteryManager.EXTRA_LEVEL, 80)
putExtra(BatteryManager.EXTRA_SCALE, 100)
}
val intent2 = Intent().apply {
action = Intent.ACTION_BATTERY_CHANGED
putExtra(BatteryManager.EXTRA_LEVEL, 75)
putExtra(BatteryManager.EXTRA_SCALE, 100)
putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB)
}
sut.receiver!!.onReceive(fixture.context, intent1)
sut.receiver!!.onReceive(fixture.context, intent2)

// should only add the first crumb
verify(fixture.hub).addBreadcrumb(
check<Breadcrumb> {
assertEquals(it.data["level"], 80f)
assertEquals(it.data["charging"], false)
},
anyOrNull()
)
verifyNoMoreInteractions(fixture.hub)
}

@Test
fun `Do not crash if registerReceiver throws exception`() {
val sut = fixture.getSut()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.sentry.DateUtils
import io.sentry.Hint
import io.sentry.IHub
import io.sentry.ReplayRecording
import io.sentry.SentryLevel
import io.sentry.SentryOptions
import io.sentry.SentryReplayEvent
import io.sentry.SentryReplayEvent.ReplayType
Expand Down Expand Up @@ -201,12 +202,12 @@ internal abstract class BaseCaptureStrategy(

hub?.configureScope { scope ->
scope.breadcrumbs.forEach { breadcrumb ->
if (breadcrumb.timestamp.after(segmentTimestamp) &&
breadcrumb.timestamp.before(endTimestamp)
if (breadcrumb.timestamp.time >= segmentTimestamp.time &&
breadcrumb.timestamp.time < endTimestamp.time
) {
// TODO: rework this later when aligned with iOS and frontend
var breadcrumbMessage: String? = null
val breadcrumbCategory: String?
var breadcrumbCategory: String? = null
var breadcrumbLevel: SentryLevel? = null
val breadcrumbData = mutableMapOf<String, Any?>()
when {
breadcrumb.category == "http" -> {
Expand All @@ -216,39 +217,68 @@ internal abstract class BaseCaptureStrategy(
return@forEach
}

breadcrumb.category == "device.orientation" -> {
breadcrumb.type == "navigation" &&
breadcrumb.category == "app.lifecycle" -> {
breadcrumbCategory = "app.${breadcrumb.data["state"]}"
}

breadcrumb.type == "navigation" &&
breadcrumb.category == "device.orientation" -> {
breadcrumbCategory = breadcrumb.category!!
breadcrumbMessage = breadcrumb.data["position"] as? String ?: ""
val position = breadcrumb.data["position"]
if (position == "landscape" || position == "portrait") {
breadcrumbData["position"] = position
} else {
return@forEach
}
}

breadcrumb.type == "navigation" -> {
breadcrumbCategory = "navigation"
breadcrumbData["to"] = when {
breadcrumb.data["state"] == "resumed" -> breadcrumb.data["screen"] as? String
breadcrumb.category == "app.lifecycle" -> breadcrumb.data["state"] as? String
breadcrumb.data["state"] == "resumed" -> (breadcrumb.data["screen"] as? String)?.substringAfterLast('.')
"to" in breadcrumb.data -> breadcrumb.data["to"] as? String
else -> return@forEach
} ?: return@forEach
}

breadcrumb.category in setOf("ui.click", "ui.scroll", "ui.swipe") -> {
breadcrumbCategory = breadcrumb.category!!
breadcrumb.category == "ui.click" -> {
breadcrumbCategory = "ui.tap"
breadcrumbMessage = (
breadcrumb.data["view.id"]
?: breadcrumb.data["view.class"]
?: breadcrumb.data["view.tag"]
) as? String ?: ""
?: breadcrumb.data["view.class"]
) as? String ?: return@forEach
breadcrumbData.putAll(breadcrumb.data)
}

breadcrumb.type == "system" -> {
breadcrumbCategory = breadcrumb.type!!
breadcrumbMessage =
breadcrumb.data.entries.joinToString() as? String ?: ""
breadcrumb.type == "system" && breadcrumb.category == "network.event" -> {
breadcrumbCategory = "device.connectivity"
breadcrumbData["state"] = when {
breadcrumb.data["action"] == "NETWORK_LOST" -> "offline"
"network_type" in breadcrumb.data -> if (!(breadcrumb.data["network_type"] as? String).isNullOrEmpty()) {
breadcrumb.data["network_type"]
} else {
return@forEach
}
else -> return@forEach
}
}

breadcrumb.data["action"] == "BATTERY_CHANGED" -> {
breadcrumbCategory = "device.battery"
breadcrumbData.putAll(
breadcrumb.data.filterKeys {
it == "level" || it == "charging"
}
)
}

else -> {
breadcrumbCategory = breadcrumb.category
breadcrumbMessage = breadcrumb.message
breadcrumbLevel = breadcrumb.level
breadcrumbData.putAll(breadcrumb.data)
}
}
if (!breadcrumbCategory.isNullOrEmpty()) {
Expand All @@ -258,6 +288,7 @@ internal abstract class BaseCaptureStrategy(
breadcrumbType = "default"
category = breadcrumbCategory
message = breadcrumbMessage
level = breadcrumbLevel
data = breadcrumbData
}
}
Expand Down

0 comments on commit 2c855ae

Please sign in to comment.