add a "skip" cache type to verbose logging mode (#53275)

When verbose logging is enabled and a cache MISS is logged due to either `revalidate: 0`,`cache: no-store` or `cache-control: no-store`, this logs a slightly different message, ie:

```
- GET / 200 in 804ms
   ├─── GET <url> in 205ms (cache: SKIP, reason: cache: no-cache)
   ├────── GET <url> 200 in 0ms (cache: HIT)
   ├────── GET <url> 200 in 373ms (cache: SKIP, reason: revalidate: 0)
   └────── GET <url> 200 in 111ms (cache: SKIP, reason: cache-control: no-cache (hard refresh))
 ```
 
 Closes NEXT-1412
This commit is contained in:
Zack Tanner 2023-07-28 13:01:31 -07:00 committed by GitHub
parent 16a5aa74d0
commit afa9f96515
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 120 additions and 23 deletions

View file

@ -43,7 +43,8 @@ export interface StaticGenerationStore {
start: number
method: string
status: number
cacheStatus: 'hit' | 'miss'
cacheReason: string
cacheStatus: 'hit' | 'miss' | 'skip'
}>
isDraftMode?: boolean

View file

@ -33,7 +33,7 @@ function trackFetchMetric(
status: number
method: string
cacheReason: string
cacheStatus: 'hit' | 'miss'
cacheStatus: 'hit' | 'miss' | 'skip'
start: number
}
) {
@ -56,6 +56,7 @@ function trackFetchMetric(
staticGenerationStore.fetchMetrics.push({
url: ctx.url,
cacheStatus: ctx.cacheStatus,
cacheReason: ctx.cacheReason,
status: ctx.status,
method: ctx.method,
start: ctx.start,
@ -176,6 +177,7 @@ export function patchFetch({
staticGenerationStore.fetchCache === 'force-no-store'
let _cache = getRequestMeta('cache')
let cacheReason = ''
if (
typeof _cache === 'string' &&
@ -192,12 +194,12 @@ export function patchFetch({
}
if (['no-cache', 'no-store'].includes(_cache || '')) {
curRevalidate = 0
cacheReason = `cache: ${_cache}`
}
if (typeof curRevalidate === 'number' || curRevalidate === false) {
revalidate = curRevalidate
}
let cacheReason = ''
const _headers = getRequestMeta('headers')
const initHeaders: Headers =
typeof _headers?.get === 'function'
@ -342,7 +344,10 @@ export function patchFetch({
const normalizedRevalidate =
typeof revalidate !== 'number' ? CACHE_ONE_YEAR : revalidate
const doOriginalFetch = async (isStale?: boolean) => {
const doOriginalFetch = async (
isStale?: boolean,
cacheReasonOverride?: string
) => {
// add metadata to init without editing the original
const clonedInit = {
...init,
@ -354,8 +359,9 @@ export function patchFetch({
trackFetchMetric(staticGenerationStore, {
start: fetchStart,
url: fetchUrl,
cacheReason,
cacheStatus: 'miss',
cacheReason: cacheReasonOverride || cacheReason,
cacheStatus:
revalidate === 0 || cacheReasonOverride ? 'skip' : 'miss',
status: res.status,
method: clonedInit.method || 'GET',
})
@ -403,6 +409,7 @@ export function patchFetch({
}
let handleUnlock = () => Promise.resolve()
let cacheReasonOverride
if (cacheKey && staticGenerationStore.incrementalCache) {
handleUnlock = await staticGenerationStore.incrementalCache.lock(
@ -421,6 +428,9 @@ export function patchFetch({
if (entry) {
await handleUnlock()
} else {
// in dev, incremental cache response will be null in case the browser adds `cache-control: no-cache` in the request headers
cacheReasonOverride = 'cache-control: no-cache (hard refresh)'
}
if (entry?.value && entry.value.kind === 'FETCH') {
@ -545,7 +555,7 @@ export function patchFetch({
}
}
return doOriginalFetch().finally(handleUnlock)
return doOriginalFetch(false, cacheReasonOverride).finally(handleUnlock)
}
)
}

View file

@ -1125,11 +1125,14 @@ export default class NextNodeServer extends BaseServer {
for (let i = 0; i < fetchMetrics.length; i++) {
const metric = fetchMetrics[i]
const lastItem = i === fetchMetrics.length - 1
let cacheStatus = metric.cacheStatus
let { cacheStatus, cacheReason } = metric
const duration = metric.end - metric.start
if (cacheStatus === 'hit') {
cacheStatus = chalk.green('HIT')
} else if (cacheStatus === 'skip') {
cacheStatus = `${chalk.yellow('SKIP')}, reason: ${cacheReason}`
} else {
cacheStatus = chalk.yellow('MISS')
}

View file

@ -1,7 +1,30 @@
import path from 'path'
import { createNextDescribe, FileRef } from 'e2e-utils'
import { createNextDescribe, FileRef } from '../../../lib/e2e-utils'
import stripAnsi from 'strip-ansi'
function parseLogsFromCli(cliOutput: string) {
return stripAnsi(cliOutput)
.split('\n')
.filter((log) => log.includes('cache:'))
.map((log) => {
const trimmedLog = log.replace(/^[^a-zA-Z]+/, '')
const parts = trimmedLog.split(' ')
const method = parts[0]
const url = parts[1]
const statusCode = parseInt(parts[2])
const responseTime = parseInt(parts[4])
const cache = parts.slice(5).join(' ')
return {
method,
url,
statusCode,
responseTime,
cache,
}
})
}
createNextDescribe(
'app-dir - data fetching with cache logging',
{
@ -15,21 +38,65 @@ createNextDescribe(
},
},
({ next, isNextDev }) => {
function runTests({ isVerbose }: { isVerbose: boolean }) {
it('should not log fetching hits in dev mode by default', async () => {
describe('with verbose logging', () => {
it('should only log requests in dev mode', async () => {
await next.fetch('/default-cache')
const logs = stripAnsi(next.cliOutput)
if (isVerbose && isNextDev) {
if (isNextDev) {
expect(logs).toContain('GET /default-cache 200')
} else {
expect(logs).not.toContain('GET /default-cache 200')
}
})
}
describe('with logging verbose', () => {
runTests({ isVerbose: true })
await next.stop()
})
if (isNextDev) {
it("should log 'skip' cache status with a reason when cache: 'no-cache' is used", async () => {
await next.start()
await next.fetch('/default-cache')
const logs = parseLogsFromCli(next.cliOutput)
const logEntry = logs.find((log) =>
log.url.includes('api/random?no-cache')
)
expect(logEntry?.cache).toMatchInlineSnapshot(
`"(cache: SKIP, reason: cache: no-cache)"`
)
await next.stop()
})
it("should log 'skip' cache status with a reason when revalidate: 0 is used", async () => {
await next.start()
await next.fetch('/default-cache')
const logs = parseLogsFromCli(next.cliOutput)
const logEntry = logs.find((log) =>
log.url.includes('api/random?revalidate-0')
)
expect(logEntry?.cache).toMatchInlineSnapshot(
`"(cache: SKIP, reason: revalidate: 0)"`
)
await next.stop()
})
it("should log 'skip' cache status with a reason when the browser indicates caching should be ignored", async () => {
await next.start()
await next.fetch('/default-cache', {
headers: { 'Cache-Control': 'no-cache' },
})
const logs = parseLogsFromCli(next.cliOutput)
const logEntry = logs.find((log) =>
log.url.includes('api/random?auto-cache')
)
expect(logEntry?.cache).toMatchInlineSnapshot(
`"(cache: SKIP, reason: cache-control: no-cache (hard refresh))"`
)
await next.stop()
})
}
})
describe('with default logging', () => {
@ -38,7 +105,13 @@ createNextDescribe(
await next.deleteFile('next.config.js')
await next.start()
})
runTests({ isVerbose: false })
it('should not log fetch requests at all', async () => {
await next.fetch('/default-cache')
const logs = stripAnsi(next.cliOutput)
expect(logs).not.toContain('GET /default-cache 200')
})
})
}
)

View file

@ -2,21 +2,30 @@ export const fetchCache = 'default-cache'
export default async function Page() {
const dataNoCache = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?a1',
'https://next-data-api-endpoint.vercel.app/api/random?no-cache',
{
cache: 'no-cache',
}
).then((res) => res.text())
const dataForceCache = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?b2',
'https://next-data-api-endpoint.vercel.app/api/random?force-cache',
{
cache: 'force-cache',
}
).then((res) => res.text())
const dataRevalidate0 = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?revalidate-0',
{
next: {
revalidate: 0,
},
}
).then((res) => res.text())
const dataRevalidateCache = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?c3',
'https://next-data-api-endpoint.vercel.app/api/random?revalidate-3',
{
next: {
revalidate: 3,
@ -25,7 +34,7 @@ export default async function Page() {
).then((res) => res.text())
const dataRevalidateAndFetchCache = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?d4',
'https://next-data-api-endpoint.vercel.app/api/random?revalidate-3-force-cache',
{
next: {
revalidate: 3,
@ -35,7 +44,7 @@ export default async function Page() {
).then((res) => res.text())
const dataAutoCache = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random?d4'
'https://next-data-api-endpoint.vercel.app/api/random?auto-cache'
).then((res) => res.text())
return (
@ -43,6 +52,7 @@ export default async function Page() {
<p>/force-cache</p>
<p id="data-no-cache">"cache: no-cache" {dataNoCache}</p>
<p id="data-force-cache">"cache: force-cache" {dataForceCache}</p>
<p id="data-revalidate-0">"revalidate: 0" {dataRevalidate0}</p>
<p id="data-revalidate-cache">"revalidate: 3" {dataRevalidateCache}</p>
<p id="data-revalidate-and-fetch-cache">
"revalidate: 3 and cache: force-cache" {dataRevalidateAndFetchCache}