Skip to content

Commit dcdb1fa

Browse files
authored
Merge branch 'main' into dependabot/github_actions/actions/checkout-6
2 parents 43bcd3f + a0991f1 commit dcdb1fa

File tree

31 files changed

+531
-165
lines changed

31 files changed

+531
-165
lines changed

CHANGELOG.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,81 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Add option to capture additional OkHttp network request/response details in session replays ([#4919](https://github.com/getsentry/sentry-java/pull/4919))
8+
- Depends on `SentryOkHttpInterceptor` to intercept the request and extract request/response bodies
9+
- To enable, add url regexes via the `io.sentry.session-replay.network-detail-allow-urls` metadata tag in AndroidManifest ([code sample](https://github.com/getsentry/sentry-java/blob/b03edbb1b0d8b871c62a09bc02cbd8a4e1f6fea1/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml#L196-L205))
10+
- Or you can manually specify SentryReplayOptions via `SentryAndroid#init`:
11+
_(Make sure you disable the auto init via manifest meta-data: io.sentry.auto-init=false)_
12+
13+
<details>
14+
<summary>Kotlin</summary>
15+
16+
```kotlin
17+
SentryAndroid.init(
18+
this,
19+
options -> {
20+
// options.dsn = "https://examplePublicKey@o0.ingest.sentry.io/0"
21+
// options.sessionReplay.sessionSampleRate = 1.0
22+
// options.sessionReplay.onErrorSampleRate = 1.0
23+
// ..
24+
25+
options.sessionReplay.networkDetailAllowUrls = listOf(".*")
26+
options.sessionReplay.networkDetailDenyUrls = listOf(".*deny.*")
27+
options.sessionReplay.networkRequestHeaders = listOf("Authorization", "X-Custom-Header", "X-Test-Request")
28+
options.sessionReplay.networkResponseHeaders = listOf("X-Response-Time", "X-Cache-Status", "X-Test-Response")
29+
});
30+
```
31+
32+
</details>
33+
34+
<details>
35+
<summary>Java</summary>
36+
37+
```java
38+
SentryAndroid.init(
39+
this,
40+
options -> {
41+
options.getSessionReplay().setNetworkDetailAllowUrls(Arrays.asList(".*"));
42+
options.getSessionReplay().setNetworkDetailDenyUrls(Arrays.asList(".*deny.*"));
43+
options.getSessionReplay().setNetworkRequestHeaders(
44+
Arrays.asList("Authorization", "X-Custom-Header", "X-Test-Request"));
45+
options.getSessionReplay().setNetworkResponseHeaders(
46+
Arrays.asList("X-Response-Time", "X-Cache-Status", "X-Test-Response"));
47+
});
48+
49+
```
50+
51+
</details>
52+
53+
54+
### Improvements
55+
56+
- Avoid forking `rootScopes` for Reactor if current thread has `NoOpScopes` ([#4793](https://github.com/getsentry/sentry-java/pull/4793))
57+
- This reduces the SDKs overhead by avoiding unnecessary scope forks
58+
59+
### Fixes
60+
61+
- Fix missing thread stacks for ANRv1 events ([#4918](https://github.com/getsentry/sentry-java/pull/4918))
62+
63+
### Internal
64+
65+
- Support `span` envelope item type ([#4935](https://github.com/getsentry/sentry-java/pull/4935))
66+
67+
### Dependencies
68+
69+
- Bump Native SDK from v0.12.1 to v0.12.2 ([#4944](https://github.com/getsentry/sentry-java/pull/4944))
70+
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0122)
71+
- [diff](https://github.com/getsentry/sentry-native/compare/0.12.1...0.12.2)
72+
73+
## 8.27.1
74+
75+
### Fixes
76+
77+
- Do not log if `sentry.properties` in rundir has not been found ([#4929](https://github.com/getsentry/sentry-java/pull/4929))
78+
379
## 8.27.0
480

581
### Features

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
1111
android.useAndroidX=true
1212

1313
# Release information
14-
versionName=8.27.0
14+
versionName=8.27.1
1515

1616
# Override the SDK name on native crashes on Android
1717
sentryAndroidSdkName=sentry.native.android

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" }
142142
reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" }
143143
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
144144
retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
145-
sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.12.1" }
145+
sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.12.2" }
146146
servlet-api = { module = "javax.servlet:javax.servlet-api", version = "3.1.0" }
147147
servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.1.0" }
148148
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ static void applyMetadata(
507507
}
508508

509509
// Network Details Configuration
510-
if (options.getSessionReplay().getNetworkDetailAllowUrls().length == 0) {
510+
if (options.getSessionReplay().getNetworkDetailAllowUrls().isEmpty()) {
511511
final @Nullable List<String> allowUrls =
512512
readList(metadata, logger, REPLAYS_NETWORK_DETAIL_ALLOW_URLS);
513513
if (allowUrls != null && !allowUrls.isEmpty()) {
@@ -519,14 +519,12 @@ static void applyMetadata(
519519
}
520520
}
521521
if (!filteredUrls.isEmpty()) {
522-
options
523-
.getSessionReplay()
524-
.setNetworkDetailAllowUrls(filteredUrls.toArray(new String[0]));
522+
options.getSessionReplay().setNetworkDetailAllowUrls(filteredUrls);
525523
}
526524
}
527525
}
528526

529-
if (options.getSessionReplay().getNetworkDetailDenyUrls().length == 0) {
527+
if (options.getSessionReplay().getNetworkDetailDenyUrls().isEmpty()) {
530528
final @Nullable List<String> denyUrls =
531529
readList(metadata, logger, REPLAYS_NETWORK_DETAIL_DENY_URLS);
532530
if (denyUrls != null && !denyUrls.isEmpty()) {
@@ -538,9 +536,7 @@ static void applyMetadata(
538536
}
539537
}
540538
if (!filteredUrls.isEmpty()) {
541-
options
542-
.getSessionReplay()
543-
.setNetworkDetailDenyUrls(filteredUrls.toArray(new String[0]));
539+
options.getSessionReplay().setNetworkDetailDenyUrls(filteredUrls);
544540
}
545541
}
546542
}
@@ -554,7 +550,7 @@ static void applyMetadata(
554550
REPLAYS_NETWORK_CAPTURE_BODIES,
555551
options.getSessionReplay().isNetworkCaptureBodies() /* defaultValue */));
556552

557-
if (options.getSessionReplay().getNetworkRequestHeaders().length
553+
if (options.getSessionReplay().getNetworkRequestHeaders().size()
558554
== SentryReplayOptions.getNetworkDetailsDefaultHeaders().size()) { // Only has defaults
559555
final @Nullable List<String> requestHeaders =
560556
readList(metadata, logger, REPLAYS_NETWORK_REQUEST_HEADERS);
@@ -572,7 +568,7 @@ static void applyMetadata(
572568
}
573569
}
574570

575-
if (options.getSessionReplay().getNetworkResponseHeaders().length
571+
if (options.getSessionReplay().getNetworkResponseHeaders().size()
576572
== SentryReplayOptions.getNetworkDetailsDefaultHeaders().size()) { // Only has defaults
577573
final @Nullable List<String> responseHeaders =
578574
readList(metadata, logger, REPLAYS_NETWORK_RESPONSE_HEADERS);

sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import io.sentry.TypeCheckHint
1010
import io.sentry.transport.CurrentDateProvider
1111
import io.sentry.util.Platform
1212
import io.sentry.util.UrlUtils
13+
import io.sentry.util.network.NetworkRequestData
1314
import java.util.concurrent.ConcurrentHashMap
1415
import java.util.concurrent.TimeUnit
1516
import java.util.concurrent.atomic.AtomicBoolean
@@ -27,6 +28,7 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques
2728
internal val callSpan: ISpan?
2829
private var response: Response? = null
2930
private var clientErrorResponse: Response? = null
31+
private var networkDetails: NetworkRequestData? = null
3032
internal val isEventFinished = AtomicBoolean(false)
3133
private var url: String
3234
private var method: String
@@ -135,6 +137,11 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques
135137
}
136138
}
137139

140+
/** Sets the [NetworkRequestData] for network detail capture. */
141+
fun setNetworkDetails(networkRequestData: NetworkRequestData?) {
142+
this.networkDetails = networkRequestData
143+
}
144+
138145
/** Record event start if the callRootSpan is not null. */
139146
fun onEventStart(event: String) {
140147
callSpan ?: return
@@ -163,6 +170,9 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques
163170
hint.set(TypeCheckHint.OKHTTP_REQUEST, request)
164171
response?.let { hint.set(TypeCheckHint.OKHTTP_RESPONSE, it) }
165172

173+
// Include network details in the hint for session replay
174+
networkDetails?.let { hint.set(TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS, it) }
175+
166176
// needs this as unix timestamp for rrweb
167177
breadcrumb.setData(
168178
SpanDataConvention.HTTP_END_TIMESTAMP,

sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import okhttp3.Interceptor
3333
import okhttp3.Request
3434
import okhttp3.RequestBody.Companion.toRequestBody
3535
import okhttp3.Response
36+
import org.jetbrains.annotations.VisibleForTesting
3637

3738
/**
3839
* The Sentry's [SentryOkHttpInterceptor], it will automatically add a breadcrumb and start a span
@@ -209,6 +210,9 @@ public open class SentryOkHttpInterceptor(
209210
)
210211
}
211212

213+
// Set network details on the OkHttpEvent so it can include them in the breadcrumb hint
214+
okHttpEvent?.setNetworkDetails(networkDetailData)
215+
212216
finishSpan(span, request, response, isFromEventListener, okHttpEvent)
213217

214218
// The SentryOkHttpEventListener will send the breadcrumb itself if used for this call
@@ -260,10 +264,19 @@ public open class SentryOkHttpInterceptor(
260264
}
261265

262266
/** Extracts headers from OkHttp Headers object into a map */
263-
private fun okhttp3.Headers.toMap(): Map<String, String> {
267+
@VisibleForTesting
268+
internal fun okhttp3.Headers.toMap(): Map<String, String> {
264269
val headers = linkedMapOf<String, String>()
265270
for (i in 0 until size) {
266-
headers[name(i)] = value(i)
271+
val name = name(i)
272+
val value = value(i)
273+
val existingValue = headers[name]
274+
if (existingValue != null) {
275+
// Concatenate duplicate headers with semicolon separator
276+
headers[name] = "$existingValue; $value"
277+
} else {
278+
headers[name] = value
279+
}
267280
}
268281
return headers
269282
}

sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import io.sentry.TransactionContext
1515
import io.sentry.TypeCheckHint
1616
import io.sentry.exception.SentryHttpClientException
1717
import io.sentry.test.getProperty
18+
import io.sentry.util.network.NetworkRequestData
1819
import kotlin.test.Test
1920
import kotlin.test.assertEquals
2021
import kotlin.test.assertFalse
@@ -425,6 +426,34 @@ class SentryOkHttpEventTest {
425426
verify(fixture.scopes, never()).captureEvent(any(), any<Hint>())
426427
}
427428

429+
@Test
430+
fun `when finish is called, the breadcrumb sent includes network details data on its hint`() {
431+
val sut = fixture.getSut()
432+
val networkRequestData = NetworkRequestData("GET")
433+
434+
sut.setNetworkDetails(networkRequestData)
435+
sut.finish()
436+
437+
verify(fixture.scopes)
438+
.addBreadcrumb(
439+
any<Breadcrumb>(),
440+
check { assertEquals(networkRequestData, it[TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS]) },
441+
)
442+
}
443+
444+
@Test
445+
fun `when setNetworkDetails is not called, no network details data is captured`() {
446+
val sut = fixture.getSut()
447+
448+
sut.finish()
449+
450+
verify(fixture.scopes)
451+
.addBreadcrumb(
452+
any<Breadcrumb>(),
453+
check { assertNull(it[TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS]) },
454+
)
455+
}
456+
428457
/** Retrieve all the spans started in the event using reflection. */
429458
private fun SentryOkHttpEvent.getEventDates() =
430459
getProperty<MutableMap<String, SentryDate>>("eventDates")

sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,4 +680,39 @@ class SentryOkHttpInterceptorTest {
680680
assertNotNull(recordedRequest.getHeader(SentryTraceHeader.SENTRY_TRACE_HEADER))
681681
assertNull(recordedRequest.getHeader(W3CTraceparentHeader.TRACEPARENT_HEADER))
682682
}
683+
684+
@Test
685+
fun `toMap handles duplicate headers correctly`() {
686+
// Create a response with duplicate headers
687+
val mockResponse =
688+
MockResponse()
689+
.setResponseCode(200)
690+
.setBody("test")
691+
.addHeader("Set-Cookie", "sessionId=123")
692+
.addHeader("Set-Cookie", "userId=456")
693+
.addHeader("Set-Cookie", "theme=dark")
694+
.addHeader("Accept", "text/html")
695+
.addHeader("Accept", "application/json")
696+
.addHeader("Single-Header", "value")
697+
698+
fixture.server.enqueue(mockResponse)
699+
700+
// Execute request to get response with headers
701+
val sut = fixture.getSut()
702+
val response = sut.newCall(getRequest()).execute()
703+
val headers = response.headers
704+
705+
// Optional: verify OkHttp preserves duplicate headers
706+
assertEquals(3, headers.values("Set-Cookie").size)
707+
assertEquals(2, headers.values("Accept").size)
708+
assertEquals(1, headers.values("Single-Header").size)
709+
710+
val interceptor = SentryOkHttpInterceptor(fixture.scopes)
711+
val headerMap = with(interceptor) { headers.toMap() }
712+
713+
// Duplicate headers will be collapsed into 1 concatenated entry with "; " separator
714+
assertEquals("sessionId=123; userId=456; theme=dark", headerMap["Set-Cookie"])
715+
assertEquals("text/html; application/json", headerMap["Accept"])
716+
assertEquals("value", headerMap["Single-Header"])
717+
}
683718
}

sentry-reactor/src/main/java/io/sentry/reactor/SentryReactorThreadLocalAccessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public Object key() {
1616

1717
@Override
1818
public IScopes getValue() {
19-
return Sentry.getCurrentScopes();
19+
return Sentry.getCurrentScopes(false);
2020
}
2121

2222
@Override

sentry-samples/sentry-samples-android/src/main/cpp/native-sample.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
#include <jni.h>
22
#include <android/log.h>
33
#include <sentry.h>
4+
#include <signal.h>
45

56
#define TAG "sentry-sample"
67

78
extern "C" {
89

910
JNIEXPORT void JNICALL Java_io_sentry_samples_android_NativeSample_crash(JNIEnv *env, jclass cls) {
1011
__android_log_print(ANDROID_LOG_WARN, TAG, "About to crash.");
11-
char *ptr = 0;
12-
*ptr += 1;
12+
raise(SIGSEGV);
1313
}
1414

1515
JNIEXPORT void JNICALL Java_io_sentry_samples_android_NativeSample_message(JNIEnv *env, jclass cls) {

0 commit comments

Comments
 (0)