feat(spring): [Cache Tracing 1] Add SentryCacheWrapper and SentryCacheManagerWrapper#5172
feat(spring): [Cache Tracing 1] Add SentryCacheWrapper and SentryCacheManagerWrapper#5172adinauer wants to merge 6 commits intofeat/cache-tracingfrom
Conversation
…eManagerWrapper Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion - Use cache key as span description instead of cache name, matching the spec and other SDKs (Python, JavaScript) - Skip instrumentation for putIfAbsent since we cannot know if a write actually occurred; override to bypass default get()+put() delegation - Wrap valueLoader Callable in get(key, Callable) to detect cache hit/miss instead of always reporting hit=true - Update tests to match new behavior Co-Authored-By: Claude <noreply@anthropic.com>
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛
🤖 This preview updates automatically when you update the PR. |
Instructions and example for changelogPlease add an entry to Example: ## Unreleased
### Features
- [Cache Tracing 1] Add SentryCacheWrapper and SentryCacheManagerWrapper ([#5172](https://github.com/getsentry/sentry-java/pull/5172))If none of the above apply, you can opt out of this check by adding |
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3e5f591 | 317.48 ms | 356.80 ms | 39.32 ms |
| 2876c40 | 339.24 ms | 405.65 ms | 66.41 ms |
| 8baac33 | 313.15 ms | 367.84 ms | 54.69 ms |
| 4b44539 | 255.79 ms | 300.12 ms | 44.33 ms |
| f94f681 | 405.20 ms | 453.20 ms | 48.00 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3e5f591 | 0 B | 0 B | 0 B |
| 2876c40 | 1.58 MiB | 2.29 MiB | 723.29 KiB |
| 8baac33 | 0 B | 0 B | 0 B |
| 4b44539 | 0 B | 0 B | 0 B |
| f94f681 | 0 B | 0 B | 0 B |
Sentry Build Distribution
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Missing double-wrapping guard creates duplicate spans
- Added an
instanceof SentryCacheWrapperguard inSentryCacheManagerWrapper.getCache()to return already wrapped caches without wrapping again.
- Added an
- ✅ Fixed: Cache hit misreported for stored null values
- Updated
SentryCacheWrapper.get(key, type)to markcache.hitas true when the typed result is null butdelegate.get(key)confirms a stored entry.
- Updated
Or push these changes by commenting:
@cursor push c7c4fa1f63
Preview (c7c4fa1f63)
diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java
--- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java
+++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java
@@ -27,6 +27,9 @@
if (cache == null) {
return null;
}
+ if (cache instanceof SentryCacheWrapper) {
+ return cache;
+ }
return new SentryCacheWrapper(cache, scopes);
}
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
--- 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
@@ -65,7 +65,8 @@
}
try {
final T result = delegate.get(key, type);
- span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null);
+ final boolean cacheHit = result != null || delegate.get(key) != null;
+ span.setData(SpanDataConvention.CACHE_HIT_KEY, cacheHit);
span.setStatus(SpanStatus.OK);
return result;
} catch (Throwable e) {
diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt
--- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt
+++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt
@@ -4,6 +4,7 @@
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
+import kotlin.test.assertSame
import kotlin.test.assertTrue
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -37,6 +38,18 @@
}
@Test
+ fun `getCache does not re-wrap SentryCacheWrapper`() {
+ val cache = mock<Cache>()
+ val wrappedCache = SentryCacheWrapper(cache, scopes)
+ whenever(delegate.getCache("test")).thenReturn(wrappedCache)
+
+ val wrapper = SentryCacheManagerWrapper(delegate, scopes)
+ val result = wrapper.getCache("test")
+
+ assertSame(wrappedCache, result)
+ }
+
+ @Test
fun `getCacheNames delegates to underlying cache manager`() {
whenever(delegate.cacheNames).thenReturn(listOf("cache1", "cache2"))
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
--- 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
@@ -102,6 +102,21 @@
assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
}
+ @Test
+ fun `get with type creates span with cache hit true for stored null value`() {
+ val tx = createTransaction()
+ val wrapper = SentryCacheWrapper(delegate, scopes)
+ val valueWrapper = mock<Cache.ValueWrapper>()
+ whenever(delegate.get("myKey", String::class.java)).thenReturn(null)
+ whenever(delegate.get("myKey")).thenReturn(valueWrapper)
+
+ 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))
+ }
+
// -- get(Object key, Callable<T>) --
@Test
sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java
Show resolved
Hide resolved
sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java
Show resolved
Hide resolved
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| } | ||
| return span; | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing retrieve overrides bypass delegate's async implementation
Medium Severity
SentryCacheWrapper doesn't override the retrieve(Object key) and retrieve(Object key, Supplier) default methods from Spring's Cache interface. The default retrieve(key) calls this.get(key) and retrieve(key, supplier) calls this.retrieve(key) then potentially this.put(key, value). This is the same class of problem the putIfAbsent override addresses (comment: "We must override to bypass the default implementation which calls this.get() + this.put()"). For caches with custom async retrieve implementations (e.g., CaffeineCache with AsyncCache), the delegate's async behavior is silently replaced with synchronous get(), potentially causing thread blocking in reactive/async @Cacheable methods.
Additional Locations (1)
Sentry Build Distribution
|



PR Stack (Cache Tracing)
📜 Description
Adds
SentryCacheWrapperandSentryCacheManagerWrapperto thesentry-spring-7module. These wrapper classes instrument Spring'sCacheandCacheManagerinterfaces to producecache.get,cache.put,cache.remove, andcache.flushspans per the Sentry cache module spec.Also adds
CACHE_HIT_KEYandCACHE_KEY_KEYconstants toSpanDataConvention.💚 How did you test it?
SentryCacheWrappercovering all cache operations, hit/miss detection, error handling, and span data assertionsSentryCacheManagerWrappercovering cache wrapping, null passthrough, andgetCacheNamesdelegation📝 Checklist
sendDefaultPIIis enabled.