Skip to content

Commit 50443a4

Browse files
authored
[SR] Align breadcrumbs with frontend (#3406)
2 parents 0af0984 + b9b78df commit 50443a4

File tree

11 files changed

+203
-46
lines changed

11 files changed

+203
-46
lines changed

sentry-android-core/api/sentry-android-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,11 @@ public final class io/sentry/android/core/CurrentActivityIntegration : android/a
183183
public final class io/sentry/android/core/DeviceInfoUtil {
184184
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;)V
185185
public fun collectDeviceInformation (ZZ)Lio/sentry/protocol/Device;
186+
public static fun getBatteryLevel (Landroid/content/Intent;Lio/sentry/SentryOptions;)Ljava/lang/Float;
186187
public static fun getInstance (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;)Lio/sentry/android/core/DeviceInfoUtil;
187188
public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem;
188189
public fun getSideLoadedInfo ()Lio/sentry/android/core/ContextUtils$SideLoadedInfo;
190+
public static fun isCharging (Landroid/content/Intent;Lio/sentry/SentryOptions;)Ljava/lang/Boolean;
189191
public static fun resetInstance ()V
190192
}
191193

sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import android.util.DisplayMetrics;
1717
import io.sentry.DateUtils;
1818
import io.sentry.SentryLevel;
19+
import io.sentry.SentryOptions;
1920
import io.sentry.android.core.internal.util.CpuInfoUtils;
2021
import io.sentry.android.core.internal.util.DeviceOrientations;
2122
import io.sentry.android.core.internal.util.RootChecker;
@@ -184,8 +185,8 @@ public ContextUtils.SideLoadedInfo getSideLoadedInfo() {
184185
private void setDeviceIO(final @NotNull Device device, final boolean includeDynamicData) {
185186
final Intent batteryIntent = getBatteryIntent();
186187
if (batteryIntent != null) {
187-
device.setBatteryLevel(getBatteryLevel(batteryIntent));
188-
device.setCharging(isCharging(batteryIntent));
188+
device.setBatteryLevel(getBatteryLevel(batteryIntent, options));
189+
device.setCharging(isCharging(batteryIntent, options));
189190
device.setBatteryTemperature(getBatteryTemperature(batteryIntent));
190191
}
191192

@@ -270,7 +271,8 @@ private Intent getBatteryIntent() {
270271
* @return the device's current battery level (as a percentage of total), or null if unknown
271272
*/
272273
@Nullable
273-
private Float getBatteryLevel(final @NotNull Intent batteryIntent) {
274+
public static Float getBatteryLevel(
275+
final @NotNull Intent batteryIntent, final @NotNull SentryOptions options) {
274276
try {
275277
int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
276278
int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
@@ -294,7 +296,8 @@ private Float getBatteryLevel(final @NotNull Intent batteryIntent) {
294296
* @return whether or not the device is currently plugged in and charging, or null if unknown
295297
*/
296298
@Nullable
297-
private Boolean isCharging(final @NotNull Intent batteryIntent) {
299+
public static Boolean isCharging(
300+
final @NotNull Intent batteryIntent, final @NotNull SentryOptions options) {
298301
try {
299302
int plugged = batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
300303
return plugged == BatteryManager.BATTERY_PLUGGED_AC

sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE;
77
import static android.content.Intent.ACTION_AIRPLANE_MODE_CHANGED;
88
import static android.content.Intent.ACTION_APP_ERROR;
9+
import static android.content.Intent.ACTION_BATTERY_CHANGED;
910
import static android.content.Intent.ACTION_BATTERY_LOW;
1011
import static android.content.Intent.ACTION_BATTERY_OKAY;
1112
import static android.content.Intent.ACTION_BOOT_COMPLETED;
@@ -41,10 +42,11 @@
4142
import io.sentry.Breadcrumb;
4243
import io.sentry.Hint;
4344
import io.sentry.IHub;
44-
import io.sentry.ILogger;
4545
import io.sentry.Integration;
4646
import io.sentry.SentryLevel;
4747
import io.sentry.SentryOptions;
48+
import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
49+
import io.sentry.android.core.internal.util.Debouncer;
4850
import io.sentry.util.Objects;
4951
import io.sentry.util.StringUtils;
5052
import java.io.Closeable;
@@ -120,7 +122,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio
120122

121123
private void startSystemEventsReceiver(
122124
final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) {
123-
receiver = new SystemEventsBroadcastReceiver(hub, options.getLogger());
125+
receiver = new SystemEventsBroadcastReceiver(hub, options);
124126
final IntentFilter filter = new IntentFilter();
125127
for (String item : actions) {
126128
filter.addAction(item);
@@ -154,6 +156,7 @@ private void startSystemEventsReceiver(
154156
actions.add(ACTION_AIRPLANE_MODE_CHANGED);
155157
actions.add(ACTION_BATTERY_LOW);
156158
actions.add(ACTION_BATTERY_OKAY);
159+
actions.add(ACTION_BATTERY_CHANGED);
157160
actions.add(ACTION_BOOT_COMPLETED);
158161
actions.add(ACTION_CAMERA_BUTTON);
159162
actions.add(ACTION_CONFIGURATION_CHANGED);
@@ -204,45 +207,69 @@ public void close() throws IOException {
204207

205208
static final class SystemEventsBroadcastReceiver extends BroadcastReceiver {
206209

210+
private static final long DEBOUNCE_WAIT_TIME_MS = 60 * 1000;
207211
private final @NotNull IHub hub;
208-
private final @NotNull ILogger logger;
212+
private final @NotNull SentryAndroidOptions options;
213+
private final @NotNull Debouncer debouncer =
214+
new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS, 0);
209215

210-
SystemEventsBroadcastReceiver(final @NotNull IHub hub, final @NotNull ILogger logger) {
216+
SystemEventsBroadcastReceiver(
217+
final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) {
211218
this.hub = hub;
212-
this.logger = logger;
219+
this.options = options;
213220
}
214221

215222
@Override
216223
public void onReceive(Context context, Intent intent) {
224+
final boolean shouldDebounce = debouncer.checkForDebounce();
225+
final String action = intent.getAction();
226+
final boolean isBatteryChanged = ACTION_BATTERY_CHANGED.equals(action);
227+
if (isBatteryChanged && shouldDebounce) {
228+
// aligning with iOS which only captures battery status changes every minute at maximum
229+
return;
230+
}
231+
217232
final Breadcrumb breadcrumb = new Breadcrumb();
218233
breadcrumb.setType("system");
219234
breadcrumb.setCategory("device.event");
220-
final String action = intent.getAction();
221235
String shortAction = StringUtils.getStringAfterDot(action);
222236
if (shortAction != null) {
223237
breadcrumb.setData("action", shortAction);
224238
}
225239

226-
final Bundle extras = intent.getExtras();
227-
final Map<String, String> newExtras = new HashMap<>();
228-
if (extras != null && !extras.isEmpty()) {
229-
for (String item : extras.keySet()) {
230-
try {
231-
@SuppressWarnings("deprecation")
232-
Object value = extras.get(item);
233-
if (value != null) {
234-
newExtras.put(item, value.toString());
240+
if (isBatteryChanged) {
241+
final Float batteryLevel = DeviceInfoUtil.getBatteryLevel(intent, options);
242+
if (batteryLevel != null) {
243+
breadcrumb.setData("level", batteryLevel);
244+
}
245+
final Boolean isCharging = DeviceInfoUtil.isCharging(intent, options);
246+
if (isCharging != null) {
247+
breadcrumb.setData("charging", isCharging);
248+
}
249+
} else {
250+
final Bundle extras = intent.getExtras();
251+
final Map<String, String> newExtras = new HashMap<>();
252+
if (extras != null && !extras.isEmpty()) {
253+
for (String item : extras.keySet()) {
254+
try {
255+
@SuppressWarnings("deprecation")
256+
Object value = extras.get(item);
257+
if (value != null) {
258+
newExtras.put(item, value.toString());
259+
}
260+
} catch (Throwable exception) {
261+
options
262+
.getLogger()
263+
.log(
264+
SentryLevel.ERROR,
265+
exception,
266+
"%s key of the %s action threw an error.",
267+
item,
268+
action);
235269
}
236-
} catch (Throwable exception) {
237-
logger.log(
238-
SentryLevel.ERROR,
239-
exception,
240-
"%s key of the %s action threw an error.",
241-
item,
242-
action);
243270
}
271+
breadcrumb.setData("extras", newExtras);
244272
}
245-
breadcrumb.setData("extras", newExtras);
246273
}
247274
breadcrumb.setLevel(SentryLevel.INFO);
248275

sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,30 @@ package io.sentry.android.core
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.os.BatteryManager
6+
import androidx.test.ext.junit.runners.AndroidJUnit4
57
import io.sentry.Breadcrumb
68
import io.sentry.IHub
79
import io.sentry.ISentryExecutorService
810
import io.sentry.SentryLevel
911
import io.sentry.test.DeferredExecutorService
1012
import io.sentry.test.ImmediateExecutorService
13+
import org.junit.runner.RunWith
1114
import org.mockito.kotlin.any
1215
import org.mockito.kotlin.anyOrNull
1316
import org.mockito.kotlin.check
1417
import org.mockito.kotlin.mock
1518
import org.mockito.kotlin.never
1619
import org.mockito.kotlin.verify
20+
import org.mockito.kotlin.verifyNoMoreInteractions
1721
import org.mockito.kotlin.whenever
1822
import kotlin.test.Test
1923
import kotlin.test.assertEquals
2024
import kotlin.test.assertFalse
2125
import kotlin.test.assertNotNull
2226
import kotlin.test.assertNull
2327

28+
@RunWith(AndroidJUnit4::class)
2429
class SystemEventsBreadcrumbsIntegrationTest {
2530

2631
private class Fixture {
@@ -111,6 +116,61 @@ class SystemEventsBreadcrumbsIntegrationTest {
111116
)
112117
}
113118

119+
@Test
120+
fun `handles battery changes`() {
121+
val sut = fixture.getSut()
122+
123+
sut.register(fixture.hub, fixture.options)
124+
val intent = Intent().apply {
125+
action = Intent.ACTION_BATTERY_CHANGED
126+
putExtra(BatteryManager.EXTRA_LEVEL, 75)
127+
putExtra(BatteryManager.EXTRA_SCALE, 100)
128+
putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB)
129+
}
130+
sut.receiver!!.onReceive(fixture.context, intent)
131+
132+
verify(fixture.hub).addBreadcrumb(
133+
check<Breadcrumb> {
134+
assertEquals("device.event", it.category)
135+
assertEquals("system", it.type)
136+
assertEquals(SentryLevel.INFO, it.level)
137+
assertEquals(it.data["level"], 75f)
138+
assertEquals(it.data["charging"], true)
139+
},
140+
anyOrNull()
141+
)
142+
}
143+
144+
@Test
145+
fun `battery changes are debounced`() {
146+
val sut = fixture.getSut()
147+
148+
sut.register(fixture.hub, fixture.options)
149+
val intent1 = Intent().apply {
150+
action = Intent.ACTION_BATTERY_CHANGED
151+
putExtra(BatteryManager.EXTRA_LEVEL, 80)
152+
putExtra(BatteryManager.EXTRA_SCALE, 100)
153+
}
154+
val intent2 = Intent().apply {
155+
action = Intent.ACTION_BATTERY_CHANGED
156+
putExtra(BatteryManager.EXTRA_LEVEL, 75)
157+
putExtra(BatteryManager.EXTRA_SCALE, 100)
158+
putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB)
159+
}
160+
sut.receiver!!.onReceive(fixture.context, intent1)
161+
sut.receiver!!.onReceive(fixture.context, intent2)
162+
163+
// should only add the first crumb
164+
verify(fixture.hub).addBreadcrumb(
165+
check<Breadcrumb> {
166+
assertEquals(it.data["level"], 80f)
167+
assertEquals(it.data["charging"], false)
168+
},
169+
anyOrNull()
170+
)
171+
verifyNoMoreInteractions(fixture.hub)
172+
}
173+
114174
@Test
115175
fun `Do not crash if registerReceiver throws exception`() {
116176
val sut = fixture.getSut()

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.sentry.DateUtils
66
import io.sentry.Hint
77
import io.sentry.IHub
88
import io.sentry.ReplayRecording
9+
import io.sentry.SentryLevel
910
import io.sentry.SentryOptions
1011
import io.sentry.SentryReplayEvent
1112
import io.sentry.SentryReplayEvent.ReplayType
@@ -202,12 +203,12 @@ internal abstract class BaseCaptureStrategy(
202203

203204
hub?.configureScope { scope ->
204205
scope.breadcrumbs.forEach { breadcrumb ->
205-
if (breadcrumb.timestamp.after(segmentTimestamp) &&
206-
breadcrumb.timestamp.before(endTimestamp)
206+
if (breadcrumb.timestamp.time >= segmentTimestamp.time &&
207+
breadcrumb.timestamp.time < endTimestamp.time
207208
) {
208-
// TODO: rework this later when aligned with iOS and frontend
209209
var breadcrumbMessage: String? = null
210-
val breadcrumbCategory: String?
210+
var breadcrumbCategory: String? = null
211+
var breadcrumbLevel: SentryLevel? = null
211212
val breadcrumbData = mutableMapOf<String, Any?>()
212213
when {
213214
breadcrumb.category == "http" -> {
@@ -217,39 +218,68 @@ internal abstract class BaseCaptureStrategy(
217218
return@forEach
218219
}
219220

220-
breadcrumb.category == "device.orientation" -> {
221+
breadcrumb.type == "navigation" &&
222+
breadcrumb.category == "app.lifecycle" -> {
223+
breadcrumbCategory = "app.${breadcrumb.data["state"]}"
224+
}
225+
226+
breadcrumb.type == "navigation" &&
227+
breadcrumb.category == "device.orientation" -> {
221228
breadcrumbCategory = breadcrumb.category!!
222-
breadcrumbMessage = breadcrumb.data["position"] as? String ?: ""
229+
val position = breadcrumb.data["position"]
230+
if (position == "landscape" || position == "portrait") {
231+
breadcrumbData["position"] = position
232+
} else {
233+
return@forEach
234+
}
223235
}
224236

225237
breadcrumb.type == "navigation" -> {
226238
breadcrumbCategory = "navigation"
227239
breadcrumbData["to"] = when {
228-
breadcrumb.data["state"] == "resumed" -> breadcrumb.data["screen"] as? String
229-
breadcrumb.category == "app.lifecycle" -> breadcrumb.data["state"] as? String
240+
breadcrumb.data["state"] == "resumed" -> (breadcrumb.data["screen"] as? String)?.substringAfterLast('.')
230241
"to" in breadcrumb.data -> breadcrumb.data["to"] as? String
231242
else -> return@forEach
232243
} ?: return@forEach
233244
}
234245

235-
breadcrumb.category in setOf("ui.click", "ui.scroll", "ui.swipe") -> {
236-
breadcrumbCategory = breadcrumb.category!!
246+
breadcrumb.category == "ui.click" -> {
247+
breadcrumbCategory = "ui.tap"
237248
breadcrumbMessage = (
238249
breadcrumb.data["view.id"]
239-
?: breadcrumb.data["view.class"]
240250
?: breadcrumb.data["view.tag"]
241-
) as? String ?: ""
251+
?: breadcrumb.data["view.class"]
252+
) as? String ?: return@forEach
253+
breadcrumbData.putAll(breadcrumb.data)
242254
}
243255

244-
breadcrumb.type == "system" -> {
245-
breadcrumbCategory = breadcrumb.type!!
246-
breadcrumbMessage =
247-
breadcrumb.data.entries.joinToString() as? String ?: ""
256+
breadcrumb.type == "system" && breadcrumb.category == "network.event" -> {
257+
breadcrumbCategory = "device.connectivity"
258+
breadcrumbData["state"] = when {
259+
breadcrumb.data["action"] == "NETWORK_LOST" -> "offline"
260+
"network_type" in breadcrumb.data -> if (!(breadcrumb.data["network_type"] as? String).isNullOrEmpty()) {
261+
breadcrumb.data["network_type"]
262+
} else {
263+
return@forEach
264+
}
265+
else -> return@forEach
266+
}
267+
}
268+
269+
breadcrumb.data["action"] == "BATTERY_CHANGED" -> {
270+
breadcrumbCategory = "device.battery"
271+
breadcrumbData.putAll(
272+
breadcrumb.data.filterKeys {
273+
it == "level" || it == "charging"
274+
}
275+
)
248276
}
249277

250278
else -> {
251279
breadcrumbCategory = breadcrumb.category
252280
breadcrumbMessage = breadcrumb.message
281+
breadcrumbLevel = breadcrumb.level
282+
breadcrumbData.putAll(breadcrumb.data)
253283
}
254284
}
255285
if (!breadcrumbCategory.isNullOrEmpty()) {
@@ -259,6 +289,7 @@ internal abstract class BaseCaptureStrategy(
259289
breadcrumbType = "default"
260290
category = breadcrumbCategory
261291
message = breadcrumbMessage
292+
level = breadcrumbLevel
262293
data = breadcrumbData
263294
}
264295
}
@@ -307,7 +338,7 @@ internal abstract class BaseCaptureStrategy(
307338
) {
308339
synchronized(currentEventsLock) {
309340
var event = currentEvents.peek()
310-
while (event != null && event.timestamp <= until) {
341+
while (event != null && event.timestamp < until) {
311342
callback?.invoke(event)
312343
currentEvents.remove()
313344
event = currentEvents.peek()

0 commit comments

Comments
 (0)