Skip to content

Commit 4522edb

Browse files
authored
Merge e4596ff into 7418781
2 parents 7418781 + e4596ff commit 4522edb

File tree

3 files changed

+141
-13
lines changed

3 files changed

+141
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Allow multiple UncaughtExceptionHandlerIntegrations to be active at the same time ([#4462](https://github.com/getsentry/sentry-java/pull/4462))
88
- Cache network capabilities and status to reduce IPC calls ([#4560](https://github.com/getsentry/sentry-java/pull/4560))
9+
- Deduplicate battery breadcrumbs ([#4561](https://github.com/getsentry/sentry-java/pull/4561))
910

1011
## 8.17.0
1112

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

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,43 @@ static final class SystemEventsBroadcastReceiver extends BroadcastReceiver {
339339
private final @NotNull Debouncer batteryChangedDebouncer =
340340
new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS, 0);
341341

342+
// Track previous battery state to avoid duplicate breadcrumbs when values haven't changed
343+
private @Nullable BatteryState previousBatteryState;
344+
345+
static final class BatteryState {
346+
private final @Nullable Float level;
347+
private final @Nullable Boolean charging;
348+
349+
BatteryState(final @Nullable Float level, final @Nullable Boolean charging) {
350+
this.level = level;
351+
this.charging = charging;
352+
}
353+
354+
@Override
355+
public boolean equals(final @Nullable Object other) {
356+
if (!(other instanceof BatteryState)) return false;
357+
BatteryState that = (BatteryState) other;
358+
return isSimilarLevel(level, that.level) && Objects.equals(charging, that.charging);
359+
}
360+
361+
@Override
362+
public int hashCode() {
363+
// Use rounded level for hash consistency
364+
Float roundedLevel = level != null ? Math.round(level * 100f) / 100f : null;
365+
return Objects.hash(roundedLevel, charging);
366+
}
367+
368+
private boolean isSimilarLevel(final @Nullable Float level1, final @Nullable Float level2) {
369+
if (level1 == null && level2 == null) return true;
370+
if (level1 == null || level2 == null) return false;
371+
372+
// Round both levels to 2 decimal places and compare
373+
float rounded1 = Math.round(level1 * 100f) / 100f;
374+
float rounded2 = Math.round(level2 * 100f) / 100f;
375+
return rounded1 == rounded2;
376+
}
377+
}
378+
342379
SystemEventsBroadcastReceiver(
343380
final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) {
344381
this.scopes = scopes;
@@ -350,19 +387,34 @@ public void onReceive(final Context context, final @NotNull Intent intent) {
350387
final @Nullable String action = intent.getAction();
351388
final boolean isBatteryChanged = ACTION_BATTERY_CHANGED.equals(action);
352389

353-
// aligning with iOS which only captures battery status changes every minute at maximum
354-
if (isBatteryChanged && batteryChangedDebouncer.checkForDebounce()) {
355-
return;
390+
@Nullable BatteryState batteryState = null;
391+
if (isBatteryChanged) {
392+
if (batteryChangedDebouncer.checkForDebounce()) {
393+
// aligning with iOS which only captures battery status changes every minute at maximum
394+
return;
395+
}
396+
397+
// For battery changes, check if the actual values have changed
398+
final @Nullable Float currentBatteryLevel = DeviceInfoUtil.getBatteryLevel(intent, options);
399+
final @Nullable Boolean currentChargingState = DeviceInfoUtil.isCharging(intent, options);
400+
batteryState = new BatteryState(currentBatteryLevel, currentChargingState);
401+
402+
// Only create breadcrumb if battery state has actually changed
403+
if (batteryState.equals(previousBatteryState)) {
404+
return;
405+
}
406+
407+
previousBatteryState = batteryState;
356408
}
357409

410+
final BatteryState state = batteryState;
358411
final long now = System.currentTimeMillis();
359412
try {
360413
options
361414
.getExecutorService()
362415
.submit(
363416
() -> {
364-
final Breadcrumb breadcrumb =
365-
createBreadcrumb(now, intent, action, isBatteryChanged);
417+
final Breadcrumb breadcrumb = createBreadcrumb(now, intent, action, state);
366418
final Hint hint = new Hint();
367419
hint.set(ANDROID_INTENT, intent);
368420
scopes.addBreadcrumb(breadcrumb, hint);
@@ -411,7 +463,7 @@ String getStringAfterDotFast(final @Nullable String str) {
411463
final long timeMs,
412464
final @NotNull Intent intent,
413465
final @Nullable String action,
414-
boolean isBatteryChanged) {
466+
final @Nullable BatteryState batteryState) {
415467
final Breadcrumb breadcrumb = new Breadcrumb(timeMs);
416468
breadcrumb.setType("system");
417469
breadcrumb.setCategory("device.event");
@@ -420,14 +472,12 @@ String getStringAfterDotFast(final @Nullable String str) {
420472
breadcrumb.setData("action", shortAction);
421473
}
422474

423-
if (isBatteryChanged) {
424-
final Float batteryLevel = DeviceInfoUtil.getBatteryLevel(intent, options);
425-
if (batteryLevel != null) {
426-
breadcrumb.setData("level", batteryLevel);
475+
if (batteryState != null) {
476+
if (batteryState.level != null) {
477+
breadcrumb.setData("level", batteryState.level);
427478
}
428-
final Boolean isCharging = DeviceInfoUtil.isCharging(intent, options);
429-
if (isCharging != null) {
430-
breadcrumb.setData("charging", isCharging);
479+
if (batteryState.charging != null) {
480+
breadcrumb.setData("charging", batteryState.charging);
431481
}
432482
} else {
433483
final Bundle extras = intent.getExtras();

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,83 @@ class SystemEventsBreadcrumbsIntegrationTest {
197197
verifyNoMoreInteractions(fixture.scopes)
198198
}
199199

200+
@Test
201+
fun `battery changes with identical values do not generate breadcrumbs`() {
202+
val sut = fixture.getSut()
203+
sut.register(fixture.scopes, fixture.options)
204+
205+
val intent1 =
206+
Intent().apply {
207+
action = Intent.ACTION_BATTERY_CHANGED
208+
putExtra(BatteryManager.EXTRA_LEVEL, 80)
209+
putExtra(BatteryManager.EXTRA_SCALE, 100)
210+
putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB)
211+
}
212+
val intent2 =
213+
Intent().apply {
214+
action = Intent.ACTION_BATTERY_CHANGED
215+
putExtra(BatteryManager.EXTRA_LEVEL, 80)
216+
putExtra(BatteryManager.EXTRA_SCALE, 100)
217+
putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB)
218+
}
219+
220+
// Receive first battery change
221+
sut.receiver!!.onReceive(fixture.context, intent1)
222+
223+
// Receive second battery change with identical values
224+
sut.receiver!!.onReceive(fixture.context, intent2)
225+
226+
// should only add the first crumb since values are identical
227+
verify(fixture.scopes)
228+
.addBreadcrumb(
229+
check<Breadcrumb> {
230+
assertEquals(it.data["level"], 80f)
231+
assertEquals(it.data["charging"], true)
232+
},
233+
anyOrNull(),
234+
)
235+
verifyNoMoreInteractions(fixture.scopes)
236+
}
237+
238+
@Test
239+
fun `battery changes with minor level differences do not generate breadcrumbs`() {
240+
val sut = fixture.getSut()
241+
sut.register(fixture.scopes, fixture.options)
242+
243+
val intent1 =
244+
Intent().apply {
245+
action = Intent.ACTION_BATTERY_CHANGED
246+
putExtra(BatteryManager.EXTRA_LEVEL, 80001) // 80.001%
247+
putExtra(BatteryManager.EXTRA_SCALE, 100000)
248+
putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB)
249+
}
250+
val intent2 =
251+
Intent().apply {
252+
action = Intent.ACTION_BATTERY_CHANGED
253+
putExtra(BatteryManager.EXTRA_LEVEL, 80002) // 80.002%
254+
putExtra(BatteryManager.EXTRA_SCALE, 100000)
255+
putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_USB)
256+
}
257+
258+
// Receive first battery change
259+
sut.receiver!!.onReceive(fixture.context, intent1)
260+
261+
// Receive second battery change with very minor level difference (rounds to same 3 decimal
262+
// places)
263+
sut.receiver!!.onReceive(fixture.context, intent2)
264+
265+
// should only add the first crumb since both round to 80.000%
266+
verify(fixture.scopes)
267+
.addBreadcrumb(
268+
check<Breadcrumb> {
269+
assertEquals(it.data["level"], 80.001f)
270+
assertEquals(it.data["charging"], true)
271+
},
272+
anyOrNull(),
273+
)
274+
verifyNoMoreInteractions(fixture.scopes)
275+
}
276+
200277
@Test
201278
fun `Do not crash if registerReceiver throws exception`() {
202279
val sut = fixture.getSut()

0 commit comments

Comments
 (0)