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
2 changes: 2 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ public final class io/sentry/ExternalOptions {
public fun getBundleIds ()Ljava/util/Set;
public fun getContextTags ()Ljava/util/List;
public fun getCron ()Lio/sentry/SentryOptions$Cron;
public fun getDataCollection ()Lio/sentry/DataCollection;
public fun getDebug ()Ljava/lang/Boolean;
public fun getDist ()Ljava/lang/String;
public fun getDsn ()Ljava/lang/String;
Expand Down Expand Up @@ -596,6 +597,7 @@ public final class io/sentry/ExternalOptions {
public fun isStrictTraceContinuation ()Ljava/lang/Boolean;
public fun setCaptureOpenTelemetryEvents (Ljava/lang/Boolean;)V
public fun setCron (Lio/sentry/SentryOptions$Cron;)V
public fun setDataCollection (Lio/sentry/DataCollection;)V
public fun setDebug (Ljava/lang/Boolean;)V
public fun setDist (Ljava/lang/String;)V
public fun setDsn (Ljava/lang/String;)V
Expand Down
105 changes: 105 additions & 0 deletions sentry/src/main/java/io/sentry/ExternalOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public final class ExternalOptions {

private @Nullable Boolean sendModules;
private @Nullable Boolean sendDefaultPii;
private @Nullable DataCollection dataCollection;
private @Nullable Boolean enableBackpressureHandling;
private @Nullable Boolean enableDatabaseTransactionTracing;
private @Nullable Boolean enableCacheTracing;
Expand Down Expand Up @@ -157,6 +158,7 @@ public final class ExternalOptions {

options.setSendModules(propertiesProvider.getBooleanProperty("send-modules"));
options.setSendDefaultPii(propertiesProvider.getBooleanProperty("send-default-pii"));
options.setDataCollection(parseDataCollection(propertiesProvider));

options.setIgnoredCheckIns(propertiesProvider.getListOrNull("ignored-checkins"));
options.setIgnoredTransactions(propertiesProvider.getListOrNull("ignored-transactions"));
Expand Down Expand Up @@ -246,6 +248,101 @@ public final class ExternalOptions {
return options;
}

private static @Nullable DataCollection parseDataCollection(
final @NotNull PropertiesProvider propertiesProvider) {
final DataCollection dataCollection = new DataCollection(false);

final Boolean userInfo = propertiesProvider.getBooleanProperty("data-collection.user-info");
if (userInfo != null) {
dataCollection.setUserInfo(userInfo);
}

final Set<HttpBodyType> httpBodies = parseHttpBodies(propertiesProvider);
if (httpBodies != null) {
dataCollection.setHttpBodies(httpBodies);
}

final KeyValueCollectionBehavior cookies =
parseKeyValueCollectionBehavior(propertiesProvider, "data-collection.cookies");
if (cookies != null) {
dataCollection.setCookies(cookies);
}

final KeyValueCollectionBehavior requestHeaders =
parseKeyValueCollectionBehavior(propertiesProvider, "data-collection.http-headers.request");
if (requestHeaders != null) {
dataCollection.getHttpHeaders().setRequest(requestHeaders);
}

final KeyValueCollectionBehavior responseHeaders =
parseKeyValueCollectionBehavior(
propertiesProvider, "data-collection.http-headers.response");
if (responseHeaders != null) {
dataCollection.getHttpHeaders().setResponse(responseHeaders);
}

final KeyValueCollectionBehavior queryParams =
parseKeyValueCollectionBehavior(propertiesProvider, "data-collection.query-params");
if (queryParams != null) {
dataCollection.setUrlQueryParams(queryParams);
}

final Boolean graphqlDocument =
propertiesProvider.getBooleanProperty("data-collection.graphql.document");
if (graphqlDocument != null) {
dataCollection.getGraphql().setDocument(graphqlDocument);
}

final Boolean graphqlVariables =
propertiesProvider.getBooleanProperty("data-collection.graphql.variables");
if (graphqlVariables != null) {
dataCollection.getGraphql().setVariables(graphqlVariables);
}

final Boolean databaseQueryData =
propertiesProvider.getBooleanProperty("data-collection.database-query-data");
if (databaseQueryData != null) {
dataCollection.setDatabaseQueryData(databaseQueryData);
}

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

private static @Nullable Set<HttpBodyType> parseHttpBodies(
final @NotNull PropertiesProvider propertiesProvider) {
final List<String> bodyTypes = propertiesProvider.getListOrNull("data-collection.http-bodies");
if (bodyTypes == null) {
return null;
}
if (bodyTypes.size() == 1 && bodyTypes.get(0).isEmpty()) {
return Collections.emptySet();
}

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

private static @Nullable KeyValueCollectionBehavior parseKeyValueCollectionBehavior(
final @NotNull PropertiesProvider propertiesProvider, final @NotNull String property) {
final String modeValue = propertiesProvider.getProperty(property + ".mode");
final List<String> terms = propertiesProvider.getListOrNull(property + ".terms");
if (modeValue == null && terms == null) {
return null;
}

final KeyValueCollectionBehavior behavior = new KeyValueCollectionBehavior();
if (modeValue != null) {
behavior.setMode(KeyValueCollectionBehavior.Mode.valueOf(modeValue.toUpperCase(Locale.ROOT)));
}
if (terms != null) {
behavior.setTerms(terms);
}
return behavior;
}

public @Nullable String getDsn() {
return dsn;
}
Expand Down Expand Up @@ -501,6 +598,14 @@ public void setSendDefaultPii(final @Nullable Boolean sendDefaultPii) {
this.sendDefaultPii = sendDefaultPii;
}

public @Nullable DataCollection getDataCollection() {
return dataCollection;
}

public void setDataCollection(final @Nullable DataCollection dataCollection) {
this.dataCollection = dataCollection;
}

public void setIgnoredCheckIns(final @Nullable List<String> ignoredCheckIns) {
this.ignoredCheckIns = ignoredCheckIns;
}
Expand Down
37 changes: 37 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -3627,6 +3627,9 @@ public void merge(final @NotNull ExternalOptions options) {
if (options.isSendDefaultPii() != null) {
setSendDefaultPii(options.isSendDefaultPii());
}
if (options.getDataCollection() != null) {
mergeDataCollection(options.getDataCollection());
}
if (options.isCaptureOpenTelemetryEvents() != null) {
setCaptureOpenTelemetryEvents(options.isCaptureOpenTelemetryEvents());
}
Expand Down Expand Up @@ -3692,6 +3695,40 @@ public void merge(final @NotNull ExternalOptions options) {
}
}

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

private @NotNull SdkVersion createSdkVersion() {
final String version = BuildConfig.VERSION_NAME;
final SdkVersion sdkVersion = new SdkVersion(BuildConfig.SENTRY_JAVA_SDK_NAME, version);
Expand Down
94 changes: 94 additions & 0 deletions sentry/src/test/java/io/sentry/ExternalOptionsTest.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.sentry

import com.google.common.truth.Truth.assertThat
import io.sentry.config.PropertiesProviderFactory
import java.lang.RuntimeException
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
Expand All @@ -15,6 +17,98 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

class ExternalOptionsTest {
@Test
fun `does not create data collection when external properties are absent`() {
withPropertiesFile { assertThat(it.dataCollection).isNull() }
}

@Test
fun `creates data collection using external properties`() {
withPropertiesFile(
listOf(
"data-collection.user-info=false",
"data-collection.http-bodies=incoming_request,outgoing_response",
"data-collection.cookies.mode=deny_list",
"data-collection.cookies.terms=authorization,session",
"data-collection.http-headers.request.mode=allow_list",
"data-collection.http-headers.request.terms=x-request-id,content-type",
"data-collection.http-headers.response.mode=off",
"data-collection.query-params.terms=search",
"data-collection.graphql.document=false",
"data-collection.graphql.variables=true",
"data-collection.database-query-data=false",
)
) { options ->
val dataCollection = options.dataCollection

assertThat(dataCollection).isNotNull()
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 `empty HTTP bodies externally disables body collection`() {
withPropertiesFile("data-collection.http-bodies=") { options ->
assertThat(options.dataCollection).isNotNull()
assertThat(options.dataCollection!!.httpBodies).isEmpty()
}
}

@Test
fun `invalid HTTP body type fails external parsing`() {
assertFailsWith<IllegalArgumentException> {
withPropertiesFile("data-collection.http-bodies=invalid") {}
}
}

@Test
fun `invalid collection mode fails external parsing`() {
assertFailsWith<IllegalArgumentException> {
withPropertiesFile("data-collection.cookies.mode=invalid") {}
}
}

@Test
fun `data collection booleans use default external parsing`() {
withPropertiesFile(
listOf(
"data-collection.user-info=invalid",
"data-collection.graphql.document=invalid",
"data-collection.graphql.variables=invalid",
"data-collection.database-query-data=invalid",
)
) { options ->
assertThat(options.dataCollection!!.userInfo).isFalse()
assertThat(options.dataCollection!!.graphql.document).isFalse()
assertThat(options.dataCollection!!.graphql.variables).isFalse()
assertThat(options.dataCollection!!.databaseQueryData).isFalse()
}
}

@Test
fun `external data collection takes precedence over external send default PII`() {
withPropertiesFile(listOf("send-default-pii=false", "data-collection.cookies.mode=off")) {
externalOptions ->
val options = SentryOptions().apply { merge(externalOptions) }

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

@Test
fun `creates options with proxy using external properties`() {
withPropertiesFile(
Expand Down
81 changes: 81 additions & 0 deletions sentry/src/test/java/io/sentry/SentryOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,87 @@ class SentryOptionsTest {
assertThat(options.dataCollection.userInfo).isFalse()
}

@Test
fun `merging absent external data collection preserves legacy mode`() {
val options = SentryOptions()

options.merge(ExternalOptions())

assertThat(options.dataCollection.isExplicitlyConfigured()).isFalse()
}

@Test
fun `merging external data collection applies only configured values`() {
val options =
SentryOptions().apply {
dataCollection.setUserInfo(false)
dataCollection.cookies = KeyValueCollectionBehavior.allowList("safe")
}
val externalOptions =
ExternalOptions().apply {
dataCollection = DataCollection().apply { graphql.setVariables(false) }
}

options.merge(externalOptions)

assertThat(options.dataCollection.userInfo).isFalse()
assertThat(options.dataCollection.cookies)
.isEqualTo(KeyValueCollectionBehavior.allowList("safe"))
assertThat(options.dataCollection.graphql.variables).isFalse()
}

@Test
fun `merging external data collection applies every supported value`() {
val externalDataCollection =
DataCollection().apply {
setUserInfo(false)
httpBodies = setOf(HttpBodyType.INCOMING_REQUEST, HttpBodyType.OUTGOING_RESPONSE)
cookies = KeyValueCollectionBehavior.denyList("cookie")
httpHeaders.request = KeyValueCollectionBehavior.allowList("request")
httpHeaders.response = KeyValueCollectionBehavior.off()
urlQueryParams = KeyValueCollectionBehavior.denyList("query")
graphql.setDocument(false)
graphql.setVariables(false)
setDatabaseQueryData(false)
}
val options = SentryOptions()

options.merge(ExternalOptions().apply { dataCollection = externalDataCollection })

assertThat(options.dataCollection.userInfo).isFalse()
assertThat(options.dataCollection.httpBodies)
.containsExactly(HttpBodyType.INCOMING_REQUEST, HttpBodyType.OUTGOING_RESPONSE)
assertThat(options.dataCollection.cookies)
.isEqualTo(KeyValueCollectionBehavior.denyList("cookie"))
assertThat(options.dataCollection.httpHeaders.request)
.isEqualTo(KeyValueCollectionBehavior.allowList("request"))
assertThat(options.dataCollection.httpHeaders.response)
.isEqualTo(KeyValueCollectionBehavior.off())
assertThat(options.dataCollection.urlQueryParams)
.isEqualTo(KeyValueCollectionBehavior.denyList("query"))
assertThat(options.dataCollection.graphql.document).isFalse()
assertThat(options.dataCollection.graphql.variables).isFalse()
assertThat(options.dataCollection.databaseQueryData).isFalse()
}

@Test
fun `external data collection takes precedence over send default PII`() {
val externalOptions =
ExternalOptions().apply {
isSendDefaultPii = false
dataCollection = DataCollection().apply { cookies = KeyValueCollectionBehavior.off() }
}
val options = SentryOptions()

options.merge(externalOptions)

assertThat(options.isSendDefaultPii).isFalse()
assertThat(options.dataCollection.isExplicitlyConfigured()).isTrue()
assertThat(options.dataCollectionResolver.isUserInfo).isTrue()
assertThat(options.dataCollectionResolver.isDatabaseQueryData).isTrue()
assertThat(options.dataCollectionResolver.cookies).isEqualTo(KeyValueCollectionBehavior.off())
}

@Test
fun `when options is initialized, logger is not null`() {
assertNotNull(SentryOptions().logger)
Expand Down
Loading