From 44992bd270577d7e82e342ddda736e22911df292 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 12 Aug 2026 10:58:59 +0200 Subject: [PATCH] fix(android): Preserve installation ID independently of Data Collection Keep the Android installation ID available for distinct ID, user ID, device ID, and hybrid scope fallbacks regardless of the userInfo setting. Continue applying userInfo only to automatic user details such as IP addresses and remove the now-unused legacy-always resolver variant. Refs #5666 Co-Authored-By: Claude --- .../core/ApplicationExitInfoEventProcessor.java | 5 ++--- .../android/core/DefaultAndroidEventProcessor.java | 6 ++---- .../java/io/sentry/android/core/DeviceInfoUtil.java | 3 +-- .../io/sentry/android/core/InternalSentrySdk.java | 3 +-- .../java/io/sentry/android/core/SentryAndroid.java | 3 +-- .../android/core/AndroidOptionsInitializerTest.kt | 9 +++------ .../core/ApplicationExitInfoEventProcessorTest.kt | 8 ++++---- .../android/core/DefaultAndroidEventProcessorTest.kt | 4 ++-- .../java/io/sentry/android/core/DeviceInfoUtilTest.kt | 6 +++--- .../io/sentry/android/core/InternalSentrySdkTest.kt | 5 ++--- sentry/api/sentry.api | 1 - .../main/java/io/sentry/DataCollectionResolver.java | 4 ---- .../test/java/io/sentry/DataCollectionResolverTest.kt | 11 ----------- 13 files changed, 21 insertions(+), 47 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java index 62b32dd76ce..4f8df86f572 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java @@ -568,7 +568,7 @@ private void mergeUser(final @NotNull SentryBaseEvent event) { } // userId should be set even if event is Cached as the userId is static and won't change anyway. - if (user.getId() == null && options.getDataCollectionResolver().isUserInfoWithLegacyAlways()) { + if (user.getId() == null) { user.setId(getDeviceId()); } if (user.getIpAddress() == null && options.getDataCollectionResolver().isUserInfo()) { @@ -635,8 +635,7 @@ private void setDevice(final @NotNull SentryBaseEvent event) { device.setScreenDpi(displayMetrics.densityDpi); } - if (device.getId() == null - && options.getDataCollectionResolver().isUserInfoWithLegacyAlways()) { + if (device.getId() == null) { device.setId(getDeviceId()); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 520706b352c..84e953ee69e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -175,7 +175,7 @@ private void mergeUser(final @NotNull SentryBaseEvent event) { } // userId should be set even if event is Cached as the userId is static and won't change anyway. - if (user.getId() == null && options.getDataCollectionResolver().isUserInfoWithLegacyAlways()) { + if (user.getId() == null) { user.setId(Installation.id(context)); } if (user.getIpAddress() == null && options.getDataCollectionResolver().isUserInfo()) { @@ -374,9 +374,7 @@ private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { */ public @NotNull User getDefaultUser(final @NotNull Context context) { final @NotNull User user = new User(); - if (options.getDataCollectionResolver().isUserInfoWithLegacyAlways()) { - user.setId(Installation.id(context)); - } + user.setId(Installation.id(context)); return user; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java index 397403ae9ad..d96dc5cb387 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java @@ -109,8 +109,7 @@ public Device collectDeviceInformation( device.setBootTime(getBootTime()); device.setTimezone(getTimeZone()); - if (device.getId() == null - && options.getDataCollectionResolver().isUserInfoWithLegacyAlways()) { + if (device.getId() == null) { device.setId(getDeviceId()); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index 822a65727d0..2779f803a69 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -99,8 +99,7 @@ public static Map serializeScope( user = new User(); scope.setUser(user); } - if (user.getId() == null - && options.getDataCollectionResolver().isUserInfoWithLegacyAlways()) { + if (user.getId() == null) { try { user.setId(Installation.id(context)); } catch (RuntimeException e) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 150569dcecf..a0dca224ccd 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -149,8 +149,7 @@ public static void init( "Error in the 'OptionsConfiguration.configure' callback.", t); } - if (options.getDistinctId() == null - && options.getDataCollectionResolver().isUserInfoWithLegacyAlways()) { + if (options.getDistinctId() == null) { try { options.setDistinctId(Installation.id(context)); } catch (RuntimeException e) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 068b0964bcc..3da83a9080b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -111,10 +111,7 @@ class AndroidOptionsInitializerTest { ) sentryOptions.configureOptions() - if ( - sentryOptions.distinctId == null && - sentryOptions.dataCollectionResolver.isUserInfoWithLegacyAlways - ) { + if (sentryOptions.distinctId == null) { sentryOptions.distinctId = Installation.id(if (useRealContext) context else mockContext) } AndroidOptionsInitializer.initializeIntegrationsAndProcessors( @@ -356,10 +353,10 @@ class AndroidOptionsInitializerTest { } @Test - fun `init should not set generated distinct id when user info is disabled`() { + fun `init should set generated distinct id when user info is disabled`() { fixture.initSut(configureOptions = { dataCollection.setUserInfo(false) }) - assertNull(fixture.sentryOptions.distinctId) + assertNotNull(fixture.sentryOptions.distinctId) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt index 012013cafd5..c57728d9659 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt @@ -228,13 +228,13 @@ class ApplicationExitInfoEventProcessorTest { } @Test - fun `when user info is disabled, does not set device id`() { + fun `when user info is disabled, sets device id`() { fixture.options.dataCollection.setUserInfo(false) val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint()) val processed = processEvent(hint) - assertNull(processed.contexts.device!!.id) + assertNotNull(processed.contexts.device!!.id) } @Test @@ -477,7 +477,7 @@ class ApplicationExitInfoEventProcessorTest { } @Test - fun `when user info is disabled, does not set installation id for missing user id`() { + fun `when user info is disabled, sets installation id for missing user id`() { fixture.options.dataCollection.setUserInfo(false) val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) val original = SentryEvent() @@ -486,7 +486,7 @@ class ApplicationExitInfoEventProcessorTest { val processed = processor.process(original, hint) - assertNull(processed!!.user!!.id) + assertEquals(Installation.deviceId, processed!!.user!!.id) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index eab6ceacc13..460e0bbec1d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -285,7 +285,7 @@ class DefaultAndroidEventProcessorTest { } @Test - fun `when user info is disabled, does not set automatic user data`() { + fun `when user info is disabled, sets installation id but not automatic ip`() { fixture.options.dataCollection.setUserInfo(false) val sut = fixture.getSut(context, isSendDefaultPii = true) val event = SentryEvent().apply { user = User() } @@ -293,7 +293,7 @@ class DefaultAndroidEventProcessorTest { sut.process(event, Hint()) assertNotNull(event.user) { - assertNull(it.id) + assertNotNull(it.id) assertNull(it.ipAddress) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt index 3cd9e079da9..fffdf6257ea 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt @@ -70,7 +70,7 @@ class DeviceInfoUtilTest { assertNotNull(enabledDevice.id) assertNotNull(enabledDevice.storageSize) assertNotNull(enabled.operatingSystem.isRooted) - assertNull(disabledDevice.id) + assertNotNull(disabledDevice.id) assertNull(disabledDevice.storageSize) assertNull(disabled.operatingSystem.isRooted) } @@ -94,12 +94,12 @@ class DeviceInfoUtilTest { } @Test - fun `does not set device id when user info is disabled`() { + fun `sets device id when user info is disabled`() { val options = SentryAndroidOptions().apply { dataCollection.setUserInfo(false) } val deviceInfo = DeviceInfoUtil.getInstance(context, options).collectDeviceInformation(false, false) - assertNull(deviceInfo.id) + assertNotNull(deviceInfo.id) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt index bd689c6a453..137c2d233e3 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt @@ -38,7 +38,6 @@ import java.util.concurrent.atomic.AtomicReference import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -327,14 +326,14 @@ class InternalSentrySdkTest { } @Test - fun `serializeScope does not provide fallback user id when user info is disabled`() { + fun `serializeScope provides fallback user id when user info is disabled`() { val options = SentryAndroidOptions().apply { dataCollection.setUserInfo(false) } val scope = Scope(options) scope.user = null val serializedScope = InternalSentrySdk.serializeScope(context, options, scope) - assertFalse((serializedScope["user"] as Map<*, *>).containsKey("id")) + assertTrue((serializedScope["user"] as Map<*, *>).containsKey("id")) } @Test diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 256a61990ea..3b17e9e2aa1 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -436,7 +436,6 @@ public final class io/sentry/DataCollectionResolver { public fun isOutgoingResponseBody ()Z public fun isOutgoingResponseBodyWithLegacyBodyGate ()Z public fun isUserInfo ()Z - public fun isUserInfoWithLegacyAlways ()Z } public final class io/sentry/DateUtils { diff --git a/sentry/src/main/java/io/sentry/DataCollectionResolver.java b/sentry/src/main/java/io/sentry/DataCollectionResolver.java index fba8149e723..e4234f6f111 100644 --- a/sentry/src/main/java/io/sentry/DataCollectionResolver.java +++ b/sentry/src/main/java/io/sentry/DataCollectionResolver.java @@ -27,10 +27,6 @@ public boolean isUserInfo() { return explicitOrSendDefaultPii(options.getDataCollection().getUserInfo(), true); } - public boolean isUserInfoWithLegacyAlways() { - return explicitOrDefault(options.getDataCollection().getUserInfo(), true, true); - } - public boolean isDatabaseQueryData() { return explicitOrSendDefaultPii(options.getDataCollection().getDatabaseQueryData(), true); } diff --git a/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt b/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt index 2bade51724e..8fb204c9d9f 100644 --- a/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt +++ b/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt @@ -55,17 +55,6 @@ class DataCollectionResolverTest { assertThat(options.dataCollectionResolver.isUserInfo).isTrue() } - @Test - fun `user info legacy always variant preserves collection when namespace is absent`() { - val options = SentryOptions().apply { isSendDefaultPii = false } - - assertThat(options.dataCollectionResolver.isUserInfoWithLegacyAlways).isTrue() - - options.dataCollection.setUserInfo(false) - - assertThat(options.dataCollectionResolver.isUserInfoWithLegacyAlways).isFalse() - } - @Test fun `omitted booleans use data collection defaults once namespace is explicit`() { val options = SentryOptions().apply { isSendDefaultPii = false }