Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sentry-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ dependencies {
testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.androidx.test.runner)
testImplementation(libs.awaitility.kotlin)
testImplementation(libs.google.truth)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.mockito.inline)
testImplementation(projects.sentryTestSupport)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Bundle;
import io.sentry.DataCollection;
import io.sentry.HttpBodyType;
import io.sentry.ILogger;
import io.sentry.InitPriority;
import io.sentry.KeyValueCollectionBehavior;
import io.sentry.ProfileLifecycle;
import io.sentry.ScreenshotStrategyType;
import io.sentry.SentryFeedbackOptions;
Expand All @@ -16,8 +19,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -102,6 +107,22 @@ final class ManifestMetadataReader {

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

static final String DATA_COLLECTION_USER_INFO = "io.sentry.data-collection.user-info";
static final String DATA_COLLECTION_HTTP_BODIES = "io.sentry.data-collection.http-bodies";
static final String DATA_COLLECTION_COOKIES = "io.sentry.data-collection.cookies";
static final String DATA_COLLECTION_HTTP_REQUEST_HEADERS =
"io.sentry.data-collection.http-headers.request";
static final String DATA_COLLECTION_HTTP_RESPONSE_HEADERS =
"io.sentry.data-collection.http-headers.response";
static final String DATA_COLLECTION_URL_QUERY_PARAMS =
"io.sentry.data-collection.url-query-params";
static final String DATA_COLLECTION_GRAPHQL_DOCUMENT =
"io.sentry.data-collection.graphql.document";
static final String DATA_COLLECTION_GRAPHQL_VARIABLES =
"io.sentry.data-collection.graphql.variables";
static final String DATA_COLLECTION_DATABASE_QUERY_DATA =
"io.sentry.data-collection.database-query-data";

static final String PERFORM_FRAMES_TRACKING = "io.sentry.traces.frames-tracking";

static final String SENTRY_GRADLE_PLUGIN_INTEGRATIONS = "io.sentry.gradle-plugin-integrations";
Expand Down Expand Up @@ -761,6 +782,12 @@ static void applyMetadata(
options.setEnableAnrFingerprinting(
readBool(
metadata, logger, ENABLE_ANR_FINGERPRINTING, options.isEnableAnrFingerprinting()));

final @Nullable DataCollection dataCollection =
readDataCollection(metadata, logger, options.getDataCollection());
if (dataCollection != null) {
mergeDataCollection(options.getDataCollection(), dataCollection);
}
}
options
.getLogger()
Expand All @@ -773,6 +800,152 @@ static void applyMetadata(
}
}

private static @Nullable DataCollection readDataCollection(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romtsn recently refactored all the methods in this class that read from the manifest in order to allow a performance improvement from the Gradle side. PR link It looks like the changes aren't in this PR. Can you see if a rebase fixes that? Without a rebase it is likely this code would crash when run together with the Gradle plugin.

final @NotNull Bundle metadata,
final @NotNull ILogger logger,
final @NotNull DataCollection currentDataCollection) {
final @NotNull DataCollection dataCollection = new DataCollection(false);

if (metadata.containsKey(DATA_COLLECTION_USER_INFO)) {
dataCollection.setUserInfo(readBool(metadata, logger, DATA_COLLECTION_USER_INFO, false));
}

if (metadata.containsKey(DATA_COLLECTION_HTTP_BODIES)) {
dataCollection.setHttpBodies(readHttpBodyTypes(metadata, logger));
}

final @Nullable KeyValueCollectionBehavior cookies =
readKeyValueCollectionBehavior(
metadata, logger, DATA_COLLECTION_COOKIES, currentDataCollection.getCookies());
if (cookies != null) {
dataCollection.setCookies(cookies);
}

final @Nullable KeyValueCollectionBehavior requestHeaders =
readKeyValueCollectionBehavior(
metadata,
logger,
DATA_COLLECTION_HTTP_REQUEST_HEADERS,
currentDataCollection.getHttpHeaders().getRequest());
if (requestHeaders != null) {
dataCollection.getHttpHeaders().setRequest(requestHeaders);
}

final @Nullable KeyValueCollectionBehavior responseHeaders =
readKeyValueCollectionBehavior(
metadata,
logger,
DATA_COLLECTION_HTTP_RESPONSE_HEADERS,
currentDataCollection.getHttpHeaders().getResponse());
if (responseHeaders != null) {
dataCollection.getHttpHeaders().setResponse(responseHeaders);
}

final @Nullable KeyValueCollectionBehavior urlQueryParams =
readKeyValueCollectionBehavior(
metadata,
logger,
DATA_COLLECTION_URL_QUERY_PARAMS,
currentDataCollection.getUrlQueryParams());
if (urlQueryParams != null) {
dataCollection.setUrlQueryParams(urlQueryParams);
}

if (metadata.containsKey(DATA_COLLECTION_GRAPHQL_DOCUMENT)) {
dataCollection
.getGraphql()
.setDocument(readBool(metadata, logger, DATA_COLLECTION_GRAPHQL_DOCUMENT, false));
}

if (metadata.containsKey(DATA_COLLECTION_GRAPHQL_VARIABLES)) {
dataCollection
.getGraphql()
.setVariables(readBool(metadata, logger, DATA_COLLECTION_GRAPHQL_VARIABLES, false));
}

if (metadata.containsKey(DATA_COLLECTION_DATABASE_QUERY_DATA)) {
dataCollection.setDatabaseQueryData(
readBool(metadata, logger, DATA_COLLECTION_DATABASE_QUERY_DATA, false));
}

return dataCollection.isExplicitlyConfigured() ? dataCollection : null;
}

private static void mergeDataCollection(
final @NotNull DataCollection target, final @NotNull DataCollection source) {
if (source.getUserInfo() != null) {
target.setUserInfo(source.getUserInfo());
}
if (source.getHttpBodies() != null) {
target.setHttpBodies(source.getHttpBodies());
}
if (source.getCookies() != null) {
target.setCookies(source.getCookies());
}
if (source.getHttpHeaders().getRequest() != null) {
target.getHttpHeaders().setRequest(source.getHttpHeaders().getRequest());
}
if (source.getHttpHeaders().getResponse() != null) {
target.getHttpHeaders().setResponse(source.getHttpHeaders().getResponse());
}
if (source.getUrlQueryParams() != null) {
target.setUrlQueryParams(source.getUrlQueryParams());
}
if (source.getGraphql().getDocument() != null) {
target.getGraphql().setDocument(source.getGraphql().getDocument());
}
if (source.getGraphql().getVariables() != null) {
target.getGraphql().setVariables(source.getGraphql().getVariables());
}
if (source.getDatabaseQueryData() != null) {
target.setDatabaseQueryData(source.getDatabaseQueryData());
}
}

private static @NotNull Set<HttpBodyType> readHttpBodyTypes(
final @NotNull Bundle metadata, final @NotNull ILogger logger) {
final @Nullable List<String> bodyTypes =
readList(metadata, logger, DATA_COLLECTION_HTTP_BODIES);
if (bodyTypes == null || (bodyTypes.size() == 1 && bodyTypes.get(0).isEmpty())) {
return Collections.emptySet();
}

final @NotNull Set<HttpBodyType> result = EnumSet.noneOf(HttpBodyType.class);
for (final String bodyType : bodyTypes) {
result.add(HttpBodyType.valueOf(bodyType.toUpperCase(Locale.ROOT)));
}
return result;
}

private static @Nullable KeyValueCollectionBehavior readKeyValueCollectionBehavior(
final @NotNull Bundle metadata,
final @NotNull ILogger logger,
final @NotNull String key,
final @Nullable KeyValueCollectionBehavior currentBehavior) {
final @NotNull String modeKey = key + ".mode";
final @NotNull String termsKey = key + ".terms";
if (!metadata.containsKey(modeKey) && !metadata.containsKey(termsKey)) {
return null;
}

final @NotNull KeyValueCollectionBehavior behavior = new KeyValueCollectionBehavior();
if (currentBehavior != null) {
behavior.setMode(currentBehavior.getMode());
behavior.setTerms(currentBehavior.getTerms());
}
if (metadata.containsKey(modeKey)) {
final @Nullable String mode = readString(metadata, logger, modeKey, null);
if (mode != null) {
behavior.setMode(KeyValueCollectionBehavior.Mode.valueOf(mode.toUpperCase(Locale.ROOT)));
}
}
if (metadata.containsKey(termsKey)) {
final @Nullable List<String> terms = readList(metadata, logger, termsKey);
behavior.setTerms(terms == null ? Collections.<String>emptyList() : terms);
}
return behavior;
}

private static boolean readBool(
final @NotNull Bundle metadata,
final @NotNull ILogger logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import android.content.Context
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.sentry.FilterString
import io.sentry.HttpBodyType
import io.sentry.ILogger
import io.sentry.KeyValueCollectionBehavior
import io.sentry.ProfileLifecycle
import io.sentry.SentryLevel
import io.sentry.SentryReplayOptions
Expand Down Expand Up @@ -1409,6 +1412,139 @@ class ManifestMetadataReaderTest {
assertTrue(fixture.options.isSendDefaultPii)
}

@Test
fun `applyMetadata preserves legacy data collection when metadata is absent`() {
val context = fixture.getContext()

ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

assertThat(fixture.options.dataCollectionResolver.isDataCollectionConfigured()).isFalse()
}

@Test
fun `applyMetadata reads data collection options`() {
val bundle =
bundleOf(
ManifestMetadataReader.DATA_COLLECTION_USER_INFO to false,
ManifestMetadataReader.DATA_COLLECTION_HTTP_BODIES to "incoming_request,outgoing_response",
ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".mode" to "deny_list",
ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".terms" to "authorization,session",
ManifestMetadataReader.DATA_COLLECTION_HTTP_REQUEST_HEADERS + ".mode" to "allow_list",
ManifestMetadataReader.DATA_COLLECTION_HTTP_REQUEST_HEADERS + ".terms" to
"x-request-id,content-type",
ManifestMetadataReader.DATA_COLLECTION_HTTP_RESPONSE_HEADERS + ".mode" to "off",
ManifestMetadataReader.DATA_COLLECTION_URL_QUERY_PARAMS + ".terms" to "search",
ManifestMetadataReader.DATA_COLLECTION_GRAPHQL_DOCUMENT to false,
ManifestMetadataReader.DATA_COLLECTION_GRAPHQL_VARIABLES to true,
ManifestMetadataReader.DATA_COLLECTION_DATABASE_QUERY_DATA to false,
)
val context = fixture.getContext(metaData = bundle)

ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

val dataCollection = fixture.options.dataCollection
assertThat(dataCollection.userInfo).isFalse()
assertThat(dataCollection.httpBodies)
.containsExactly(HttpBodyType.INCOMING_REQUEST, HttpBodyType.OUTGOING_RESPONSE)
assertThat(dataCollection.cookies)
.isEqualTo(KeyValueCollectionBehavior.denyList("authorization", "session"))
assertThat(dataCollection.httpHeaders.request)
.isEqualTo(KeyValueCollectionBehavior.allowList("x-request-id", "content-type"))
assertThat(dataCollection.httpHeaders.response).isEqualTo(KeyValueCollectionBehavior.off())
assertThat(dataCollection.urlQueryParams)
.isEqualTo(KeyValueCollectionBehavior.denyList("search"))
assertThat(dataCollection.graphql.document).isFalse()
assertThat(dataCollection.graphql.variables).isTrue()
assertThat(dataCollection.databaseQueryData).isFalse()
}

@Test
fun `applyMetadata only overrides explicitly configured data collection options`() {
val dataCollection =
fixture.options.dataCollection.apply {
setUserInfo(true)
setHttpBodies(setOf(HttpBodyType.OUTGOING_REQUEST))
cookies = KeyValueCollectionBehavior.allowList("existing-cookie")
httpHeaders.request = KeyValueCollectionBehavior.denyList("existing-request-header")
httpHeaders.response = KeyValueCollectionBehavior.allowList("existing-response-header")
urlQueryParams = KeyValueCollectionBehavior.off()
graphql.setDocument(true)
graphql.setVariables(false)
setDatabaseQueryData(true)
}
val bundle =
bundleOf(
ManifestMetadataReader.DATA_COLLECTION_USER_INFO to false,
ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".terms" to "manifest-cookie",
ManifestMetadataReader.DATA_COLLECTION_HTTP_REQUEST_HEADERS + ".mode" to "allow_list",
)
val context = fixture.getContext(metaData = bundle)

ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

assertThat(fixture.options.dataCollection).isSameInstanceAs(dataCollection)
assertThat(dataCollection.userInfo).isFalse()
assertThat(dataCollection.httpBodies).containsExactly(HttpBodyType.OUTGOING_REQUEST)
assertThat(dataCollection.cookies)
.isEqualTo(KeyValueCollectionBehavior.allowList("manifest-cookie"))
assertThat(dataCollection.httpHeaders.request)
.isEqualTo(KeyValueCollectionBehavior.allowList("existing-request-header"))
assertThat(dataCollection.httpHeaders.response)
.isEqualTo(KeyValueCollectionBehavior.allowList("existing-response-header"))
assertThat(dataCollection.urlQueryParams).isEqualTo(KeyValueCollectionBehavior.off())
assertThat(dataCollection.graphql.document).isTrue()
assertThat(dataCollection.graphql.variables).isFalse()
assertThat(dataCollection.databaseQueryData).isTrue()
}

@Test
fun `applyMetadata reads empty HTTP bodies as disabled`() {
val bundle = bundleOf(ManifestMetadataReader.DATA_COLLECTION_HTTP_BODIES to "")
val context = fixture.getContext(metaData = bundle)

ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

assertThat(fixture.options.dataCollection.httpBodies).isEmpty()
}

@Test
fun `applyMetadata data collection takes precedence over send default pii`() {
val bundle =
bundleOf(
ManifestMetadataReader.SEND_DEFAULT_PII to false,
ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".mode" to "off",
)
val context = fixture.getContext(metaData = bundle)

ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

assertThat(fixture.options.isSendDefaultPii).isFalse()
assertThat(fixture.options.dataCollectionResolver.isDataCollectionConfigured()).isTrue()
assertThat(fixture.options.dataCollectionResolver.isUserInfo).isTrue()
assertThat(fixture.options.dataCollectionResolver.cookies)
.isEqualTo(KeyValueCollectionBehavior.off())
}

@Test
fun `applyMetadata ignores invalid data collection body type`() {
val bundle = bundleOf(ManifestMetadataReader.DATA_COLLECTION_HTTP_BODIES to "invalid")
val context = fixture.getContext(metaData = bundle)

ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

assertThat(fixture.options.dataCollectionResolver.isDataCollectionConfigured()).isFalse()
}

@Test
fun `applyMetadata ignores invalid data collection mode`() {
val bundle = bundleOf(ManifestMetadataReader.DATA_COLLECTION_COOKIES + ".mode" to "invalid")
val context = fixture.getContext(metaData = bundle)

ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

assertThat(fixture.options.dataCollectionResolver.isDataCollectionConfigured()).isFalse()
}

@Test
fun `applyMetadata reads frames tracking flag and keeps default value if not found`() {
// Arrange
Expand Down
2 changes: 2 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -386,13 +386,15 @@ public final class io/sentry/DataCategory : java/lang/Enum {

public final class io/sentry/DataCollection {
public fun <init> ()V
public fun <init> (Z)V

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird, there's only one constructor. this makes it seem like there are two

public fun getCookies ()Lio/sentry/KeyValueCollectionBehavior;
public fun getDatabaseQueryData ()Ljava/lang/Boolean;
public fun getGraphql ()Lio/sentry/DataCollection$Graphql;
public fun getHttpBodies ()Ljava/util/Set;
public fun getHttpHeaders ()Lio/sentry/DataCollection$HttpHeaders;
public fun getUrlQueryParams ()Lio/sentry/KeyValueCollectionBehavior;
public fun getUserInfo ()Ljava/lang/Boolean;
public fun isExplicitlyConfigured ()Z
public fun setCookies (Lio/sentry/KeyValueCollectionBehavior;)V
public fun setDatabaseQueryData (Z)V
public fun setHttpBodies (Ljava/util/Set;)V
Expand Down
Loading
Loading