Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158))
- Make the tombstone merge time threshold configurable via `SentryAndroidOptions.setTombstoneMergeTimeThresholdMillis` and the `io.sentry.tombstone.merge-time-threshold-millis` manifest option ([#6154](https://github.com/getsentry/sentry-java/pull/6154))
- Add `sentry-apollo-5` integration for Apollo Kotlin 5, providing HTTP tracing and failed GraphQL request reporting ([#6074](https://github.com/getsentry/sentry-java/pull/6074))
- Add OkHttp autoconfiguration for Spring Boot ([#5797](https://github.com/getsentry/sentry-java/pull/5797))

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.sentry.IScopes
import io.sentry.ISpan
import io.sentry.SentryDate
import io.sentry.SpanDataConvention
import io.sentry.SpanOptions
import io.sentry.TypeCheckHint
import io.sentry.transport.CurrentDateProvider
import io.sentry.util.Platform
Expand Down Expand Up @@ -40,8 +41,8 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques

// We start the call span that will contain all the others
val parentSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span
callSpan = parentSpan?.startChild("http.client")
callSpan?.spanContext?.origin = TRACE_ORIGIN
callSpan =
parentSpan?.startChild("http.client", null, SpanOptions().apply { origin = TRACE_ORIGIN })

breadcrumb =
Breadcrumb().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS
import io.sentry.SentryReplayOptions
import io.sentry.SpanDataConvention
import io.sentry.SpanOptions
import io.sentry.SpanStatus
import io.sentry.TypeCheckHint.OKHTTP_REQUEST
import io.sentry.TypeCheckHint.OKHTTP_RESPONSE
Expand Down Expand Up @@ -96,13 +97,16 @@ public open class SentryOkHttpInterceptor(
// read the span from the bound scope
okHttpEvent = null
val parentSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span
span = parentSpan?.startChild("http.client", "$method $url")
span =
parentSpan?.startChild(
"http.client",
"$method $url",
SpanOptions().apply { origin = TRACE_ORIGIN },
)
}

val startTimestamp = CurrentDateProvider.getInstance().currentTimeMillis

span?.spanContext?.origin = TRACE_ORIGIN

urlDetails.applyToSpan(span)

val isFromEventListener = okHttpEvent != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,21 @@ class SentryOkHttpEventTest {
val callSpan = sut.callSpan
assertNotNull(callSpan)
assertEquals("http.client", callSpan.operation)
assertEquals("auto.http.okhttp", callSpan.spanContext.origin)
assertEquals("${fixture.mockRequest.method} ${fixture.mockRequest.url}", callSpan.description)
assertEquals(fixture.mockRequest.url.toString(), callSpan.getData("url"))
assertEquals(fixture.mockRequest.url.host, callSpan.getData("host"))
assertEquals(fixture.mockRequest.url.encodedPath, callSpan.getData("path"))
assertEquals(fixture.mockRequest.method, callSpan.getData(SpanDataConvention.HTTP_METHOD_KEY))
}

@Test
fun `when span origin is ignored, call span is a no-op`() {
fixture.scopes.options.setIgnoredSpanOrigins(listOf("auto.http.okhttp"))
val sut = fixture.getSut()
assertTrue(sut.callSpan!!.isNoOp)
}

@Test
fun `when call span is null, breadcrumb is created anyway`() {
val sut = fixture.getSut(currentSpan = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ class SentryOkHttpInterceptorTest {
assertTrue(httpClientSpan.isFinished)
}

@Test
fun `does not create a span when span origin is ignored`() {
val sut = fixture.getSut { options ->
options.setIgnoredSpanOrigins(listOf("auto.http.okhttp"))
}
sut.newCall(getRequest()).execute()
assertEquals(0, fixture.sentryTracer.children.size)
}

@Test
fun `maps http status code to SpanStatus`() {
val sut = fixture.getSut(httpStatusCode = 400)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ dependencies {
implementation(libs.springboot4.starter.kafka)
implementation(projects.sentryKafka)

// okhttp client instrumentation
implementation(projects.sentryOkhttp)
implementation(libs.okhttp)

// database query tracing
implementation(projects.sentryJdbc)
runtimeOnly(libs.hsqldb)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import io.sentry.samples.spring.boot4.quartz.SampleJob;
import java.util.Collections;
import okhttp3.OkHttpClient;
import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.springframework.boot.SpringApplication;
Expand Down Expand Up @@ -42,6 +43,12 @@ RestClient restClient(RestClient.Builder builder) {
return builder.build();
}

@Bean
OkHttpClient okHttpClient() {
// automatically instrumented by Sentry via sentry.clients.ok-http-enabled=true
return new OkHttpClient.Builder().build();
}

@Bean
public JobDetailFactoryBean jobDetail() {
JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package io.sentry.samples.spring.boot4;

import io.sentry.reactor.SentryReactorUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -10,17 +16,27 @@
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import tools.jackson.databind.ObjectMapper;

@RestController
public class TodoController {
private final RestTemplate restTemplate;
private final WebClient webClient;
private final RestClient restClient;
private final OkHttpClient okHttpClient;
private final ObjectMapper objectMapper;

public TodoController(RestTemplate restTemplate, WebClient webClient, RestClient restClient) {
public TodoController(
RestTemplate restTemplate,
WebClient webClient,
RestClient restClient,
OkHttpClient okHttpClient,
ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.webClient = webClient;
this.restClient = restClient;
this.okHttpClient = okHttpClient;
this.objectMapper = objectMapper;
}

@GetMapping("/todo/{id}")
Expand Down Expand Up @@ -54,4 +70,19 @@ Todo todoRestClient(@PathVariable Long id) {
.retrieve()
.body(Todo.class);
}

@GetMapping("/todo-okhttp/{id}")
Todo todoOkHttp(@PathVariable Long id) {
final Request request =
new Request.Builder().url("https://jsonplaceholder.typicode.com/todos/" + id).build();
try (Response response = okHttpClient.newCall(request).execute()) {
final ResponseBody body = response.body();
if (body == null) {
throw new IllegalStateException("Response body was null");
}
return objectMapper.readValue(body.byteStream(), Todo.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ sentry.profile-session-sample-rate=1.0
sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces
sentry.profile-lifecycle=TRACE
sentry.enable-cache-tracing=true
# Automatically instrument Spring-managed OkHttpClient beans
sentry.clients.ok-http-enabled=true
spring.cache.cache-names=todos
spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,20 @@ class TodoSystemTest {
)
}
}

@Test
fun `get todo okhttp works`() {
val restClient = testHelper.restClient
restClient.getTodoOkHttp(1L)
assertEquals(200, restClient.lastKnownStatusCode)

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
transaction.transaction == "GET /todo-okhttp/{id}" &&
testHelper.doesTransactionContainSpanWithOpAndDescription(
transaction,
"http.client",
"GET https://jsonplaceholder.typicode.com/todos/1",
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ dependencies {
// OpenFeature SDK
implementation(libs.openfeature)

// okhttp client instrumentation
implementation(projects.sentryOkhttp)
implementation(libs.okhttp)

// database query tracing
implementation(projects.sentryJdbc)
runtimeOnly(libs.hsqldb)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob;
import java.util.Collections;
import okhttp3.OkHttpClient;
import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.springframework.boot.SpringApplication;
Expand Down Expand Up @@ -42,6 +43,12 @@ RestClient restClient(RestClient.Builder builder) {
return builder.build();
}

@Bean
OkHttpClient okHttpClient() {
// automatically instrumented by Sentry via sentry.clients.ok-http-enabled=true
return new OkHttpClient.Builder().build();
}

@Bean
public JobDetailFactoryBean jobDetail() {
JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package io.sentry.samples.spring.boot.jakarta;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.sentry.reactor.SentryReactorUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -16,11 +23,20 @@ public class TodoController {
private final RestTemplate restTemplate;
private final WebClient webClient;
private final RestClient restClient;
private final OkHttpClient okHttpClient;
private final ObjectMapper objectMapper;

public TodoController(RestTemplate restTemplate, WebClient webClient, RestClient restClient) {
public TodoController(
RestTemplate restTemplate,
WebClient webClient,
RestClient restClient,
OkHttpClient okHttpClient,
ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.webClient = webClient;
this.restClient = restClient;
this.okHttpClient = okHttpClient;
this.objectMapper = objectMapper;
}

@GetMapping("/todo/{id}")
Expand Down Expand Up @@ -54,4 +70,19 @@ Todo todoRestClient(@PathVariable Long id) {
.retrieve()
.body(Todo.class);
}

@GetMapping("/todo-okhttp/{id}")
Todo todoOkHttp(@PathVariable Long id) {
final Request request =
new Request.Builder().url("https://jsonplaceholder.typicode.com/todos/" + id).build();
try (Response response = okHttpClient.newCall(request).execute()) {
final ResponseBody body = response.body();
if (body == null) {
throw new IllegalStateException("Response body was null");
}
return objectMapper.readValue(body.byteStream(), Todo.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ spring.quartz.job-store-type=memory

# Cache tracing
sentry.enable-cache-tracing=true
# Automatically instrument Spring-managed OkHttpClient beans
sentry.clients.ok-http-enabled=true

spring.cache.cache-names=todos
spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,20 @@ class TodoSystemTest {
)
}
}

@Test
fun `get todo okhttp works`() {
val restClient = testHelper.restClient
restClient.getTodoOkHttp(1L)
assertEquals(200, restClient.lastKnownStatusCode)

testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
transaction.transaction == "GET /todo-okhttp/{id}" &&
testHelper.doesTransactionContainSpanWithOpAndDescription(
transaction,
"http.client",
"GET https://jsonplaceholder.typicode.com/todos/1",
)
}
}
}
4 changes: 4 additions & 0 deletions sentry-samples/sentry-samples-spring-boot/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ dependencies {
implementation(projects.sentryQuartz)
implementation(projects.sentryAsyncProfiler)

// okhttp client instrumentation
implementation(projects.sentryOkhttp)
implementation(libs.okhttp)

// database query tracing
implementation(projects.sentryJdbc)
runtimeOnly(libs.hsqldb)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import io.sentry.samples.spring.boot.quartz.SampleJob;
import java.util.Collections;
import okhttp3.OkHttpClient;
import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.springframework.boot.SpringApplication;
Expand Down Expand Up @@ -36,6 +37,12 @@ WebClient webClient(WebClient.Builder builder) {
return builder.build();
}

@Bean
OkHttpClient okHttpClient() {
// automatically instrumented by Sentry via sentry.clients.ok-http-enabled=true
return new OkHttpClient.Builder().build();
}

@Bean
public JobDetailFactoryBean jobDetail() {
JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
Expand Down
Loading
Loading