From 282054940e6972cdcb5fd41233a6270be45f514b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 05:35:50 +0100 Subject: [PATCH 1/2] fix(spring): Use ValueWrapper to determine cache hit in typed get The get(key, type) method incorrectly used result != null to detect cache hits, failing to distinguish a miss from a cached null value. Now uses delegate.get(key) to check the ValueWrapper first. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../spring7/cache/SentryCacheWrapper.java | 3 +- .../spring7/cache/SentryCacheWrapperTest.kt | 34 +++++++++++++++++++ .../jakarta/cache/SentryCacheWrapper.java | 3 +- .../jakarta/cache/SentryCacheWrapperTest.kt | 34 +++++++++++++++++++ .../spring/cache/SentryCacheWrapper.java | 3 +- .../spring/cache/SentryCacheWrapperTest.kt | 34 +++++++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index bd04285362..58b803d40e 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -67,8 +67,9 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, type); } try { + final ValueWrapper wrapper = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null); final T result = delegate.get(key, type); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index 71ce645b37..d42fcabaeb 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -85,6 +85,8 @@ class SentryCacheWrapperTest { fun `get with type creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) whenever(delegate.get("myKey", String::class.java)).thenReturn("value") val result = wrapper.get("myKey", String::class.java) @@ -94,10 +96,26 @@ class SentryCacheWrapperTest { assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type creates span with cache hit true when cached value is null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) + whenever(delegate.get("myKey", String::class.java)).thenReturn(null) + + val result = wrapper.get("myKey", String::class.java) + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + @Test fun `get with type creates span with cache hit false on miss`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) whenever(delegate.get("myKey", String::class.java)).thenReturn(null) val result = wrapper.get("myKey", String::class.java) @@ -107,6 +125,22 @@ class SentryCacheWrapperTest { assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenReturn(mock()) + whenever(delegate.get("myKey", String::class.java)).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey", String::class.java) } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } + // -- get(Object key, Callable) -- @Test diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index 061669f678..79ea227dbe 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -67,8 +67,9 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, type); } try { + final ValueWrapper wrapper = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null); final T result = delegate.get(key, type); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index bddde75969..62cb26a5af 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -85,6 +85,8 @@ class SentryCacheWrapperTest { fun `get with type creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) whenever(delegate.get("myKey", String::class.java)).thenReturn("value") val result = wrapper.get("myKey", String::class.java) @@ -94,10 +96,26 @@ class SentryCacheWrapperTest { assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type creates span with cache hit true when cached value is null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) + whenever(delegate.get("myKey", String::class.java)).thenReturn(null) + + val result = wrapper.get("myKey", String::class.java) + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + @Test fun `get with type creates span with cache hit false on miss`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) whenever(delegate.get("myKey", String::class.java)).thenReturn(null) val result = wrapper.get("myKey", String::class.java) @@ -107,6 +125,22 @@ class SentryCacheWrapperTest { assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenReturn(mock()) + whenever(delegate.get("myKey", String::class.java)).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey", String::class.java) } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } + // -- get(Object key, Callable) -- @Test diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index dab20fd4d5..1b895deea5 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -65,8 +65,9 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, type); } try { + final ValueWrapper wrapper = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null); final T result = delegate.get(key, type); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index 163d0935fc..74af2721b7 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -83,6 +83,8 @@ class SentryCacheWrapperTest { fun `get with type creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) whenever(delegate.get("myKey", String::class.java)).thenReturn("value") val result = wrapper.get("myKey", String::class.java) @@ -92,10 +94,26 @@ class SentryCacheWrapperTest { assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type creates span with cache hit true when cached value is null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) + whenever(delegate.get("myKey", String::class.java)).thenReturn(null) + + val result = wrapper.get("myKey", String::class.java) + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + @Test fun `get with type creates span with cache hit false on miss`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) whenever(delegate.get("myKey", String::class.java)).thenReturn(null) val result = wrapper.get("myKey", String::class.java) @@ -105,6 +123,22 @@ class SentryCacheWrapperTest { assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenReturn(mock()) + whenever(delegate.get("myKey", String::class.java)).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey", String::class.java) } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } + // -- get(Object key, Callable) -- @Test From 4b60cec958afdfc31cac356cde73b70d6d224e59 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 05:35:53 +0100 Subject: [PATCH 2/2] docs(jcache): Fix docs link in README Co-Authored-By: Claude Opus 4.6 (1M context) --- sentry-jcache/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-jcache/README.md b/sentry-jcache/README.md index 071950a7a2..e4f4d8e49a 100644 --- a/sentry-jcache/README.md +++ b/sentry-jcache/README.md @@ -10,4 +10,4 @@ JCache is a standard API — you need a provider implementation at runtime. Comm - [Apache Ignite](https://ignite.apache.org/) - [Infinispan](https://infinispan.org/) -Please consult the documentation on how to install and use this integration in the Sentry Docs for [Java](https://docs.sentry.io/platforms/java/tracing/instrumentation/jcache/). +Please consult the documentation on how to install and use this integration in the Sentry Docs for [Java](https://docs.sentry.io/platforms/java/integrations/jcache/).