Next Cache Telemetry (#47939)
### What? Report additional telemetry related to cached fetches: - Fetch Index number to group related fetches (cache-get, cache-set, origin) - Origin URL map cache key to original upstream URL ### Why? This is needed for fetch cache telemetry on the Vercel platform. ### How? Telemetry is provided through optional parameters added to the fetch call configuration. It is similar to the `next: {revalidate: X}` and `{next: { internal: true }}` fields. The origin URL and fetch index are calculated in the patch-fetch function and are passed down to the caching classes as needed. These fields are optional and ignored by the `FileSystemCache`.
This commit is contained in:
parent
b5f785aab1
commit
5ea70b85c1
4 changed files with 84 additions and 17 deletions
|
@ -24,6 +24,8 @@ export interface StaticGenerationStore {
|
|||
|
||||
dynamicUsageDescription?: string
|
||||
dynamicUsageStack?: string
|
||||
|
||||
nextFetchId?: number
|
||||
}
|
||||
|
||||
export type StaticGenerationAsyncStorage =
|
||||
|
|
|
@ -5,6 +5,13 @@ import type { CacheHandler, CacheHandlerContext, CacheHandlerValue } from './'
|
|||
|
||||
let memoryCache: LRUCache<string, CacheHandlerValue> | undefined
|
||||
|
||||
interface NextFetchCacheParams {
|
||||
internal?: boolean
|
||||
fetchType?: string
|
||||
fetchIdx?: number
|
||||
originUrl?: string
|
||||
}
|
||||
|
||||
export default class FetchCache implements CacheHandler {
|
||||
private headers: Record<string, string>
|
||||
private cacheEndpoint?: string
|
||||
|
@ -66,7 +73,12 @@ export default class FetchCache implements CacheHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public async get(key: string, fetchCache?: boolean) {
|
||||
public async get(
|
||||
key: string,
|
||||
fetchCache?: boolean,
|
||||
originUrl?: string,
|
||||
fetchIdx?: number
|
||||
) {
|
||||
if (!fetchCache) return null
|
||||
|
||||
let data = memoryCache?.get(key)
|
||||
|
@ -81,13 +93,18 @@ export default class FetchCache implements CacheHandler {
|
|||
if (!data && this.cacheEndpoint) {
|
||||
try {
|
||||
const start = Date.now()
|
||||
const fetchParams: NextFetchCacheParams = {
|
||||
internal: true,
|
||||
fetchType: 'fetch-get',
|
||||
originUrl,
|
||||
fetchIdx,
|
||||
}
|
||||
const res = await fetch(
|
||||
`${this.cacheEndpoint}/v1/suspense-cache/${key}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: this.headers,
|
||||
// @ts-expect-error
|
||||
next: { internal: true },
|
||||
next: fetchParams as NextFetchRequestConfig,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -150,7 +167,9 @@ export default class FetchCache implements CacheHandler {
|
|||
public async set(
|
||||
key: string,
|
||||
data: CacheHandlerValue['value'],
|
||||
fetchCache?: boolean
|
||||
fetchCache?: boolean,
|
||||
originUrl?: string,
|
||||
fetchIdx?: number
|
||||
) {
|
||||
if (!fetchCache) return
|
||||
|
||||
|
@ -174,14 +193,19 @@ export default class FetchCache implements CacheHandler {
|
|||
data.data.headers['cache-control']
|
||||
}
|
||||
const body = JSON.stringify(data)
|
||||
const fetchParams: NextFetchCacheParams = {
|
||||
internal: true,
|
||||
fetchType: 'fetch-set',
|
||||
originUrl,
|
||||
fetchIdx,
|
||||
}
|
||||
const res = await fetch(
|
||||
`${this.cacheEndpoint}/v1/suspense-cache/${key}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: this.headers,
|
||||
body: body,
|
||||
// @ts-expect-error
|
||||
next: { internal: true },
|
||||
next: fetchParams as NextFetchRequestConfig,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -39,7 +39,9 @@ export class CacheHandler {
|
|||
|
||||
public async get(
|
||||
_key: string,
|
||||
_fetchCache?: boolean
|
||||
_fetchCache?: boolean,
|
||||
_originUrl?: string,
|
||||
_fetchIdx?: number
|
||||
): Promise<CacheHandlerValue | null> {
|
||||
return {} as any
|
||||
}
|
||||
|
@ -47,7 +49,9 @@ export class CacheHandler {
|
|||
public async set(
|
||||
_key: string,
|
||||
_data: IncrementalCacheValue | null,
|
||||
_fetchCache?: boolean
|
||||
_fetchCache?: boolean,
|
||||
_originUrl?: string,
|
||||
_fetchIdx?: number
|
||||
): Promise<void> {}
|
||||
}
|
||||
|
||||
|
@ -266,7 +270,9 @@ export class IncrementalCache {
|
|||
async get(
|
||||
pathname: string,
|
||||
fetchCache?: boolean,
|
||||
revalidate?: number
|
||||
revalidate?: number,
|
||||
originUrl?: string,
|
||||
fetchIdx?: number
|
||||
): Promise<IncrementalCacheEntry | null> {
|
||||
// we don't leverage the prerender cache in dev mode
|
||||
// so that getStaticProps is always called for easier debugging
|
||||
|
@ -279,7 +285,12 @@ export class IncrementalCache {
|
|||
|
||||
pathname = this._getPathname(pathname, fetchCache)
|
||||
let entry: IncrementalCacheEntry | null = null
|
||||
const cacheData = await this.cacheHandler?.get(pathname, fetchCache)
|
||||
const cacheData = await this.cacheHandler?.get(
|
||||
pathname,
|
||||
fetchCache,
|
||||
originUrl,
|
||||
fetchIdx
|
||||
)
|
||||
|
||||
if (cacheData?.value?.kind === 'FETCH') {
|
||||
revalidate = revalidate || cacheData.value.revalidate
|
||||
|
@ -337,7 +348,14 @@ export class IncrementalCache {
|
|||
curRevalidate,
|
||||
revalidateAfter,
|
||||
}
|
||||
this.set(pathname, entry.value, curRevalidate, fetchCache)
|
||||
this.set(
|
||||
pathname,
|
||||
entry.value,
|
||||
curRevalidate,
|
||||
fetchCache,
|
||||
originUrl,
|
||||
fetchIdx
|
||||
)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
@ -347,7 +365,9 @@ export class IncrementalCache {
|
|||
pathname: string,
|
||||
data: IncrementalCacheValue | null,
|
||||
revalidateSeconds?: number | false,
|
||||
fetchCache?: boolean
|
||||
fetchCache?: boolean,
|
||||
originUrl?: string,
|
||||
fetchIdx?: number
|
||||
) {
|
||||
if (this.dev && !fetchCache) return
|
||||
// fetchCache has upper limit of 2MB per-entry currently
|
||||
|
@ -374,7 +394,13 @@ export class IncrementalCache {
|
|||
initialRevalidateSeconds: revalidateSeconds,
|
||||
}
|
||||
}
|
||||
await this.cacheHandler?.set(pathname, data, fetchCache)
|
||||
await this.cacheHandler?.set(
|
||||
pathname,
|
||||
data,
|
||||
fetchCache,
|
||||
originUrl,
|
||||
fetchIdx
|
||||
)
|
||||
} catch (error) {
|
||||
console.warn('Failed to update prerender cache for', pathname, error)
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export function patchFetch({
|
|||
input: RequestInfo | URL,
|
||||
init: RequestInit | undefined
|
||||
) => {
|
||||
let url
|
||||
let url: URL | undefined
|
||||
try {
|
||||
url = new URL(input instanceof Request ? input.url : input)
|
||||
url.username = ''
|
||||
|
@ -163,6 +163,7 @@ export function patchFetch({
|
|||
!autoNoCache &&
|
||||
(typeof staticGenerationStore.revalidate === 'undefined' ||
|
||||
(typeof revalidate === 'number' &&
|
||||
typeof staticGenerationStore.revalidate === 'number' &&
|
||||
revalidate < staticGenerationStore.revalidate))
|
||||
) {
|
||||
staticGenerationStore.revalidate = revalidate
|
||||
|
@ -222,8 +223,18 @@ export function patchFetch({
|
|||
}
|
||||
}
|
||||
|
||||
const originUrl = url?.toString() ?? ''
|
||||
const fetchIdx = staticGenerationStore.nextFetchId ?? 1
|
||||
staticGenerationStore.nextFetchId = fetchIdx + 1
|
||||
|
||||
const doOriginalFetch = async () => {
|
||||
return originFetch(input, init).then(async (res) => {
|
||||
// add metadata to init without editing the original
|
||||
const clonedInit = {
|
||||
...init,
|
||||
next: { ...init?.next, fetchType: 'origin', fetchIdx, originUrl },
|
||||
}
|
||||
|
||||
return originFetch(input, clonedInit).then(async (res) => {
|
||||
if (
|
||||
res.ok &&
|
||||
staticGenerationStore.incrementalCache &&
|
||||
|
@ -256,7 +267,9 @@ export function patchFetch({
|
|||
revalidate,
|
||||
},
|
||||
revalidate,
|
||||
true
|
||||
true,
|
||||
originUrl,
|
||||
fetchIdx
|
||||
)
|
||||
} catch (err) {
|
||||
console.warn(`Failed to set fetch cache`, input, err)
|
||||
|
@ -277,7 +290,9 @@ export function patchFetch({
|
|||
: await staticGenerationStore.incrementalCache.get(
|
||||
cacheKey,
|
||||
true,
|
||||
revalidate
|
||||
revalidate,
|
||||
originUrl,
|
||||
fetchIdx
|
||||
)
|
||||
|
||||
if (entry?.value && entry.value.kind === 'FETCH') {
|
||||
|
|
Loading…
Reference in a new issue