terms) {
+ this.terms = Collections.unmodifiableList(new ArrayList<>(terms));
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ final KeyValueCollectionBehavior that = (KeyValueCollectionBehavior) other;
+ return mode == that.mode && terms.equals(that.terms);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mode, terms);
+ }
+}
diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java
index d84c9e47be8..d72783cfe9c 100644
--- a/sentry/src/main/java/io/sentry/MainEventProcessor.java
+++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java
@@ -206,7 +206,7 @@ private void mergeUser(final @NotNull SentryBaseEvent event) {
user = new User();
event.setUser(user);
}
- if (user.getIpAddress() == null && options.isSendDefaultPii()) {
+ if (user.getIpAddress() == null && options.getDataCollectionResolver().isUserInfo()) {
user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS);
}
}
diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java
index d7a16d4ee23..690d5e535ce 100644
--- a/sentry/src/main/java/io/sentry/SentryOptions.java
+++ b/sentry/src/main/java/io/sentry/SentryOptions.java
@@ -347,6 +347,11 @@ public class SentryOptions {
/** whether to send personal identifiable information along with events */
private boolean sendDefaultPii = false;
+ private @NotNull DataCollection dataCollection = new DataCollection(false);
+
+ private final @NotNull DataCollectionResolver dataCollectionResolver =
+ new DataCollectionResolver(this);
+
/** SSLSocketFactory for self-signed certificate trust * */
private @Nullable SSLSocketFactory sslSocketFactory;
@@ -1751,6 +1756,33 @@ public void setSendDefaultPii(boolean sendDefaultPii) {
this.sendDefaultPii = sendDefaultPii;
}
+ /**
+ * Returns the configuration for data that the SDK collects automatically.
+ *
+ * The returned object is always present. Accessing it does not configure data collection, but
+ * setting one of its options does.
+ */
+ public @NotNull DataCollection getDataCollection() {
+ return dataCollection;
+ }
+
+ /**
+ * Replaces the configuration for data that the SDK collects automatically.
+ *
+ *
Passing an empty {@link DataCollection} opts into the documented data-collection defaults.
+ */
+ public void setDataCollection(final @NotNull DataCollection dataCollection) {
+ if (dataCollection != null) {
+ this.dataCollection = dataCollection;
+ }
+ }
+
+ /** Returns the Data Collection policy resolver used by SDK integrations. */
+ @ApiStatus.Internal
+ public @NotNull DataCollectionResolver getDataCollectionResolver() {
+ return dataCollectionResolver;
+ }
+
/**
* Adds a Scope observer
*
@@ -3712,6 +3744,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());
}
@@ -3777,6 +3812,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);
diff --git a/sentry/src/main/java/io/sentry/TraceContext.java b/sentry/src/main/java/io/sentry/TraceContext.java
index b10954f5285..1bb5508f85b 100644
--- a/sentry/src/main/java/io/sentry/TraceContext.java
+++ b/sentry/src/main/java/io/sentry/TraceContext.java
@@ -1,7 +1,6 @@
package io.sentry;
import io.sentry.protocol.SentryId;
-import io.sentry.protocol.User;
import io.sentry.vendor.gson.stream.JsonToken;
import java.io.IOException;
import java.util.Map;
@@ -81,16 +80,6 @@ public final class TraceContext implements JsonUnknown, JsonSerializable {
this.sampleRand = sampleRand;
}
- @SuppressWarnings("UnusedMethod")
- private static @Nullable String getUserId(
- final @NotNull SentryOptions options, final @Nullable User user) {
- if (options.isSendDefaultPii() && user != null) {
- return user.getId();
- }
-
- return null;
- }
-
public @NotNull SentryId getTraceId() {
return traceId;
}
diff --git a/sentry/src/main/java/io/sentry/util/CookieUtils.java b/sentry/src/main/java/io/sentry/util/CookieUtils.java
new file mode 100644
index 00000000000..935f98aaf1a
--- /dev/null
+++ b/sentry/src/main/java/io/sentry/util/CookieUtils.java
@@ -0,0 +1,291 @@
+package io.sentry.util;
+
+import static io.sentry.util.UrlUtils.SENSITIVE_DATA_SUBSTITUTE;
+
+import io.sentry.KeyValueCollectionBehavior;
+import io.sentry.SentryOptions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@ApiStatus.Internal
+public final class CookieUtils {
+
+ public static final String COOKIE_HEADER_NAME = "Cookie";
+
+ private static final List SECURITY_COOKIES =
+ Arrays.asList(
+ "JSESSIONID",
+ "JSESSIONIDSSO",
+ "JSSOSESSIONID",
+ "SESSIONID",
+ "SID",
+ "CSRFTOKEN",
+ "XSRF-TOKEN");
+
+ public static @Nullable List filterCookiesFromHeader(
+ final @Nullable Enumeration headers,
+ final @NotNull KeyValueCollectionBehavior behavior,
+ final @Nullable List additionalSensitiveCookieNames) {
+ return headers == null
+ ? null
+ : filterCookiesFromHeader(
+ Collections.list(headers), behavior, additionalSensitiveCookieNames);
+ }
+
+ public static @Nullable List filterCookiesFromHeader(
+ final @Nullable List headers,
+ final @NotNull KeyValueCollectionBehavior behavior,
+ final @Nullable List additionalSensitiveCookieNames) {
+ if (headers == null || behavior.getMode() == KeyValueCollectionBehavior.Mode.OFF) {
+ return null;
+ }
+
+ final @NotNull List filteredHeaders = new ArrayList<>();
+ for (final String header : headers) {
+ final @Nullable String filteredHeader =
+ filterCookies(header, behavior, additionalSensitiveCookieNames);
+ if (filteredHeader != null) {
+ filteredHeaders.add(filteredHeader);
+ }
+ }
+ return filteredHeaders;
+ }
+
+ public static @Nullable String filterCookies(
+ final @Nullable String cookies, final @NotNull SentryOptions options) {
+ if (!options.getDataCollectionResolver().isDataCollectionConfigured()) {
+ return options.isSendDefaultPii() ? cookies : null;
+ }
+ return filterCookies(cookies, options.getDataCollectionResolver().getCookies(), null);
+ }
+
+ public static @Nullable String filterCookies(
+ final @Nullable String cookies,
+ final @NotNull KeyValueCollectionBehavior behavior,
+ final @Nullable List additionalSensitiveCookieNames) {
+ if (cookies == null || behavior.getMode() == KeyValueCollectionBehavior.Mode.OFF) {
+ return null;
+ }
+
+ final @NotNull String[] cookieValues = cookies.split(";", -1);
+ final @NotNull StringBuilder filteredCookies = new StringBuilder();
+ for (int i = 0; i < cookieValues.length; i++) {
+ if (i > 0) {
+ filteredCookies.append(';');
+ }
+ filteredCookies.append(
+ filterCookie(cookieValues[i], behavior, additionalSensitiveCookieNames));
+ }
+ return filteredCookies.toString();
+ }
+
+ public static @Nullable String filterSetCookie(
+ final @Nullable String cookie, final @NotNull SentryOptions options) {
+ if (!options.getDataCollectionResolver().isDataCollectionConfigured()) {
+ return options.isSendDefaultPii() ? cookie : null;
+ }
+ return filterSetCookie(cookie, options.getDataCollectionResolver().getCookies());
+ }
+
+ public static @Nullable String filterSetCookie(
+ final @Nullable String cookie, final @NotNull KeyValueCollectionBehavior behavior) {
+ if (cookie == null || behavior.getMode() == KeyValueCollectionBehavior.Mode.OFF) {
+ return null;
+ }
+
+ final int attributesSeparator = cookie.indexOf(';');
+ final @NotNull String cookieValue =
+ attributesSeparator < 0 ? cookie : cookie.substring(0, attributesSeparator);
+ if (!isValidCookiePair(cookieValue)) {
+ return SENSITIVE_DATA_SUBSTITUTE;
+ }
+ final @NotNull String attributes =
+ attributesSeparator < 0 ? "" : cookie.substring(attributesSeparator);
+ return filterCookie(cookieValue, behavior, null) + attributes;
+ }
+
+ private static @NotNull String filterCookie(
+ final @NotNull String cookie,
+ final @NotNull KeyValueCollectionBehavior behavior,
+ final @Nullable List additionalSensitiveCookieNames) {
+ if (cookie.trim().isEmpty()) {
+ return cookie;
+ }
+
+ if (!isValidCookiePair(cookie)) {
+ return SENSITIVE_DATA_SUBSTITUTE;
+ }
+
+ final int separator = cookie.indexOf('=');
+ final @NotNull String name = cookie.substring(0, separator);
+ final @NotNull String normalizedName = name.trim();
+ final boolean sensitive =
+ HttpUtils.containsSensitiveDataKey(normalizedName)
+ || isSecurityCookie(normalizedName, additionalSensitiveCookieNames);
+ final boolean matchesTerm = HttpUtils.containsTerm(normalizedName, behavior.getTerms());
+ final boolean shouldFilter =
+ sensitive
+ || (behavior.getMode() == KeyValueCollectionBehavior.Mode.DENY_LIST && matchesTerm)
+ || (behavior.getMode() == KeyValueCollectionBehavior.Mode.ALLOW_LIST && !matchesTerm);
+
+ if (shouldFilter) {
+ return name + "=" + SENSITIVE_DATA_SUBSTITUTE;
+ }
+ return cookie;
+ }
+
+ private static boolean isValidCookiePair(final @NotNull String cookie) {
+ final @NotNull String cookiePair = cookie.trim();
+ final int separator = cookiePair.indexOf('=');
+ if (separator <= 0 || !isValidCookieName(cookiePair.substring(0, separator))) {
+ return false;
+ }
+
+ final @NotNull String value = cookiePair.substring(separator + 1);
+ int start = 0;
+ int end = value.length();
+ if (!value.isEmpty() && value.charAt(0) == '"') {
+ if (value.length() < 2 || value.charAt(value.length() - 1) != '"') {
+ return false;
+ }
+ start++;
+ end--;
+ }
+
+ for (int i = start; i < end; i++) {
+ if (!isCookieOctet(value.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean isValidCookieName(final @NotNull String name) {
+ for (int i = 0; i < name.length(); i++) {
+ if (!isCookieNameCharacter(name.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean isCookieNameCharacter(final char value) {
+ if ((value >= 'a' && value <= 'z')
+ || (value >= 'A' && value <= 'Z')
+ || (value >= '0' && value <= '9')) {
+ return true;
+ }
+
+ switch (value) {
+ case '!':
+ case '#':
+ case '$':
+ case '%':
+ case '&':
+ case '\'':
+ case '*':
+ case '+':
+ case '-':
+ case '.':
+ case '^':
+ case '_':
+ case '`':
+ case '|':
+ case '~':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static boolean isCookieOctet(final char value) {
+ return value == 0x21
+ || (value >= 0x23 && value <= 0x2B)
+ || (value >= 0x2D && value <= 0x3A)
+ || (value >= 0x3C && value <= 0x5B)
+ || (value >= 0x5D && value <= 0x7E);
+ }
+
+ public static @Nullable List filterOutSecurityCookiesFromHeader(
+ final @Nullable Enumeration headers,
+ final @Nullable String headerName,
+ final @Nullable List additionalCookieNamesToFilter) {
+ if (headers == null) {
+ return null;
+ }
+
+ return filterOutSecurityCookiesFromHeader(
+ Collections.list(headers), headerName, additionalCookieNamesToFilter);
+ }
+
+ public static @Nullable List filterOutSecurityCookiesFromHeader(
+ final @Nullable List headers,
+ final @Nullable String headerName,
+ final @Nullable List additionalCookieNamesToFilter) {
+ if (headers == null) {
+ return null;
+ }
+
+ if (headerName != null && !COOKIE_HEADER_NAME.equalsIgnoreCase(headerName)) {
+ return headers;
+ }
+
+ final @NotNull ArrayList filteredHeaders = new ArrayList<>();
+ for (final String header : headers) {
+ filteredHeaders.add(filterOutSecurityCookies(header, additionalCookieNamesToFilter));
+ }
+ return filteredHeaders;
+ }
+
+ public static @Nullable String filterOutSecurityCookies(
+ final @Nullable String cookieString,
+ final @Nullable List additionalCookieNamesToFilter) {
+ if (cookieString == null) {
+ return null;
+ }
+
+ final @NotNull String[] cookies = cookieString.split(";", -1);
+ final @NotNull StringBuilder filteredCookieString = new StringBuilder();
+ boolean isFirst = true;
+ for (String cookie : cookies) {
+ if (!isFirst) {
+ filteredCookieString.append(";");
+ }
+
+ final @NotNull String[] cookieParts = cookie.split("=", -1);
+ final @NotNull String cookieName = cookieParts[0];
+ if (isSecurityCookie(cookieName.trim(), additionalCookieNamesToFilter)) {
+ filteredCookieString.append(cookieName + "=" + SENSITIVE_DATA_SUBSTITUTE);
+ } else {
+ filteredCookieString.append(cookie);
+ }
+ isFirst = false;
+ }
+ return filteredCookieString.toString();
+ }
+
+ public static boolean isSecurityCookie(
+ final @NotNull String cookieName,
+ final @Nullable List additionalCookieNamesToFilter) {
+ final @NotNull String cookieNameToSearchFor = cookieName.toUpperCase(Locale.ROOT);
+ if (SECURITY_COOKIES.contains(cookieNameToSearchFor)) {
+ return true;
+ }
+
+ if (additionalCookieNamesToFilter != null) {
+ for (String additionalCookieName : additionalCookieNamesToFilter) {
+ if (additionalCookieName.toUpperCase(Locale.ROOT).equals(cookieNameToSearchFor)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/sentry/src/main/java/io/sentry/util/GraphqlUtils.java b/sentry/src/main/java/io/sentry/util/GraphqlUtils.java
new file mode 100644
index 00000000000..faffc98afa5
--- /dev/null
+++ b/sentry/src/main/java/io/sentry/util/GraphqlUtils.java
@@ -0,0 +1,79 @@
+package io.sentry.util;
+
+import io.sentry.DataCollectionResolver;
+import io.sentry.JsonObjectReader;
+import io.sentry.SentryLevel;
+import io.sentry.SentryOptions;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@ApiStatus.Internal
+public final class GraphqlUtils {
+
+ private GraphqlUtils() {}
+
+ public static @Nullable String filterRequestBody(
+ final @NotNull String body, final @NotNull SentryOptions options) {
+ final @NotNull DataCollectionResolver resolver = options.getDataCollectionResolver();
+ final boolean includeDocument = resolver.isGraphqlDocumentWithLegacyAlways();
+ final boolean includeVariables = resolver.isGraphqlVariablesWithLegacyAlways();
+
+ if (includeDocument && includeVariables) {
+ return body;
+ }
+ if (!includeDocument && !includeVariables) {
+ return null;
+ }
+
+ try (JsonObjectReader reader = new JsonObjectReader(new StringReader(body))) {
+ final @Nullable Object value = reader.nextObjectOrNull();
+ final @NotNull Object filtered;
+ if (value instanceof Map) {
+ @SuppressWarnings("unchecked")
+ final @NotNull Map requestBody = (Map) value;
+ filtered = filterRequest(requestBody, includeDocument, includeVariables);
+ } else if (value instanceof List) {
+ final @NotNull List