Skip to content

Commit 4b95c7e

Browse files
authored
Merge pull request #6038 from getsentry/feat/data-collection-android-manifest
feat(android): [Data Collection 22] Add manifest configuration
2 parents 5e3fa6c + 0b2763d commit 4b95c7e

4 files changed

Lines changed: 313 additions & 2 deletions

File tree

‎sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java‎

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import android.content.Context;
44
import android.content.pm.ApplicationInfo;
55
import android.os.Bundle;
6+
import io.sentry.DataCollection;
7+
import io.sentry.HttpBodyType;
68
import io.sentry.ILogger;
79
import io.sentry.InitPriority;
10+
import io.sentry.KeyValueCollectionBehavior;
811
import io.sentry.ProfileLifecycle;
912
import io.sentry.ScreenshotStrategyType;
1013
import io.sentry.SentryFeedbackOptions;
@@ -16,9 +19,11 @@
1619
import java.util.ArrayList;
1720
import java.util.Arrays;
1821
import java.util.Collections;
22+
import java.util.EnumSet;
1923
import java.util.List;
2024
import java.util.Locale;
2125
import java.util.Map;
26+
import java.util.Set;
2227
import org.jetbrains.annotations.ApiStatus;
2328
import org.jetbrains.annotations.NotNull;
2429
import org.jetbrains.annotations.Nullable;
@@ -106,6 +111,22 @@ final class ManifestMetadataReader {
106111

107112
static final String SEND_DEFAULT_PII = "io.sentry.send-default-pii";
108113

114+
static final String DATA_COLLECTION_USER_INFO = "io.sentry.data-collection.user-info";
115+
static final String DATA_COLLECTION_HTTP_BODIES = "io.sentry.data-collection.http-bodies";
116+
static final String DATA_COLLECTION_COOKIES = "io.sentry.data-collection.cookies";
117+
static final String DATA_COLLECTION_HTTP_REQUEST_HEADERS =
118+
"io.sentry.data-collection.http-headers.request";
119+
static final String DATA_COLLECTION_HTTP_RESPONSE_HEADERS =
120+
"io.sentry.data-collection.http-headers.response";
121+
static final String DATA_COLLECTION_URL_QUERY_PARAMS =
122+
"io.sentry.data-collection.url-query-params";
123+
static final String DATA_COLLECTION_GRAPHQL_DOCUMENT =
124+
"io.sentry.data-collection.graphql.document";
125+
static final String DATA_COLLECTION_GRAPHQL_VARIABLES =
126+
"io.sentry.data-collection.graphql.variables";
127+
static final String DATA_COLLECTION_DATABASE_QUERY_DATA =
128+
"io.sentry.data-collection.database-query-data";
129+
109130
static final String PERFORM_FRAMES_TRACKING = "io.sentry.traces.frames-tracking";
110131

111132
static final String SENTRY_GRADLE_PLUGIN_INTEGRATIONS = "io.sentry.gradle-plugin-integrations";
@@ -779,6 +800,12 @@ static void applyMetadata(
779800
options.setEnableAnrFingerprinting(
780801
readBool(
781802
metadata, logger, ENABLE_ANR_FINGERPRINTING, options.isEnableAnrFingerprinting()));
803+
804+
final @Nullable DataCollection dataCollection =
805+
readDataCollection(metadata, logger, options.getDataCollection());
806+
if (dataCollection != null) {
807+
mergeDataCollection(options.getDataCollection(), dataCollection);
808+
}
782809
}
783810
options
784811
.getLogger()
@@ -791,6 +818,152 @@ static void applyMetadata(
791818
}
792819
}
793820

821+
private static @Nullable DataCollection readDataCollection(
822+
final @NotNull Object metadata,
823+
final @NotNull ILogger logger,
824+
final @NotNull DataCollection currentDataCollection) {
825+
final @NotNull DataCollection dataCollection = new DataCollection(false);
826+
827+
if (containsKey(metadata, DATA_COLLECTION_USER_INFO)) {
828+
dataCollection.setUserInfo(readBool(metadata, logger, DATA_COLLECTION_USER_INFO, false));
829+
}
830+
831+
if (containsKey(metadata, DATA_COLLECTION_HTTP_BODIES)) {
832+
dataCollection.setHttpBodies(readHttpBodyTypes(metadata, logger));
833+
}
834+
835+
final @Nullable KeyValueCollectionBehavior cookies =
836+
readKeyValueCollectionBehavior(
837+
metadata, logger, DATA_COLLECTION_COOKIES, currentDataCollection.getCookies());
838+
if (cookies != null) {
839+
dataCollection.setCookies(cookies);
840+
}
841+
842+
final @Nullable KeyValueCollectionBehavior requestHeaders =
843+
readKeyValueCollectionBehavior(
844+
metadata,
845+
logger,
846+
DATA_COLLECTION_HTTP_REQUEST_HEADERS,
847+
currentDataCollection.getHttpHeaders().getRequest());
848+
if (requestHeaders != null) {
849+
dataCollection.getHttpHeaders().setRequest(requestHeaders);
850+
}
851+
852+
final @Nullable KeyValueCollectionBehavior responseHeaders =
853+
readKeyValueCollectionBehavior(
854+
metadata,
855+
logger,
856+
DATA_COLLECTION_HTTP_RESPONSE_HEADERS,
857+
currentDataCollection.getHttpHeaders().getResponse());
858+
if (responseHeaders != null) {
859+
dataCollection.getHttpHeaders().setResponse(responseHeaders);
860+
}
861+
862+
final @Nullable KeyValueCollectionBehavior urlQueryParams =
863+
readKeyValueCollectionBehavior(
864+
metadata,
865+
logger,
866+
DATA_COLLECTION_URL_QUERY_PARAMS,
867+
currentDataCollection.getUrlQueryParams());
868+
if (urlQueryParams != null) {
869+
dataCollection.setUrlQueryParams(urlQueryParams);
870+
}
871+
872+
if (containsKey(metadata, DATA_COLLECTION_GRAPHQL_DOCUMENT)) {
873+
dataCollection
874+
.getGraphql()
875+
.setDocument(readBool(metadata, logger, DATA_COLLECTION_GRAPHQL_DOCUMENT, false));
876+
}
877+
878+
if (containsKey(metadata, DATA_COLLECTION_GRAPHQL_VARIABLES)) {
879+
dataCollection
880+
.getGraphql()
881+
.setVariables(readBool(metadata, logger, DATA_COLLECTION_GRAPHQL_VARIABLES, false));
882+
}
883+
884+
if (containsKey(metadata, DATA_COLLECTION_DATABASE_QUERY_DATA)) {
885+
dataCollection.setDatabaseQueryData(
886+
readBool(metadata, logger, DATA_COLLECTION_DATABASE_QUERY_DATA, false));
887+
}
888+
889+
return dataCollection.isExplicitlyConfigured() ? dataCollection : null;
890+
}
891+
892+
private static void mergeDataCollection(
893+
final @NotNull DataCollection target, final @NotNull DataCollection source) {
894+
if (source.getUserInfo() != null) {
895+
target.setUserInfo(source.getUserInfo());
896+
}
897+
if (source.getHttpBodies() != null) {
898+
target.setHttpBodies(source.getHttpBodies());
899+
}
900+
if (source.getCookies() != null) {
901+
target.setCookies(source.getCookies());
902+
}
903+
if (source.getHttpHeaders().getRequest() != null) {
904+
target.getHttpHeaders().setRequest(source.getHttpHeaders().getRequest());
905+
}
906+
if (source.getHttpHeaders().getResponse() != null) {
907+
target.getHttpHeaders().setResponse(source.getHttpHeaders().getResponse());
908+
}
909+
if (source.getUrlQueryParams() != null) {
910+
target.setUrlQueryParams(source.getUrlQueryParams());
911+
}
912+
if (source.getGraphql().getDocument() != null) {
913+
target.getGraphql().setDocument(source.getGraphql().getDocument());
914+
}
915+
if (source.getGraphql().getVariables() != null) {
916+
target.getGraphql().setVariables(source.getGraphql().getVariables());
917+
}
918+
if (source.getDatabaseQueryData() != null) {
919+
target.setDatabaseQueryData(source.getDatabaseQueryData());
920+
}
921+
}
922+
923+
private static @NotNull Set<HttpBodyType> readHttpBodyTypes(
924+
final @NotNull Object metadata, final @NotNull ILogger logger) {
925+
final @Nullable List<String> bodyTypes =
926+
readList(metadata, logger, DATA_COLLECTION_HTTP_BODIES);
927+
if (bodyTypes == null || (bodyTypes.size() == 1 && bodyTypes.get(0).isEmpty())) {
928+
return Collections.emptySet();
929+
}
930+
931+
final @NotNull Set<HttpBodyType> result = EnumSet.noneOf(HttpBodyType.class);
932+
for (final String bodyType : bodyTypes) {
933+
result.add(HttpBodyType.valueOf(bodyType.toUpperCase(Locale.ROOT)));
934+
}
935+
return result;
936+
}
937+
938+
private static @Nullable KeyValueCollectionBehavior readKeyValueCollectionBehavior(
939+
final @NotNull Object metadata,
940+
final @NotNull ILogger logger,
941+
final @NotNull String key,
942+
final @Nullable KeyValueCollectionBehavior currentBehavior) {
943+
final @NotNull String modeKey = key + ".mode";
944+
final @NotNull String termsKey = key + ".terms";
945+
if (!containsKey(metadata, modeKey) && !containsKey(metadata, termsKey)) {
946+
return null;
947+
}
948+
949+
final @NotNull KeyValueCollectionBehavior behavior = new KeyValueCollectionBehavior();
950+
if (currentBehavior != null) {
951+
behavior.setMode(currentBehavior.getMode());
952+
behavior.setTerms(currentBehavior.getTerms());
953+
}
954+
if (containsKey(metadata, modeKey)) {
955+
final @Nullable String mode = readString(metadata, logger, modeKey, null);
956+
if (mode != null) {
957+
behavior.setMode(KeyValueCollectionBehavior.Mode.valueOf(mode.toUpperCase(Locale.ROOT)));
958+
}
959+
}
960+
if (containsKey(metadata, termsKey)) {
961+
final @Nullable List<String> terms = readList(metadata, logger, termsKey);
962+
behavior.setTerms(terms == null ? Collections.<String>emptyList() : terms);
963+
}
964+
return behavior;
965+
}
966+
794967
private static boolean readBool(
795968
final @NotNull Object metadata,
796969
final @NotNull ILogger logger,

‎sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import androidx.core.os.bundleOf
66
import androidx.test.ext.junit.runners.AndroidJUnit4
77
import com.google.common.truth.Truth.assertThat
88
import io.sentry.FilterString
9+
import io.sentry.HttpBodyType
910
import io.sentry.ILogger
11+
import io.sentry.KeyValueCollectionBehavior
1012
import io.sentry.ProfileLifecycle
1113
import io.sentry.SentryLevel
1214
import io.sentry.SentryReplayOptions
@@ -1458,6 +1460,139 @@ class ManifestMetadataReaderTest {
14581460
assertTrue(fixture.options.isSendDefaultPii)
14591461
}
14601462

1463+
@Test
1464+
fun `applyMetadata preserves legacy data collection when metadata is absent`() {
1465+
val context = fixture.getContext()
1466+
1467+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1468+
1469+
assertThat(fixture.options.dataCollectionResolver.isDataCollectionConfigured()).isFalse()
1470+
}
1471+
1472+
@Test
1473+
fun `applyMetadata reads data collection options`() {
1474+
val bundle =
1475+
bundleOf(
1476+
ManifestMetadataReader.DATA_COLLECTION_USER_INFO to false,
1477+
ManifestMetadataReader.DATA_COLLECTION_HTTP_BODIES to "incoming_request,outgoing_response",
1478+
ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".mode" to "deny_list",
1479+
ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".terms" to "authorization,session",
1480+
ManifestMetadataReader.DATA_COLLECTION_HTTP_REQUEST_HEADERS + ".mode" to "allow_list",
1481+
ManifestMetadataReader.DATA_COLLECTION_HTTP_REQUEST_HEADERS + ".terms" to
1482+
"x-request-id,content-type",
1483+
ManifestMetadataReader.DATA_COLLECTION_HTTP_RESPONSE_HEADERS + ".mode" to "off",
1484+
ManifestMetadataReader.DATA_COLLECTION_URL_QUERY_PARAMS + ".terms" to "search",
1485+
ManifestMetadataReader.DATA_COLLECTION_GRAPHQL_DOCUMENT to false,
1486+
ManifestMetadataReader.DATA_COLLECTION_GRAPHQL_VARIABLES to true,
1487+
ManifestMetadataReader.DATA_COLLECTION_DATABASE_QUERY_DATA to false,
1488+
)
1489+
val context = fixture.getContext(metaData = bundle)
1490+
1491+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1492+
1493+
val dataCollection = fixture.options.dataCollection
1494+
assertThat(dataCollection.userInfo).isFalse()
1495+
assertThat(dataCollection.httpBodies)
1496+
.containsExactly(HttpBodyType.INCOMING_REQUEST, HttpBodyType.OUTGOING_RESPONSE)
1497+
assertThat(dataCollection.cookies)
1498+
.isEqualTo(KeyValueCollectionBehavior.denyList("authorization", "session"))
1499+
assertThat(dataCollection.httpHeaders.request)
1500+
.isEqualTo(KeyValueCollectionBehavior.allowList("x-request-id", "content-type"))
1501+
assertThat(dataCollection.httpHeaders.response).isEqualTo(KeyValueCollectionBehavior.off())
1502+
assertThat(dataCollection.urlQueryParams)
1503+
.isEqualTo(KeyValueCollectionBehavior.denyList("search"))
1504+
assertThat(dataCollection.graphql.document).isFalse()
1505+
assertThat(dataCollection.graphql.variables).isTrue()
1506+
assertThat(dataCollection.databaseQueryData).isFalse()
1507+
}
1508+
1509+
@Test
1510+
fun `applyMetadata only overrides explicitly configured data collection options`() {
1511+
val dataCollection =
1512+
fixture.options.dataCollection.apply {
1513+
setUserInfo(true)
1514+
setHttpBodies(setOf(HttpBodyType.OUTGOING_REQUEST))
1515+
cookies = KeyValueCollectionBehavior.allowList("existing-cookie")
1516+
httpHeaders.request = KeyValueCollectionBehavior.denyList("existing-request-header")
1517+
httpHeaders.response = KeyValueCollectionBehavior.allowList("existing-response-header")
1518+
urlQueryParams = KeyValueCollectionBehavior.off()
1519+
graphql.setDocument(true)
1520+
graphql.setVariables(false)
1521+
setDatabaseQueryData(true)
1522+
}
1523+
val bundle =
1524+
bundleOf(
1525+
ManifestMetadataReader.DATA_COLLECTION_USER_INFO to false,
1526+
ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".terms" to "manifest-cookie",
1527+
ManifestMetadataReader.DATA_COLLECTION_HTTP_REQUEST_HEADERS + ".mode" to "allow_list",
1528+
)
1529+
val context = fixture.getContext(metaData = bundle)
1530+
1531+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1532+
1533+
assertThat(fixture.options.dataCollection).isSameInstanceAs(dataCollection)
1534+
assertThat(dataCollection.userInfo).isFalse()
1535+
assertThat(dataCollection.httpBodies).containsExactly(HttpBodyType.OUTGOING_REQUEST)
1536+
assertThat(dataCollection.cookies)
1537+
.isEqualTo(KeyValueCollectionBehavior.allowList("manifest-cookie"))
1538+
assertThat(dataCollection.httpHeaders.request)
1539+
.isEqualTo(KeyValueCollectionBehavior.allowList("existing-request-header"))
1540+
assertThat(dataCollection.httpHeaders.response)
1541+
.isEqualTo(KeyValueCollectionBehavior.allowList("existing-response-header"))
1542+
assertThat(dataCollection.urlQueryParams).isEqualTo(KeyValueCollectionBehavior.off())
1543+
assertThat(dataCollection.graphql.document).isTrue()
1544+
assertThat(dataCollection.graphql.variables).isFalse()
1545+
assertThat(dataCollection.databaseQueryData).isTrue()
1546+
}
1547+
1548+
@Test
1549+
fun `applyMetadata reads empty HTTP bodies as disabled`() {
1550+
val bundle = bundleOf(ManifestMetadataReader.DATA_COLLECTION_HTTP_BODIES to "")
1551+
val context = fixture.getContext(metaData = bundle)
1552+
1553+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1554+
1555+
assertThat(fixture.options.dataCollection.httpBodies).isEmpty()
1556+
}
1557+
1558+
@Test
1559+
fun `applyMetadata data collection takes precedence over send default pii`() {
1560+
val bundle =
1561+
bundleOf(
1562+
ManifestMetadataReader.SEND_DEFAULT_PII to false,
1563+
ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".mode" to "off",
1564+
)
1565+
val context = fixture.getContext(metaData = bundle)
1566+
1567+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1568+
1569+
assertThat(fixture.options.isSendDefaultPii).isFalse()
1570+
assertThat(fixture.options.dataCollectionResolver.isDataCollectionConfigured()).isTrue()
1571+
assertThat(fixture.options.dataCollectionResolver.isUserInfo).isTrue()
1572+
assertThat(fixture.options.dataCollectionResolver.cookies)
1573+
.isEqualTo(KeyValueCollectionBehavior.off())
1574+
}
1575+
1576+
@Test
1577+
fun `applyMetadata ignores invalid data collection body type`() {
1578+
val bundle = bundleOf(ManifestMetadataReader.DATA_COLLECTION_HTTP_BODIES to "invalid")
1579+
val context = fixture.getContext(metaData = bundle)
1580+
1581+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1582+
1583+
assertThat(fixture.options.dataCollectionResolver.isDataCollectionConfigured()).isFalse()
1584+
}
1585+
1586+
@Test
1587+
fun `applyMetadata ignores invalid data collection mode`() {
1588+
val bundle = bundleOf(ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".mode" to "invalid")
1589+
val context = fixture.getContext(metaData = bundle)
1590+
1591+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1592+
1593+
assertThat(fixture.options.dataCollectionResolver.isDataCollectionConfigured()).isFalse()
1594+
}
1595+
14611596
@Test
14621597
fun `applyMetadata reads frames tracking flag and keeps default value if not found`() {
14631598
// Arrange

‎sentry/api/sentry.api‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,13 +387,15 @@ public final class io/sentry/DataCategory : java/lang/Enum {
387387

388388
public final class io/sentry/DataCollection {
389389
public fun <init> ()V
390+
public fun <init> (Z)V
390391
public fun getCookies ()Lio/sentry/KeyValueCollectionBehavior;
391392
public fun getDatabaseQueryData ()Ljava/lang/Boolean;
392393
public fun getGraphql ()Lio/sentry/DataCollection$Graphql;
393394
public fun getHttpBodies ()Ljava/util/Set;
394395
public fun getHttpHeaders ()Lio/sentry/DataCollection$HttpHeaders;
395396
public fun getUrlQueryParams ()Lio/sentry/KeyValueCollectionBehavior;
396397
public fun getUserInfo ()Ljava/lang/Boolean;
398+
public fun isExplicitlyConfigured ()Z
397399
public fun setCookies (Lio/sentry/KeyValueCollectionBehavior;)V
398400
public fun setDatabaseQueryData (Z)V
399401
public fun setHttpBodies (Ljava/util/Set;)V

‎sentry/src/main/java/io/sentry/DataCollection.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public DataCollection() {
2424
this(true);
2525
}
2626

27-
DataCollection(final boolean forceDataCollection) {
27+
@ApiStatus.Internal
28+
public DataCollection(final boolean forceDataCollection) {
2829
this.forceDataCollection = forceDataCollection;
2930
}
3031

@@ -82,7 +83,7 @@ public void setDatabaseQueryData(final boolean databaseQueryData) {
8283
}
8384

8485
@ApiStatus.Internal
85-
boolean isExplicitlyConfigured() {
86+
public boolean isExplicitlyConfigured() {
8687
return forceDataCollection
8788
|| userInfo != null
8889
|| cookies != null

0 commit comments

Comments
 (0)