diff --git a/bun.lock b/bun.lock index 46ac2592dd..a7d865c06d 100644 --- a/bun.lock +++ b/bun.lock @@ -145,6 +145,7 @@ "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", + "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", @@ -156,7 +157,6 @@ "warn-once": "^0.1.1", }, "devDependencies": { - "@opennextjs/cloudflare": "^1.0.0-beta.3", "@types/rison": "^0.0.9", "gitbook": "*", "postcss": "^8", @@ -791,9 +791,9 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opennextjs/aws": ["@opennextjs/aws@3.5.7", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-YjyHJrkIHI7YwQRCp8GjDOudu86oOc1RiwxvBBpPHrplsS18H4ZmkzGggAKhK6B4myGsJQ/q9kNP2TraoZiNzg=="], + "@opennextjs/aws": ["@opennextjs/aws@3.6.1", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-RYU9K58vEUPXqc3pZO6kr9vBy1MmJZFQZLe0oXBskC005oGju/m4e3DCCP4eZ/Q/HdYQXCoqNXgSGi8VCAYgew=="], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.0-beta.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.5.7", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^3.114.3 || ^4.7.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-qKBXQZhUeQ+iGvfJeF7PO30g59LHnPOlRVZd77zxwn6Uc9C+c0LSwo8N28XRIWyQPkY007rKk9pSIxOrP4MHtQ=="], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@https://pkg.pr.new/@opennextjs/cloudflare@666", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], diff --git a/package.json b/package.json index 447997ab82..5398e52f9f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "turbo": "^2.5.0", "vercel": "^39.3.0" }, - "packageManager": "bun@1.2.8", + "packageManager": "bun@1.2.11", "overrides": { "@codemirror/state": "6.4.1", "@gitbook/api": "^0.115.0", diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts index 8f4d389da6..4ff4f74e88 100644 --- a/packages/gitbook-v2/open-next.config.ts +++ b/packages/gitbook-v2/open-next.config.ts @@ -3,16 +3,27 @@ import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cac import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache'; import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue'; import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache'; +import { + softTagFilter, + withFilter, +} from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter'; export default defineCloudflareConfig({ incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }), - tagCache: doShardedTagCache({ - baseShardSize: 12, - regionalCache: true, - shardReplication: { - numberOfSoftReplicas: 2, - numberOfHardReplicas: 1, - }, + tagCache: withFilter({ + tagCache: doShardedTagCache({ + baseShardSize: 12, + regionalCache: true, + shardReplication: { + numberOfSoftReplicas: 2, + numberOfHardReplicas: 1, + }, + }), + // We don't use `revalidatePath`, so we filter out soft tags + filterFn: softTagFilter, }), queue: doQueue, + + // Performance improvements as we don't use PPR + enableCacheInterception: true, }); diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index e78e3e2dfe..646bfb4719 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -5,6 +5,7 @@ "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", + "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", @@ -17,7 +18,6 @@ }, "devDependencies": { "gitbook": "*", - "@opennextjs/cloudflare": "^1.0.0-beta.3", "@types/rison": "^0.0.9", "tailwindcss": "^3.4.0", "postcss": "^8" diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 20e495656e..7a2d03d16d 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -7,14 +7,8 @@ import { type RenderIntegrationUI, } from '@gitbook/api'; import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags'; -import { - GITBOOK_API_TOKEN, - GITBOOK_API_URL, - GITBOOK_RUNTIME, - GITBOOK_USER_AGENT, -} from '@v2/lib/env'; +import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; -import { unstable_cache } from 'next/cache'; import { getCloudflareContext, getCloudflareRequestGlobal } from './cloudflare'; import { DataFetcherError, wrapDataFetcherError } from './errors'; import { withCacheKey, withoutConcurrentExecution } from './memoize'; @@ -28,16 +22,13 @@ interface DataFetcherInput { } /** - * Revalidation profile for the cache. - * Based on https://nextjs.org/docs/app/api-reference/functions/cacheLife#default-cache-profiles + * Options to pass to the `fetch` call to disable the Next data-cache when wrapped in `use cache`. */ -enum RevalidationProfile { - minutes = 60, - hours = 60 * 60, - days = 60 * 60 * 24, - weeks = 60 * 60 * 24 * 7, - max = 60 * 60 * 24 * 30, -} +export const noCacheFetchOptions: Partial = { + next: { + revalidate: 0, + }, +}; /** * Create a data fetcher using an API token. @@ -206,589 +197,301 @@ export function createDataFetcher( }; } -/* - * For the following functions, we: - * - Wrap them with `withCacheKey` to compute a cache key from the function arguments ONCE (to be performant) - * - Pass the cache key to `unstable_cache` to ensure the cache is not tied to closures - * - Call the uncached function in a `withoutConcurrentExecution` wrapper to prevent concurrent executions - * - * Important: - * - Only the function inside the `unstable_cache` is wrapped in `withoutConcurrentExecution` as Next.js needs to call - * the return of `unstable_cache` to identify the tags. - */ - const getUserById = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, - async (cacheKey, input: DataFetcherInput, params: { userId: string }) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getUserByIdUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getUserByIdUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [], - } - ); - - return uncached(); + async (_, input: DataFetcherInput, params: { userId: string }) => { + 'use cache'; + return trace(`getUserById(${params.userId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.users.getUserById(params.userId, { + ...noCacheFetchOptions, + }); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); } ) ); -const getUserByIdUseCache = async (input: DataFetcherInput, params: { userId: string }) => { - 'use cache'; - return getUserByIdUncached(input, params, true); -}; - -const getUserByIdUncached = async ( - input: DataFetcherInput, - params: { userId: string }, - withUseCache = false -) => { - return trace(`getUserById.uncached(${params.userId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.users.getUserById(params.userId); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - }); -}; - const getSpace = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; shareKey: string | undefined } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getSpaceUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getSpaceUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [ - getCacheTag({ - tag: 'space', - space: params.spaceId, - }), - ], - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) ); - return uncached(); + return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getSpaceById( + params.spaceId, + { + shareKey: params.shareKey, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); } ) ); -const getSpaceUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; shareKey: string | undefined } -) => { - 'use cache'; - return getSpaceUncached(input, params, true); -}; - -const getSpaceUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; shareKey: string | undefined }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); - } - - return trace(`getSpace.uncached(${params.spaceId}, ${params.shareKey})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getSpaceById(params.spaceId, { - shareKey: params.shareKey, - }); - - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - }); -}; - const getChangeRequest = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; changeRequestId: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getChangeRequestUseCache(input, params); - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'change-request', + space: params.spaceId, + changeRequest: params.changeRequestId, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => { - return getChangeRequestUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.minutes * 5, - tags: [ - getCacheTag({ - tag: 'change-request', - space: params.spaceId, - changeRequest: params.changeRequestId, - }), - ], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getChangeRequestById( + params.spaceId, + params.changeRequestId, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('minutes'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getChangeRequestUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; changeRequestId: string } -) => { - 'use cache'; - return getChangeRequestUncached(input, params, true); -}; - -const getChangeRequestUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; changeRequestId: string }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'change-request', - space: params.spaceId, - changeRequest: params.changeRequestId, - }) - ); - } - - return trace( - `getChangeRequest.uncached(${params.spaceId}, ${params.changeRequestId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getChangeRequestById( - params.spaceId, - params.changeRequestId - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('minutes'); - } - return res.data; - }); - } - ); -}; - const getRevision = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; metadata: boolean } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getRevisionUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], - } - ); - - return uncached(); + 'use cache'; + return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); } ) ); -const getRevisionUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } -) => { - 'use cache'; - return getRevisionUncached(input, params, true); -}; - -const getRevisionUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean }, - withUseCache = false -) => { - return trace(`getRevision.uncached(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, { - metadata: params.metadata, - }); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - }); -}; - const getRevisionPages = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; metadata: boolean } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionPagesUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getRevisionPagesUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], - } - ); - - return uncached(); + 'use cache'; + return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.listPagesInRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data.pages; + }); + }); } ) ); -const getRevisionPagesUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } -) => { - 'use cache'; - return getRevisionPagesUncached(input, params, true); -}; - -const getRevisionPagesUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean }, - withUseCache = false -) => { - return trace(`getRevisionPages.uncached(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.listPagesInRevisionById( - params.spaceId, - params.revisionId, - { - metadata: params.metadata, - } - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data.pages; - }); - }); -}; - const getRevisionFile = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; fileId: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionFileUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + 'use cache'; + return trace( + `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, async () => { - return getRevisionFileUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getFileInRevisionById( + params.spaceId, + params.revisionId, + params.fileId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getRevisionFileUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; fileId: string } -) => { - 'use cache'; - return getRevisionFileUncached(input, params, true); -}; - -const getRevisionFileUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; fileId: string }, - withUseCache = false -) => { - return trace( - `getRevisionFile.uncached(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getFileInRevisionById( - params.spaceId, - params.revisionId, - params.fileId, - {} - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getRevisionPageMarkdown = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; pageId: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionPageMarkdownUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + 'use cache'; + return trace( + `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, async () => { - return getRevisionPageMarkdownUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionById( + params.spaceId, + params.revisionId, + params.pageId, + { + format: 'markdown', + }, + { + ...noCacheFetchOptions, + } + ); + + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + + if (!('markdown' in res.data)) { + throw new DataFetcherError('Page is not a document', 404); + } + return res.data.markdown; + }); } ); - - return uncached(); } ) ); -const getRevisionPageMarkdownUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; pageId: string } -) => { - 'use cache'; - return getRevisionPageMarkdownUncached(input, params, true); -}; - -const getRevisionPageMarkdownUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; pageId: string }, - withUseCache = false -) => { - return trace( - `getRevisionPageMarkdown.uncached(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionById( - params.spaceId, - params.revisionId, - params.pageId, - { - format: 'markdown', - } - ); - - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - - if (!('markdown' in res.data)) { - throw new DataFetcherError('Page is not a document', 404); - } - return res.data.markdown; - }); - } - ); -}; - const getRevisionPageByPath = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; path: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionPageByPathUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + 'use cache'; + return trace( + `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, async () => { - return getRevisionPageByPathUncached(input, params); - }, - [cacheKey, 'v2'], - { - revalidate: RevalidationProfile.max, - tags: [], + const encodedPath = encodeURIComponent(params.path); + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionByPath( + params.spaceId, + params.revisionId, + encodedPath, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getRevisionPageByPathUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; path: string } -) => { - 'use cache'; - return getRevisionPageByPathUncached(input, params, true); -}; - -const getRevisionPageByPathUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; path: string }, - withUseCache = false -) => { - return trace( - `getRevisionPageByPath.uncached(${params.spaceId}, ${params.revisionId}, ${params.path})`, - async () => { - const encodedPath = encodeURIComponent(params.path); - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionByPath( - params.spaceId, - params.revisionId, - encodedPath, - {} - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getDocument = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, - async ( - cacheKey, - input: DataFetcherInput, - params: { spaceId: string; documentId: string } - ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getDocumentUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getDocumentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], - } - ); - - return uncached(); + async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => { + 'use cache'; + return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getDocumentById( + params.spaceId, + params.documentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); } ) ); -const getDocumentUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; documentId: string } -) => { - 'use cache'; - return getDocumentUncached(input, params, true); -}; - -const getDocumentUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; documentId: string }, - withUseCache = false -) => { - return trace(`getDocument.uncached(${params.spaceId}, ${params.documentId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {}); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - }); -}; - const getComputedDocument = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; @@ -797,309 +500,157 @@ const getComputedDocument = withCacheKey( seed: string; } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getComputedDocumentUseCache(input, params); - } + 'use cache'; + cacheTag( + ...getComputedContentSourceCacheTags( + { + spaceId: params.spaceId, + organizationId: params.organizationId, + }, + params.source + ) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, async () => { - return getComputedDocumentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: getComputedContentSourceCacheTags( - { - spaceId: params.spaceId, - organizationId: params.organizationId, - }, - params.source - ), + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getComputedDocument( + params.spaceId, + { + source: params.source, + seed: params.seed, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getComputedDocumentUseCache = async ( - input: DataFetcherInput, - params: { - spaceId: string; - organizationId: string; - source: ComputedContentSource; - seed: string; - } -) => { - 'use cache'; - return getComputedDocumentUncached(input, params, true); -}; - -const getComputedDocumentUncached = async ( - input: DataFetcherInput, - params: { - spaceId: string; - organizationId: string; - source: ComputedContentSource; - seed: string; - }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - ...getComputedContentSourceCacheTags( - { - spaceId: params.spaceId, - organizationId: params.organizationId, - }, - params.source - ) - ); - } - - return trace( - `getComputedDocument.uncached(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getComputedDocument(params.spaceId, { - source: params.source, - seed: params.seed, - }); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getReusableContent = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; reusableContentId: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getReusableContentUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + 'use cache'; + return trace( + `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, async () => { - return getReusableContentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getReusableContentInRevisionById( + params.spaceId, + params.revisionId, + params.reusableContentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getReusableContentUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; reusableContentId: string } -) => { - 'use cache'; - return getReusableContentUncached(input, params, true); -}; - -const getReusableContentUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; reusableContentId: string }, - withUseCache = false -) => { - return trace( - `getReusableContent.uncached(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getReusableContentInRevisionById( - params.spaceId, - params.revisionId, - params.reusableContentId - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getLatestOpenAPISpecVersionContent = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, - async ( - cacheKey, - input: DataFetcherInput, - params: { organizationId: string; slug: string } - ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getLatestOpenAPISpecVersionContentUseCache(input, params); - } + async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'openapi', + organization: params.organizationId, + openAPISpec: params.slug, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, async () => { - return getLatestOpenAPISpecVersionContentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [ - getCacheTag({ - tag: 'openapi', - organization: params.organizationId, - openAPISpec: params.slug, - }), - ], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getLatestOpenApiSpecVersionContent( + params.organizationId, + params.slug, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getLatestOpenAPISpecVersionContentUseCache = async ( - input: DataFetcherInput, - params: { organizationId: string; slug: string } -) => { - 'use cache'; - return getLatestOpenAPISpecVersionContentUncached(input, params, true); -}; - -const getLatestOpenAPISpecVersionContentUncached = async ( - input: DataFetcherInput, - params: { organizationId: string; slug: string }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'openapi', - organization: params.organizationId, - openAPISpec: params.slug, - }) - ); - } - - return trace( - `getLatestOpenAPISpecVersionContent.uncached(${params.organizationId}, ${params.slug})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getLatestOpenApiSpecVersionContent( - params.organizationId, - params.slug - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getPublishedContentSite = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { organizationId: string; siteId: string; siteShareKey: string | undefined } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getPublishedContentSiteUseCache(input, params); - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, async () => { - return getPublishedContentSiteUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [ - getCacheTag({ - tag: 'site', - site: params.siteId, - }), - ], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getPublishedContentSite( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getPublishedContentSiteUseCache = async ( - input: DataFetcherInput, - params: { organizationId: string; siteId: string; siteShareKey: string | undefined } -) => { - 'use cache'; - return getPublishedContentSiteUncached(input, params, true); -}; - -const getPublishedContentSiteUncached = async ( - input: DataFetcherInput, - params: { organizationId: string; siteId: string; siteShareKey: string | undefined }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - } - - return trace( - `getPublishedContentSite.uncached(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getPublishedContentSite( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - } - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - } - ); -}; - const getSiteRedirectBySource = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { organizationId: string; @@ -1108,293 +659,169 @@ const getSiteRedirectBySource = withCacheKey( source: string; } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getSiteRedirectBySourceUseCache(input, params); - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, async () => { - return getSiteRedirectBySourceUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [ - getCacheTag({ - tag: 'site', - site: params.siteId, - }), - ], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getSiteRedirectBySource( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + source: params.source, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getSiteRedirectBySourceUseCache = async ( - input: DataFetcherInput, - params: { - organizationId: string; - siteId: string; - siteShareKey: string | undefined; - source: string; - } -) => { - 'use cache'; - return getSiteRedirectBySourceUncached(input, params, true); -}; - -const getSiteRedirectBySourceUncached = async ( - input: DataFetcherInput, - params: { - organizationId: string; - siteId: string; - siteShareKey: string | undefined; - source: string; - }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - } - - return trace( - `getSiteRedirectBySource.uncached(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getSiteRedirectBySource( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - source: params.source, - } - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - } - ); -}; - const getEmbedByUrl = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, - async (cacheKey, input: DataFetcherInput, params: { spaceId: string; url: string }) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getEmbedByUrlUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getEmbedByUrlUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.weeks, - tags: [], - } + async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) ); - return uncached(); + return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getEmbedByUrlInSpace( + params.spaceId, + { + url: params.url, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('weeks'); + return res.data; + }); + }); } ) ); -const getEmbedByUrlUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; url: string } -) => { - 'use cache'; - return getEmbedByUrlUncached(input, params, true); -}; - -const getEmbedByUrlUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; url: string }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); - } - - return trace(`getEmbedByUrl.uncached(${params.spaceId}, ${params.url})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { - url: params.url, - }); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('weeks'); - } - return res.data; - }); - }); -}; - const searchSiteContent = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: Parameters[0] ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return searchSiteContentUseCache(input, params); - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, async () => { - return searchSiteContentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.hours, - tags: [], + return wrapDataFetcherError(async () => { + const { organizationId, siteId, query, scope } = params; + const api = apiClient(input); + const res = await api.orgs.searchSiteContent( + organizationId, + siteId, + { + query, + ...scope, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('hours'); + return res.data.items; + }); } ); - - return uncached(); } ) ); -const searchSiteContentUseCache = async ( - input: DataFetcherInput, - params: Parameters[0] -) => { - 'use cache'; - return searchSiteContentUncached(input, params, true); -}; - -const searchSiteContentUncached = async ( - input: DataFetcherInput, - params: Parameters[0], - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - } - - return trace( - `searchSiteContent.uncached(${params.organizationId}, ${params.siteId}, ${params.query})`, - async () => { - return wrapDataFetcherError(async () => { - const { organizationId, siteId, query, scope } = params; - const api = apiClient(input); - const res = await api.orgs.searchSiteContent(organizationId, siteId, { - query, - ...scope, - }); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('hours'); - } - return res.data.items; - }); - } - ); -}; - const renderIntegrationUi = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { integrationName: string; request: RenderIntegrationUI } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return renderIntegrationUiUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return renderIntegrationUiUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [ - getCacheTag({ - tag: 'integration', - integration: params.integrationName, - }), - ], - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'integration', + integration: params.integrationName, + }) ); - return uncached(); + return trace(`renderIntegrationUi(${params.integrationName})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.integrations.renderIntegrationUiWithPost( + params.integrationName, + params.request, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); } ) ); -const renderIntegrationUiUseCache = async ( - input: DataFetcherInput, - params: { integrationName: string; request: RenderIntegrationUI } -) => { - 'use cache'; - return renderIntegrationUiUncached(input, params, true); -}; - -const renderIntegrationUiUncached = ( - input: DataFetcherInput, - params: { integrationName: string; request: RenderIntegrationUI }, - withUseCache = false -) => { - return trace(`renderIntegrationUi.uncached(${params.integrationName})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.integrations.renderIntegrationUiWithPost( - params.integrationName, - params.request - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - }); -}; - async function* streamAIResponse( input: DataFetcherInput, params: Parameters[0] ) { const api = apiClient(input); - const res = await api.orgs.streamAiResponseInSite(params.organizationId, params.siteId, { - input: params.input, - output: params.output, - model: params.model, - }); + const res = await api.orgs.streamAiResponseInSite( + params.organizationId, + params.siteId, + { + input: params.input, + output: params.output, + model: params.model, + }, + { + ...noCacheFetchOptions, + } + ); for await (const event of res) { yield event; diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc index e316e2a242..7fbecf3bc6 100644 --- a/packages/gitbook-v2/wrangler.jsonc +++ b/packages/gitbook-v2/wrangler.jsonc @@ -2,7 +2,11 @@ "main": ".open-next/worker.js", "name": "gitbook-open-v2", "compatibility_date": "2025-04-14", - "compatibility_flags": ["nodejs_compat", "allow_importable_env"], + "compatibility_flags": [ + "nodejs_compat", + "allow_importable_env", + "global_fetch_strictly_public" + ], "assets": { "directory": ".open-next/assets", "binding": "ASSETS" diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts index d1f8c03cef..b496bbe59f 100644 --- a/packages/gitbook/src/lib/openapi/fetch.ts +++ b/packages/gitbook/src/lib/openapi/fetch.ts @@ -1,5 +1,4 @@ import { parseOpenAPI } from '@gitbook/openapi-parser'; -import { unstable_cache } from 'next/cache'; import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cache'; import type { @@ -10,7 +9,6 @@ import type { } from '@/lib/openapi/types'; import { getCloudflareRequestGlobal } from '@v2/lib/data/cloudflare'; import { withCacheKey, withoutConcurrentExecution } from '@v2/lib/data/memoize'; -import { GITBOOK_RUNTIME } from '@v2/lib/env'; import { assert } from 'ts-essentials'; import { resolveContentRef } from '../references'; import { isV2 } from '../v2'; @@ -71,18 +69,8 @@ const fetchFilesystemV1 = cache({ }); const fetchFilesystemV2 = withCacheKey( - withoutConcurrentExecution(getCloudflareRequestGlobal, async (cacheKey, url: string) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return fetchFilesystemUseCache(url); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache(async () => fetchFilesystemUncached(url), [cacheKey], { - revalidate: 60 * 60 * 24, - }); - - const response = await uncached(); - return response; + withoutConcurrentExecution(getCloudflareRequestGlobal, async (_cacheKey, url: string) => { + return fetchFilesystemUseCache(url); }) ); diff --git a/packages/gitbook/src/lib/tracing.ts b/packages/gitbook/src/lib/tracing.ts index 38db339240..71b5c072e8 100644 --- a/packages/gitbook/src/lib/tracing.ts +++ b/packages/gitbook/src/lib/tracing.ts @@ -28,19 +28,19 @@ export async function trace( }; const start = now(); - let failed = false; + let traceError: null | Error = null; try { return await fn(span); } catch (error) { span.setAttribute('error', true); - failed = true; + traceError = error as Error; throw error; } finally { if (process.env.SILENT !== 'true' && process.env.NODE_ENV !== 'development') { const end = now(); // biome-ignore lint/suspicious/noConsole: we want to log performance data console.log( - `trace ${completeName} ${failed ? 'failed' : 'succeeded'} in ${end - start}ms`, + `trace ${completeName} ${traceError ? `failed with ${traceError.message}` : 'succeeded'} in ${end - start}ms`, attributes ); }