@@ -13,6 +13,7 @@ import {
1313} from '@sentry/core' ;
1414// eslint-disable-next-line import/no-extraneous-dependencies
1515import { defineNitroPlugin , useStorage } from 'nitropack/runtime' ;
16+ import type { CacheEntry , ResponseCacheEntry } from 'nitropack/types' ;
1617import type { Driver , Storage } from 'unstorage' ;
1718// @ts -expect-error - This is a virtual module
1819import { userStorageMounts } from '#sentry/storage-config.mjs' ;
@@ -153,7 +154,7 @@ function createMethodWrapper(
153154 span . setStatus ( { code : SPAN_STATUS_OK } ) ;
154155
155156 if ( CACHE_HIT_METHODS . has ( methodName ) ) {
156- span . setAttribute ( SEMANTIC_ATTRIBUTE_CACHE_HIT , ! isEmptyValue ( result ) ) ;
157+ span . setAttribute ( SEMANTIC_ATTRIBUTE_CACHE_HIT , isCacheHit ( args ?. [ 0 ] ?? '' , result ) ) ;
157158 }
158159
159160 return result ;
@@ -229,6 +230,75 @@ function normalizeMethodName(methodName: string): string {
229230/**
230231 * Checks if the value is empty, used for cache hit detection.
231232 */
232- function isEmptyValue ( value : unknown ) : boolean {
233+ function isEmptyValue ( value : unknown ) : value is null | undefined {
233234 return value === null || value === undefined ;
234235}
236+
237+ const CACHED_FN_HANDLERS_RE = / ^ n i t r o : ( f u n c t i o n s | h a n d l e r s ) : / i;
238+
239+ /**
240+ * Since Nitro's cache may not utilize the driver's TTL, it is possible that the value is present in the cache but won't be used by Nitro.
241+ * The maxAge and expires values is serialized by Nitro in the cache entry. This means the value presence does not necessarily mean a cache hit.
242+ * So in order to properly report cache hits for `defineCachedFunction` and `defineCachedEventHandler` we need to check the cached value ourselves.
243+ * First we check if the key matches the `defineCachedFunction` or `defineCachedEventHandler` key patterns, and if so we check the cached value.
244+ */
245+ function isCacheHit ( key : string , value : unknown ) : boolean {
246+ const isEmpty = isEmptyValue ( value ) ;
247+ // Empty value means no cache hit either way
248+ // Or if key doesn't match the cached function or handler patterns, we can return the empty value check
249+ if ( isEmpty || ! CACHED_FN_HANDLERS_RE . test ( key ) ) {
250+ return ! isEmpty ;
251+ }
252+
253+ try {
254+ return validateCacheEntry ( key , JSON . parse ( String ( value ) ) as CacheEntry ) ;
255+ } catch ( error ) {
256+ // this is a best effort, so we return false if we can't validate the cache entry
257+ return false ;
258+ }
259+ }
260+
261+ /**
262+ * Validates the cache entry.
263+ */
264+ function validateCacheEntry (
265+ key : string ,
266+ entry : CacheEntry | CacheEntry < ResponseCacheEntry & { status : number } > ,
267+ ) : boolean {
268+ if ( isEmptyValue ( entry . value ) ) {
269+ return false ;
270+ }
271+
272+ // Date.now is used by Nitro internally, so safe to use here.
273+ // https://github.com/nitrojs/nitro/blob/5508f71b77730e967fb131de817725f5aa7c4862/src/runtime/internal/cache.ts#L78
274+ if ( Date . now ( ) > ( entry . expires || 0 ) ) {
275+ return false ;
276+ }
277+
278+ /**
279+ * Pulled from Nitro's cache entry validation
280+ * https://github.com/nitrojs/nitro/blob/5508f71b77730e967fb131de817725f5aa7c4862/src/runtime/internal/cache.ts#L223-L241
281+ */
282+ if ( isResponseCacheEntry ( key , entry ) ) {
283+ if ( entry . value . status >= 400 ) {
284+ return false ;
285+ }
286+
287+ if ( entry . value . body === undefined ) {
288+ return false ;
289+ }
290+
291+ if ( entry . value . headers . etag === 'undefined' || entry . value . headers [ 'last-modified' ] === 'undefined' ) {
292+ return false ;
293+ }
294+ }
295+
296+ return true ;
297+ }
298+
299+ /**
300+ * Checks if the cache entry is a response cache entry.
301+ */
302+ function isResponseCacheEntry ( key : string , _ : CacheEntry ) : _ is CacheEntry < ResponseCacheEntry & { status : number } > {
303+ return key . startsWith ( 'nitro:handlers:' ) ;
304+ }
0 commit comments