Skip to content

Commit 8de3fcf

Browse files
committed
feat: implement better cache hit detection
1 parent 165ff2a commit 8de3fcf

File tree

1 file changed

+72
-2
lines changed

1 file changed

+72
-2
lines changed

packages/nuxt/src/runtime/plugins/storage.server.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '@sentry/core';
1515
// eslint-disable-next-line import/no-extraneous-dependencies
1616
import { defineNitroPlugin, useStorage } from 'nitropack/runtime';
17+
import type { CacheEntry, ResponseCacheEntry } from 'nitropack/types';
1718
import type { Driver, Storage } from 'unstorage';
1819
// @ts-expect-error - This is a virtual module
1920
import { userStorageMounts } from '#sentry/storage-config.mjs';
@@ -131,7 +132,7 @@ function createMethodWrapper(
131132
span.setStatus({ code: SPAN_STATUS_OK });
132133

133134
if (CACHE_HIT_METHODS.has(methodName)) {
134-
span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, !isEmptyValue(result));
135+
span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, isCacheHit(args[0], result));
135136
}
136137

137138
return result;
@@ -185,7 +186,7 @@ function normalizeMethodName(methodName: string): string {
185186
/**
186187
* Checks if the value is empty, used for cache hit detection.
187188
*/
188-
function isEmptyValue(value: unknown): boolean {
189+
function isEmptyValue(value: unknown): value is null | undefined {
189190
return value === null || value === undefined;
190191
}
191192

@@ -242,3 +243,72 @@ function normalizeKey(key: unknown, prefix: string): string {
242243

243244
return `${prefix}${isEmptyValue(key) ? '' : String(key)}`;
244245
}
246+
247+
const CACHED_FN_HANDLERS_RE = /^nitro:(functions|handlers):/i;
248+
249+
/**
250+
* 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.
251+
* 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.
252+
* So in order to properly report cache hits for `defineCachedFunction` and `defineCachedEventHandler` we need to check the cached value ourselves.
253+
* First we check if the key matches the `defineCachedFunction` or `defineCachedEventHandler` key patterns, and if so we check the cached value.
254+
*/
255+
function isCacheHit(key: string, value: unknown): boolean {
256+
const isEmpty = isEmptyValue(value);
257+
// Empty value means no cache hit either way
258+
// Or if key doesn't match the cached function or handler patterns, we can return the empty value check
259+
if (isEmpty || !CACHED_FN_HANDLERS_RE.test(key)) {
260+
return !isEmpty;
261+
}
262+
263+
try {
264+
return validateCacheEntry(key, JSON.parse(String(value)) as CacheEntry);
265+
} catch (error) {
266+
// this is a best effort, so we return false if we can't validate the cache entry
267+
return false;
268+
}
269+
}
270+
271+
/**
272+
* Validates the cache entry.
273+
*/
274+
function validateCacheEntry(
275+
key: string,
276+
entry: CacheEntry | CacheEntry<ResponseCacheEntry & { status: number }>,
277+
): boolean {
278+
if (isEmptyValue(entry.value)) {
279+
return false;
280+
}
281+
282+
// Date.now is used by Nitro internally, so safe to use here.
283+
// https://github.com/nitrojs/nitro/blob/5508f71b77730e967fb131de817725f5aa7c4862/src/runtime/internal/cache.ts#L78
284+
if (Date.now() > (entry.expires || 0)) {
285+
return false;
286+
}
287+
288+
/**
289+
* Pulled from Nitro's cache entry validation
290+
* https://github.com/nitrojs/nitro/blob/5508f71b77730e967fb131de817725f5aa7c4862/src/runtime/internal/cache.ts#L223-L241
291+
*/
292+
if (isResponseCacheEntry(key, entry)) {
293+
if (entry.value.status >= 400) {
294+
return false;
295+
}
296+
297+
if (entry.value.body === undefined) {
298+
return false;
299+
}
300+
301+
if (entry.value.headers.etag === 'undefined' || entry.value.headers['last-modified'] === 'undefined') {
302+
return false;
303+
}
304+
}
305+
306+
return true;
307+
}
308+
309+
/**
310+
* Checks if the cache entry is a response cache entry.
311+
*/
312+
function isResponseCacheEntry(key: string, _: CacheEntry): _ is CacheEntry<ResponseCacheEntry & { status: number }> {
313+
return key.startsWith('nitro:handlers:');
314+
}

0 commit comments

Comments
 (0)