Skip to content

Commit b03edbb

Browse files
43jaygetsentry-bot
andauthored
SentryAndroidOptions to enable NetworkDetails extraction (#4900)
* Add network details to SentryReplayOptions API ./gradlew apiDump * Extract NetworkDetails options from manifest * Hook SentryOkHttpInterceptor into SentryReplayOptions * Merge requested headers with default headers on write instead of on read More efficient -> getNetworkRequestHeaders/getNetworkResponseHeaders is invoked on every http request but setNetwork... is only invoked on start-up * Initialize RRWebOptionsEvent#networkDetailHasUrls based on SentryReplayOptions networkDetailHasUrls is a gate that the front-end uses to determine whether there is data to show the end-user https://github.com/getsentry/sentry-javascript/blob/090a3e35a94014aad4dfd06a6ff3c361f0420009/packages/replay-internal/src/util/handleRecordingEmit.ts#L134 * Remove defensive copy when returning request|responseHeaders getNetworkRequestHeaders / getNetworkResponseHeaders are called on every http request => move the memory operation to the setNetworkRequest|ResponseHeaders path which is called 1x on start-up * Add the network details options as 'tags' on the replay * Add network details flags in manifest for sentry-samples test app --------- Co-authored-by: Sentry Github Bot <[email protected]>
1 parent 23b6ef9 commit b03edbb

File tree

11 files changed

+779
-22
lines changed

11 files changed

+779
-22
lines changed

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
import io.sentry.SentryFeedbackOptions;
1111
import io.sentry.SentryIntegrationPackageStorage;
1212
import io.sentry.SentryLevel;
13+
import io.sentry.SentryReplayOptions;
1314
import io.sentry.protocol.SdkVersion;
1415
import io.sentry.util.Objects;
16+
import java.util.ArrayList;
1517
import java.util.Arrays;
1618
import java.util.Collections;
1719
import java.util.List;
@@ -114,6 +116,21 @@ final class ManifestMetadataReader {
114116
static final String REPLAYS_DEBUG = "io.sentry.session-replay.debug";
115117
static final String REPLAYS_SCREENSHOT_STRATEGY = "io.sentry.session-replay.screenshot-strategy";
116118

119+
static final String REPLAYS_NETWORK_DETAIL_ALLOW_URLS =
120+
"io.sentry.session-replay.network-detail-allow-urls";
121+
122+
static final String REPLAYS_NETWORK_DETAIL_DENY_URLS =
123+
"io.sentry.session-replay.network-detail-deny-urls";
124+
125+
static final String REPLAYS_NETWORK_CAPTURE_BODIES =
126+
"io.sentry.session-replay.network-capture-bodies";
127+
128+
static final String REPLAYS_NETWORK_REQUEST_HEADERS =
129+
"io.sentry.session-replay.network-request-headers";
130+
131+
static final String REPLAYS_NETWORK_RESPONSE_HEADERS =
132+
"io.sentry.session-replay.network-response-headers";
133+
117134
static final String FORCE_INIT = "io.sentry.force-init";
118135

119136
static final String MAX_BREADCRUMBS = "io.sentry.max-breadcrumbs";
@@ -488,6 +505,91 @@ static void applyMetadata(
488505
options.getSessionReplay().setScreenshotStrategy(ScreenshotStrategyType.PIXEL_COPY);
489506
}
490507
}
508+
509+
// Network Details Configuration
510+
if (options.getSessionReplay().getNetworkDetailAllowUrls().length == 0) {
511+
final @Nullable List<String> allowUrls =
512+
readList(metadata, logger, REPLAYS_NETWORK_DETAIL_ALLOW_URLS);
513+
if (allowUrls != null && !allowUrls.isEmpty()) {
514+
final List<String> filteredUrls = new ArrayList<>();
515+
for (String url : allowUrls) {
516+
final String trimmedUrl = url.trim();
517+
if (!trimmedUrl.isEmpty()) {
518+
filteredUrls.add(trimmedUrl);
519+
}
520+
}
521+
if (!filteredUrls.isEmpty()) {
522+
options
523+
.getSessionReplay()
524+
.setNetworkDetailAllowUrls(filteredUrls.toArray(new String[0]));
525+
}
526+
}
527+
}
528+
529+
if (options.getSessionReplay().getNetworkDetailDenyUrls().length == 0) {
530+
final @Nullable List<String> denyUrls =
531+
readList(metadata, logger, REPLAYS_NETWORK_DETAIL_DENY_URLS);
532+
if (denyUrls != null && !denyUrls.isEmpty()) {
533+
final List<String> filteredUrls = new ArrayList<>();
534+
for (String url : denyUrls) {
535+
final String trimmedUrl = url.trim();
536+
if (!trimmedUrl.isEmpty()) {
537+
filteredUrls.add(trimmedUrl);
538+
}
539+
}
540+
if (!filteredUrls.isEmpty()) {
541+
options
542+
.getSessionReplay()
543+
.setNetworkDetailDenyUrls(filteredUrls.toArray(new String[0]));
544+
}
545+
}
546+
}
547+
548+
options
549+
.getSessionReplay()
550+
.setNetworkCaptureBodies(
551+
readBool(
552+
metadata,
553+
logger,
554+
REPLAYS_NETWORK_CAPTURE_BODIES,
555+
options.getSessionReplay().isNetworkCaptureBodies() /* defaultValue */));
556+
557+
if (options.getSessionReplay().getNetworkRequestHeaders().length
558+
== SentryReplayOptions.getNetworkDetailsDefaultHeaders().size()) { // Only has defaults
559+
final @Nullable List<String> requestHeaders =
560+
readList(metadata, logger, REPLAYS_NETWORK_REQUEST_HEADERS);
561+
if (requestHeaders != null) {
562+
final List<String> filteredHeaders = new ArrayList<>();
563+
for (String header : requestHeaders) {
564+
final String trimmedHeader = header.trim();
565+
if (!trimmedHeader.isEmpty()) {
566+
filteredHeaders.add(trimmedHeader);
567+
}
568+
}
569+
if (!filteredHeaders.isEmpty()) {
570+
options.getSessionReplay().setNetworkRequestHeaders(filteredHeaders);
571+
}
572+
}
573+
}
574+
575+
if (options.getSessionReplay().getNetworkResponseHeaders().length
576+
== SentryReplayOptions.getNetworkDetailsDefaultHeaders().size()) { // Only has defaults
577+
final @Nullable List<String> responseHeaders =
578+
readList(metadata, logger, REPLAYS_NETWORK_RESPONSE_HEADERS);
579+
if (responseHeaders != null && !responseHeaders.isEmpty()) {
580+
final List<String> filteredHeaders = new ArrayList<>();
581+
for (String header : responseHeaders) {
582+
final String trimmedHeader = header.trim();
583+
if (!trimmedHeader.isEmpty()) {
584+
filteredHeaders.add(trimmedHeader);
585+
}
586+
}
587+
if (!filteredHeaders.isEmpty()) {
588+
options.getSessionReplay().setNetworkResponseHeaders(filteredHeaders);
589+
}
590+
}
591+
}
592+
491593
options.setIgnoredErrors(readList(metadata, logger, IGNORED_ERRORS));
492594

493595
final @Nullable List<String> includes = readList(metadata, logger, IN_APP_INCLUDES);

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

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,4 +1882,250 @@ class ManifestMetadataReaderTest {
18821882
fixture.options.sessionReplay.screenshotStrategy,
18831883
)
18841884
}
1885+
1886+
// Network Detail Configuration Tests
1887+
1888+
@Test
1889+
fun `applyMetadata reads comma-separated networkDetailAllowUrls from manifest`() {
1890+
// Arrange
1891+
val expectedUrls = "https://api.example.com/.*,https://cdn.example.com/.*"
1892+
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_NETWORK_DETAIL_ALLOW_URLS to expectedUrls)
1893+
val context = fixture.getContext(metaData = bundle)
1894+
1895+
// Act
1896+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1897+
1898+
// Assert
1899+
val urls = fixture.options.sessionReplay.networkDetailAllowUrls
1900+
assertEquals(2, urls.size)
1901+
assertEquals("https://api.example.com/.*", urls[0])
1902+
assertEquals("https://cdn.example.com/.*", urls[1])
1903+
}
1904+
1905+
@Test
1906+
fun `applyMetadata keeps empty networkDetailAllowUrls when not present`() {
1907+
// Arrange
1908+
val context = fixture.getContext()
1909+
1910+
// Act
1911+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1912+
1913+
// Assert
1914+
assertEquals(0, fixture.options.sessionReplay.networkDetailAllowUrls.size)
1915+
}
1916+
1917+
@Test
1918+
fun `applyMetadata reads comma-separated networkDetailDenyUrls from manifest`() {
1919+
// Arrange
1920+
val expectedUrls = "https://private.example.com/.*,https://internal.example.com/.*"
1921+
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_NETWORK_DETAIL_DENY_URLS to expectedUrls)
1922+
val context = fixture.getContext(metaData = bundle)
1923+
1924+
// Act
1925+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1926+
1927+
// Assert
1928+
val urls = fixture.options.sessionReplay.networkDetailDenyUrls
1929+
assertEquals(2, urls.size)
1930+
assertEquals("https://private.example.com/.*", urls[0])
1931+
assertEquals("https://internal.example.com/.*", urls[1])
1932+
}
1933+
1934+
@Test
1935+
fun `applyMetadata keeps empty networkDetailDenyUrls when not present`() {
1936+
// Arrange
1937+
val context = fixture.getContext()
1938+
1939+
// Act
1940+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1941+
1942+
// Assert
1943+
assertEquals(0, fixture.options.sessionReplay.networkDetailDenyUrls.size)
1944+
}
1945+
1946+
@Test
1947+
fun `applyMetadata reads networkCaptureBodies from manifest`() {
1948+
// Arrange
1949+
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_NETWORK_CAPTURE_BODIES to false)
1950+
val context = fixture.getContext(metaData = bundle)
1951+
1952+
// Act
1953+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1954+
1955+
// Assert
1956+
assertFalse(fixture.options.sessionReplay.isNetworkCaptureBodies)
1957+
}
1958+
1959+
@Test
1960+
fun `applyMetadata keeps default networkCaptureBodies as true when not present`() {
1961+
// Arrange
1962+
val context = fixture.getContext()
1963+
1964+
// Act
1965+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1966+
1967+
// Assert
1968+
assertTrue(fixture.options.sessionReplay.isNetworkCaptureBodies)
1969+
}
1970+
1971+
@Test
1972+
fun `applyMetadata keeps the default networkRequestHeaders`() {
1973+
// Arrange
1974+
val context = fixture.getContext()
1975+
1976+
// Act
1977+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1978+
1979+
// Assert
1980+
val headers = fixture.options.sessionReplay.networkRequestHeaders
1981+
val defaultHeaders = SentryReplayOptions.getNetworkDetailsDefaultHeaders()
1982+
1983+
// Should have exactly the default headers
1984+
assertEquals(defaultHeaders.size, headers.size)
1985+
defaultHeaders.forEach { defaultHeader -> assertTrue(headers.contains(defaultHeader)) }
1986+
}
1987+
1988+
@Test
1989+
fun `applyMetadata reads networkRequestHeaders from manifest`() {
1990+
// Arrange
1991+
val expectedHeaders = "Authorization,X-Custom-Header,X-Request-Id"
1992+
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_NETWORK_REQUEST_HEADERS to expectedHeaders)
1993+
val context = fixture.getContext(metaData = bundle)
1994+
1995+
// Act
1996+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1997+
1998+
// Assert
1999+
val allHeaders = fixture.options.sessionReplay.networkRequestHeaders
2000+
val defaultHeaders = SentryReplayOptions.getNetworkDetailsDefaultHeaders()
2001+
2002+
// Should include default headers + additional headers
2003+
defaultHeaders.forEach { defaultHeader ->
2004+
assertTrue(allHeaders.contains(defaultHeader)) // default
2005+
}
2006+
assertTrue(allHeaders.contains("Authorization")) // additional
2007+
assertTrue(allHeaders.contains("X-Custom-Header")) // additional
2008+
assertTrue(allHeaders.contains("X-Request-Id")) // additional
2009+
}
2010+
2011+
@Test
2012+
fun `applyMetadata keeps the default networkResponseHeaders`() {
2013+
// Arrange
2014+
val context = fixture.getContext()
2015+
2016+
// Act
2017+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2018+
2019+
// Assert
2020+
val headers = fixture.options.sessionReplay.networkResponseHeaders
2021+
val defaultHeaders = SentryReplayOptions.getNetworkDetailsDefaultHeaders()
2022+
2023+
// Should have exactly the default headers
2024+
assertEquals(defaultHeaders.size, headers.size)
2025+
defaultHeaders.forEach { defaultHeader -> assertTrue(headers.contains(defaultHeader)) }
2026+
}
2027+
2028+
@Test
2029+
fun `applyMetadata reads networkResponseHeaders from manifest`() {
2030+
// Arrange
2031+
val expectedHeaders = "X-Response-Time,X-Cache-Status,X-Server-Id"
2032+
val bundle =
2033+
bundleOf(ManifestMetadataReader.REPLAYS_NETWORK_RESPONSE_HEADERS to expectedHeaders)
2034+
val context = fixture.getContext(metaData = bundle)
2035+
2036+
// Act
2037+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2038+
2039+
// Assert
2040+
val allHeaders = fixture.options.sessionReplay.networkResponseHeaders
2041+
// Should include default headers + additional headers
2042+
val defaultHeaders = SentryReplayOptions.getNetworkDetailsDefaultHeaders()
2043+
defaultHeaders.forEach { defaultHeader -> assertTrue(allHeaders.contains(defaultHeader)) }
2044+
assertTrue(allHeaders.contains("X-Response-Time")) // additional
2045+
assertTrue(allHeaders.contains("X-Cache-Status")) // additional
2046+
assertTrue(allHeaders.contains("X-Server-Id")) // additional
2047+
}
2048+
2049+
@Test
2050+
fun `applyMetadata skips empty strings for networkDetailAllowUrls and networkDetailDenyUrls`() {
2051+
// Arrange
2052+
val bundle =
2053+
bundleOf(
2054+
ManifestMetadataReader.REPLAYS_NETWORK_DETAIL_ALLOW_URLS to ", ",
2055+
ManifestMetadataReader.REPLAYS_NETWORK_DETAIL_DENY_URLS to " ,, ",
2056+
)
2057+
val context = fixture.getContext(metaData = bundle)
2058+
2059+
// Act
2060+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2061+
2062+
// Assert
2063+
assertEquals(0, fixture.options.sessionReplay.networkDetailAllowUrls.size)
2064+
assertEquals(0, fixture.options.sessionReplay.networkDetailDenyUrls.size)
2065+
}
2066+
2067+
@Test
2068+
fun `applyMetadata skips empty strings for networkRequestHeaders and networkResponseHeaders`() {
2069+
// Arrange
2070+
val bundle =
2071+
bundleOf(
2072+
ManifestMetadataReader.REPLAYS_NETWORK_REQUEST_HEADERS to ",",
2073+
ManifestMetadataReader.REPLAYS_NETWORK_RESPONSE_HEADERS to " ,",
2074+
)
2075+
val context = fixture.getContext(metaData = bundle)
2076+
2077+
// Act
2078+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2079+
2080+
// Assert
2081+
// Should still have default headers even with empty string
2082+
val defaultHeaders = SentryReplayOptions.getNetworkDetailsDefaultHeaders()
2083+
2084+
val requestHeaders = fixture.options.sessionReplay.networkRequestHeaders
2085+
assertEquals(defaultHeaders.size, requestHeaders.size)
2086+
defaultHeaders.forEach { defaultHeader -> assertTrue(requestHeaders.contains(defaultHeader)) }
2087+
2088+
val responseHeaders = fixture.options.sessionReplay.networkResponseHeaders
2089+
assertEquals(defaultHeaders.size, responseHeaders.size)
2090+
defaultHeaders.forEach { defaultHeader -> assertTrue(responseHeaders.contains(defaultHeader)) }
2091+
}
2092+
2093+
@Test
2094+
fun `applyMetadata trims whitespace from network URLs`() {
2095+
// Arrange
2096+
val bundle =
2097+
bundleOf(
2098+
ManifestMetadataReader.REPLAYS_NETWORK_DETAIL_ALLOW_URLS to
2099+
" https://api.example.com/.* , https://cdn.example.com/.* "
2100+
)
2101+
val context = fixture.getContext(metaData = bundle)
2102+
2103+
// Act
2104+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2105+
2106+
// Assert
2107+
val urls = fixture.options.sessionReplay.networkDetailAllowUrls
2108+
assertEquals(2, urls.size)
2109+
assertEquals("https://api.example.com/.*", urls[0])
2110+
assertEquals("https://cdn.example.com/.*", urls[1])
2111+
}
2112+
2113+
@Test
2114+
fun `applyMetadata trims whitespace from network headers`() {
2115+
// Arrange
2116+
val bundle =
2117+
bundleOf(
2118+
ManifestMetadataReader.REPLAYS_NETWORK_REQUEST_HEADERS to
2119+
" Authorization , X-Custom-Header "
2120+
)
2121+
val context = fixture.getContext(metaData = bundle)
2122+
2123+
// Act
2124+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2125+
2126+
// Assert
2127+
val headers = fixture.options.sessionReplay.networkRequestHeaders
2128+
assertTrue(headers.contains("Authorization"))
2129+
assertTrue(headers.contains("X-Custom-Header"))
2130+
}
18852131
}

0 commit comments

Comments
 (0)