Skip to content

Commit 2820549

Browse files
adinauerclaude
andcommitted
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) <noreply@anthropic.com>
1 parent b8911d9 commit 2820549

File tree

6 files changed

+108
-3
lines changed

6 files changed

+108
-3
lines changed

sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes
6767
return delegate.get(key, type);
6868
}
6969
try {
70+
final ValueWrapper wrapper = delegate.get(key);
71+
span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null);
7072
final T result = delegate.get(key, type);
71-
span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null);
7273
span.setStatus(SpanStatus.OK);
7374
return result;
7475
} catch (Throwable e) {

sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ class SentryCacheWrapperTest {
8585
fun `get with type creates span with cache hit true on hit`() {
8686
val tx = createTransaction()
8787
val wrapper = SentryCacheWrapper(delegate, scopes)
88+
val valueWrapper = mock<Cache.ValueWrapper>()
89+
whenever(delegate.get("myKey")).thenReturn(valueWrapper)
8890
whenever(delegate.get("myKey", String::class.java)).thenReturn("value")
8991

9092
val result = wrapper.get("myKey", String::class.java)
@@ -94,10 +96,26 @@ class SentryCacheWrapperTest {
9496
assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
9597
}
9698

99+
@Test
100+
fun `get with type creates span with cache hit true when cached value is null`() {
101+
val tx = createTransaction()
102+
val wrapper = SentryCacheWrapper(delegate, scopes)
103+
val valueWrapper = mock<Cache.ValueWrapper>()
104+
whenever(delegate.get("myKey")).thenReturn(valueWrapper)
105+
whenever(delegate.get("myKey", String::class.java)).thenReturn(null)
106+
107+
val result = wrapper.get("myKey", String::class.java)
108+
109+
assertNull(result)
110+
assertEquals(1, tx.spans.size)
111+
assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
112+
}
113+
97114
@Test
98115
fun `get with type creates span with cache hit false on miss`() {
99116
val tx = createTransaction()
100117
val wrapper = SentryCacheWrapper(delegate, scopes)
118+
whenever(delegate.get("myKey")).thenReturn(null)
101119
whenever(delegate.get("myKey", String::class.java)).thenReturn(null)
102120

103121
val result = wrapper.get("myKey", String::class.java)
@@ -107,6 +125,22 @@ class SentryCacheWrapperTest {
107125
assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
108126
}
109127

128+
@Test
129+
fun `get with type sets error status and throwable on exception`() {
130+
val tx = createTransaction()
131+
val wrapper = SentryCacheWrapper(delegate, scopes)
132+
val exception = RuntimeException("cache error")
133+
whenever(delegate.get("myKey")).thenReturn(mock<Cache.ValueWrapper>())
134+
whenever(delegate.get("myKey", String::class.java)).thenThrow(exception)
135+
136+
assertFailsWith<RuntimeException> { wrapper.get("myKey", String::class.java) }
137+
138+
assertEquals(1, tx.spans.size)
139+
val span = tx.spans.first()
140+
assertEquals(SpanStatus.INTERNAL_ERROR, span.status)
141+
assertEquals(exception, span.throwable)
142+
}
143+
110144
// -- get(Object key, Callable<T>) --
111145

112146
@Test

sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes
6767
return delegate.get(key, type);
6868
}
6969
try {
70+
final ValueWrapper wrapper = delegate.get(key);
71+
span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null);
7072
final T result = delegate.get(key, type);
71-
span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null);
7273
span.setStatus(SpanStatus.OK);
7374
return result;
7475
} catch (Throwable e) {

sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ class SentryCacheWrapperTest {
8585
fun `get with type creates span with cache hit true on hit`() {
8686
val tx = createTransaction()
8787
val wrapper = SentryCacheWrapper(delegate, scopes)
88+
val valueWrapper = mock<Cache.ValueWrapper>()
89+
whenever(delegate.get("myKey")).thenReturn(valueWrapper)
8890
whenever(delegate.get("myKey", String::class.java)).thenReturn("value")
8991

9092
val result = wrapper.get("myKey", String::class.java)
@@ -94,10 +96,26 @@ class SentryCacheWrapperTest {
9496
assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
9597
}
9698

99+
@Test
100+
fun `get with type creates span with cache hit true when cached value is null`() {
101+
val tx = createTransaction()
102+
val wrapper = SentryCacheWrapper(delegate, scopes)
103+
val valueWrapper = mock<Cache.ValueWrapper>()
104+
whenever(delegate.get("myKey")).thenReturn(valueWrapper)
105+
whenever(delegate.get("myKey", String::class.java)).thenReturn(null)
106+
107+
val result = wrapper.get("myKey", String::class.java)
108+
109+
assertNull(result)
110+
assertEquals(1, tx.spans.size)
111+
assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
112+
}
113+
97114
@Test
98115
fun `get with type creates span with cache hit false on miss`() {
99116
val tx = createTransaction()
100117
val wrapper = SentryCacheWrapper(delegate, scopes)
118+
whenever(delegate.get("myKey")).thenReturn(null)
101119
whenever(delegate.get("myKey", String::class.java)).thenReturn(null)
102120

103121
val result = wrapper.get("myKey", String::class.java)
@@ -107,6 +125,22 @@ class SentryCacheWrapperTest {
107125
assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
108126
}
109127

128+
@Test
129+
fun `get with type sets error status and throwable on exception`() {
130+
val tx = createTransaction()
131+
val wrapper = SentryCacheWrapper(delegate, scopes)
132+
val exception = RuntimeException("cache error")
133+
whenever(delegate.get("myKey")).thenReturn(mock<Cache.ValueWrapper>())
134+
whenever(delegate.get("myKey", String::class.java)).thenThrow(exception)
135+
136+
assertFailsWith<RuntimeException> { wrapper.get("myKey", String::class.java) }
137+
138+
assertEquals(1, tx.spans.size)
139+
val span = tx.spans.first()
140+
assertEquals(SpanStatus.INTERNAL_ERROR, span.status)
141+
assertEquals(exception, span.throwable)
142+
}
143+
110144
// -- get(Object key, Callable<T>) --
111145

112146
@Test

sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes
6565
return delegate.get(key, type);
6666
}
6767
try {
68+
final ValueWrapper wrapper = delegate.get(key);
69+
span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null);
6870
final T result = delegate.get(key, type);
69-
span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null);
7071
span.setStatus(SpanStatus.OK);
7172
return result;
7273
} catch (Throwable e) {

sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ class SentryCacheWrapperTest {
8383
fun `get with type creates span with cache hit true on hit`() {
8484
val tx = createTransaction()
8585
val wrapper = SentryCacheWrapper(delegate, scopes)
86+
val valueWrapper = mock<Cache.ValueWrapper>()
87+
whenever(delegate.get("myKey")).thenReturn(valueWrapper)
8688
whenever(delegate.get("myKey", String::class.java)).thenReturn("value")
8789

8890
val result = wrapper.get("myKey", String::class.java)
@@ -92,10 +94,26 @@ class SentryCacheWrapperTest {
9294
assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
9395
}
9496

97+
@Test
98+
fun `get with type creates span with cache hit true when cached value is null`() {
99+
val tx = createTransaction()
100+
val wrapper = SentryCacheWrapper(delegate, scopes)
101+
val valueWrapper = mock<Cache.ValueWrapper>()
102+
whenever(delegate.get("myKey")).thenReturn(valueWrapper)
103+
whenever(delegate.get("myKey", String::class.java)).thenReturn(null)
104+
105+
val result = wrapper.get("myKey", String::class.java)
106+
107+
assertNull(result)
108+
assertEquals(1, tx.spans.size)
109+
assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
110+
}
111+
95112
@Test
96113
fun `get with type creates span with cache hit false on miss`() {
97114
val tx = createTransaction()
98115
val wrapper = SentryCacheWrapper(delegate, scopes)
116+
whenever(delegate.get("myKey")).thenReturn(null)
99117
whenever(delegate.get("myKey", String::class.java)).thenReturn(null)
100118

101119
val result = wrapper.get("myKey", String::class.java)
@@ -105,6 +123,22 @@ class SentryCacheWrapperTest {
105123
assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
106124
}
107125

126+
@Test
127+
fun `get with type sets error status and throwable on exception`() {
128+
val tx = createTransaction()
129+
val wrapper = SentryCacheWrapper(delegate, scopes)
130+
val exception = RuntimeException("cache error")
131+
whenever(delegate.get("myKey")).thenReturn(mock<Cache.ValueWrapper>())
132+
whenever(delegate.get("myKey", String::class.java)).thenThrow(exception)
133+
134+
assertFailsWith<RuntimeException> { wrapper.get("myKey", String::class.java) }
135+
136+
assertEquals(1, tx.spans.size)
137+
val span = tx.spans.first()
138+
assertEquals(SpanStatus.INTERNAL_ERROR, span.status)
139+
assertEquals(exception, span.throwable)
140+
}
141+
108142
// -- get(Object key, Callable<T>) --
109143

110144
@Test

0 commit comments

Comments
 (0)