Skip to content
Open
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

### Fixes

- Release `MediaMuxer` when a replay segment has no encodable frames to avoid a resource leak ([#5583](https://github.com/getsentry/sentry-java/pull/5583))
- Session Replay: Fix network detail response body size being unknown for gzip-compressed responses ([#5592](https://github.com/getsentry/sentry-java/pull/5592))
- Session Replay: Release `MediaMuxer` when a replay segment has no encodable frames to avoid a resource leak ([#5583](https://github.com/getsentry/sentry-java/pull/5583))

## 8.44.1

Expand Down
2 changes: 2 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -7975,7 +7975,9 @@ public final class io/sentry/util/UrlUtils$UrlDetails {
public final class io/sentry/util/network/NetworkBody {
public fun <init> (Ljava/lang/Object;)V
public fun <init> (Ljava/lang/Object;Ljava/util/List;)V
public fun <init> (Ljava/lang/Object;Ljava/util/List;J)V
public fun getBody ()Ljava/lang/Object;
public fun getOriginalByteCount ()J
public fun getWarnings ()Ljava/util/List;
public fun toString ()Ljava/lang/String;
}
Expand Down
15 changes: 14 additions & 1 deletion sentry/src/main/java/io/sentry/util/network/NetworkBody.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,24 @@ public final class NetworkBody {

private final @Nullable Object body;
private final @Nullable List<NetworkBodyWarning> warnings;
private final long originalByteCount;

public NetworkBody(final @Nullable Object body) {
this(body, null);
this(body, null, -1);
}

public NetworkBody(
final @Nullable Object body, final @Nullable List<NetworkBodyWarning> warnings) {
this(body, warnings, -1);
}

public NetworkBody(
final @Nullable Object body,
final @Nullable List<NetworkBodyWarning> warnings,
final long originalByteCount) {
this.body = body;
this.warnings = warnings;
this.originalByteCount = originalByteCount;
}

public @Nullable Object getBody() {
Expand All @@ -35,6 +44,10 @@ public NetworkBody(
return warnings;
}

public long getOriginalByteCount() {
return originalByteCount;
}

// Based on
// https://github.com/getsentry/sentry/blob/ccb61aa9b0f33e1333830093a5ce3bd5db88ef33/static/app/utils/replays/replay.tsx#L5-L12
public enum NetworkBodyWarning {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,33 @@ private NetworkBodyParser() {}
return null;
}

final boolean isTruncated = bytes.length > maxSizeBytes;
final long originalByteCount = bytes.length;

if (contentType != null && isBinaryContentType(contentType)) {
// For binary content, return a description instead of the actual content
return new NetworkBody(
"[Binary data, " + bytes.length + " bytes, type: " + contentType + "]");
"[Binary data, " + bytes.length + " bytes, type: " + contentType + "]",
null,
originalByteCount);
}

// Convert to string and parse
try {
final String effectiveCharset = charset != null ? charset : "UTF-8";
final int size = Math.min(bytes.length, maxSizeBytes);
final boolean isPartial = bytes.length > maxSizeBytes;
final String content = new String(bytes, 0, size, effectiveCharset);
return parse(content, contentType, isPartial, logger);
final NetworkBody parsed = parse(content, contentType, isTruncated, logger);
if (parsed == null) {
return null;
}
return new NetworkBody(parsed.getBody(), parsed.getWarnings(), originalByteCount);
} catch (UnsupportedEncodingException e) {
logger.log(SentryLevel.WARNING, "Failed to decode bytes: " + e.getMessage());
return new NetworkBody(
"[Failed to decode bytes, " + bytes.length + " bytes]",
Collections.singletonList(NetworkBody.NetworkBodyWarning.BODY_PARSE_ERROR));
Collections.singletonList(NetworkBody.NetworkBodyWarning.BODY_PARSE_ERROR),
originalByteCount);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,15 @@ private static boolean shouldCaptureUrl(
body = bodyExtractor.extract(httpObject);
}

// When contentLength is unknown (-1), use the actual byte count from body extraction
Long effectiveBodySize = bodySize;
if ((bodySize == null || bodySize == -1L) && body != null && body.getOriginalByteCount() >= 0) {
effectiveBodySize = body.getOriginalByteCount();
}

Map<String, String> headers =
getCaptureHeaders(headerExtractor.extract(httpObject), allowedHeaders);

return new ReplayNetworkRequestOrResponse(bodySize, body, headers);
return new ReplayNetworkRequestOrResponse(effectiveBodySize, body, headers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,27 @@ class NetworkBodyParserTest {
val body = NetworkBodyParser.fromBytes(bytes, "image/png", null, bytes.size, logger)
assertNotNull(body)
assertEquals("[Binary data, 100 bytes, type: image/png]", body.body)
assertEquals(100, body.originalByteCount)
}

@Test
fun `originalByteCount is set when body fits within limit`() {
val logger = mock<ILogger>()
val bytes = """{"key":"value"}""".toByteArray()

val body = NetworkBodyParser.fromBytes(bytes, "application/json", null, bytes.size, logger)
assertNotNull(body)
assertEquals(bytes.size.toLong(), body.originalByteCount)
}

@Test
fun `originalByteCount is set to capped size when body is truncated`() {
val logger = mock<ILogger>()
val bytes = """{"key":"value"}""".toByteArray()

val body = NetworkBodyParser.fromBytes(bytes, "application/json", null, bytes.size - 1, logger)
assertNotNull(body)
assertEquals(bytes.size.toLong(), body.originalByteCount)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,70 @@
package io.sentry.util.network

import io.sentry.ILogger
import java.util.LinkedHashMap
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.junit.Test
import org.mockito.kotlin.mock

class NetworkDetailCaptureUtilsTest {

@Test
fun `createResponse uses originalByteCount when bodySize is unknown`() {
val logger = mock<ILogger>()
val jsonBytes = """{"key":"value"}""".toByteArray()

val result =
NetworkDetailCaptureUtils.createResponse(
jsonBytes,
-1L,
true,
{ bytes ->
NetworkBodyParser.fromBytes(bytes, "application/json", null, bytes.size, logger)
},
emptyList(),
{ emptyMap() },
)

assertEquals(jsonBytes.size.toLong(), result.size)
}

@Test
fun `createResponse keeps explicit bodySize when available`() {
val logger = mock<ILogger>()
val jsonBytes = """{"key":"value"}""".toByteArray()

val result =
NetworkDetailCaptureUtils.createResponse(
jsonBytes,
42L,
true,
{ bytes ->
NetworkBodyParser.fromBytes(bytes, "application/json", null, bytes.size, logger)
},
emptyList(),
{ emptyMap() },
)

assertEquals(42L, result.size)
}

@Test
fun `createResponse keeps null bodySize when body capture is off`() {
val result =
NetworkDetailCaptureUtils.createResponse(
"unused",
null,
false,
{ null },
emptyList(),
{ emptyMap() },
)

assertNull(result.size)
}

@Test
fun `getCaptureHeaders should match headers case-insensitively`() {
// Setup: allHeaders with mixed case keys
Expand Down
Loading