diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/logs/[logId]/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/logs/[logId]/page-client.tsx index 640882d7bb7..e6975e11f79 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/logs/[logId]/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/logs/[logId]/page-client.tsx @@ -7,7 +7,12 @@ import { PageContent } from "@/ui/layout/page-content"; import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper"; import { getStatusCodeBadgeVariant } from "@/ui/logs/log-utils"; import { UserAvatar } from "@/ui/users/user-avatar"; -import { CopyButton, StatusBadge, TimestampTooltip } from "@dub/ui"; +import { + CopyButton, + LoadingSpinner, + StatusBadge, + TimestampTooltip, +} from "@dub/ui"; import { ChevronRight, StackY3 } from "@dub/ui/icons"; import { fetcher, formatDateTime } from "@dub/utils"; import Link from "next/link"; @@ -25,12 +30,22 @@ export function LogDetailPageClient({ logId }: { logId: string }) { } = useSWR( workspaceId && `/api/logs/${logId}?workspaceId=${workspaceId}`, fetcher, + { + onErrorRetry: (error, _key, _config, revalidate, { retryCount }) => { + if (error.status === 404 && retryCount < 8) { + setTimeout(() => revalidate({ retryCount }), 2000); + return; + } + }, + }, ); + const isRetrying = error?.status === 404 && !log; + return ( ) : (
@@ -55,8 +70,16 @@ export function LogDetailPageClient({ logId }: { logId: string }) { {log ? ( - ) : isLoading ? ( - + ) : isLoading || isRetrying ? ( +
+ {isRetrying && ( +
+ + Retrieving log... this may take a few seconds +
+ )} + +
) : error ? (

diff --git a/apps/web/lib/api-logs/capture-request-log.ts b/apps/web/lib/api-logs/capture-request-log.ts index 51cc227063c..fe4279cd660 100644 --- a/apps/web/lib/api-logs/capture-request-log.ts +++ b/apps/web/lib/api-logs/capture-request-log.ts @@ -38,6 +38,7 @@ export function getRoutePattern(path: string): string { } export function captureRequestLog({ + requestId, req, response, workspace, @@ -47,6 +48,7 @@ export function captureRequestLog({ requestHeaders, startTime, }: { + requestId?: string; req: Request; response: Response; workspace: WorkspaceWithUsers; @@ -83,6 +85,7 @@ export function captureRequestLog({ } catch {} await recordApiLog({ + requestId, workspaceId: workspace.id, method: req.method, path: url.pathname, diff --git a/apps/web/lib/api-logs/record-api-log.ts b/apps/web/lib/api-logs/record-api-log.ts index b6fafd0df7b..b95a60c036b 100644 --- a/apps/web/lib/api-logs/record-api-log.ts +++ b/apps/web/lib/api-logs/record-api-log.ts @@ -12,6 +12,7 @@ const ingestionApiLogSchemaTB = apiLogSchemaTB.extend({ type ApiLogInput = z.infer; type RecordApiLogParams = { + requestId?: string; workspaceId: string; method: string; path: string; @@ -33,6 +34,7 @@ const recordApiLogTB = tb.buildIngestEndpoint({ }); export const recordApiLog = async ({ + requestId, workspaceId, method, path, @@ -46,8 +48,10 @@ export const recordApiLog = async ({ userId, requestType, }: RecordApiLogParams) => { + requestId = requestId ?? createId({ prefix: "req_" }); + const apiLog: ApiLogInput = { - id: createId({ prefix: "req_" }), + id: requestId, timestamp: new Date().toISOString(), workspace_id: workspaceId, method, diff --git a/apps/web/lib/api/errors.ts b/apps/web/lib/api/errors.ts index ec0d40e5c4c..eea90437ed7 100644 --- a/apps/web/lib/api/errors.ts +++ b/apps/web/lib/api/errors.ts @@ -34,6 +34,11 @@ const ErrorSchema = z.object({ description: "A URL to more information about the error code reported.", example: "https://dub.co/docs/api-reference", }), + request_log_url: z.string().optional().meta({ + description: "A URL to the request log for this error.", + example: + "https://app.dub.co/acme/settings/logs/req_1KP2VXTJ5ZHJAMZYTYX1YS1RN", + }), }), }); @@ -43,19 +48,23 @@ export type ErrorCodes = z.infer; export class DubApiError extends Error { public readonly code: z.infer; public readonly docUrl?: string; + public readonly requestLogUrl?: string; constructor({ code, message, docUrl, + requestLogUrl, }: { code: z.infer; message: string; docUrl?: string; + requestLogUrl?: string; }) { super(message); this.code = code; this.docUrl = docUrl ?? `${docErrorUrl}#${code.replace("_", "-")}`; + this.requestLogUrl = requestLogUrl; } } diff --git a/apps/web/lib/auth/workspace.ts b/apps/web/lib/auth/workspace.ts index d3efda76cb4..5aa55809a26 100644 --- a/apps/web/lib/auth/workspace.ts +++ b/apps/web/lib/auth/workspace.ts @@ -7,6 +7,7 @@ import { API_DOMAIN, getSearchParams } from "@dub/utils"; import { waitUntil } from "@vercel/functions"; import { headers } from "next/headers"; import { captureRequestLog } from "../api-logs/capture-request-log"; +import { createId } from "../api/create-id"; import { getRatelimitForPlan } from "../api/get-ratelimit-for-plan"; import { PermissionAction, @@ -91,6 +92,7 @@ export const withWorkspace = ( const params = (await initialParams) || {}; const searchParams = getSearchParams(req.url); + const requestId = createId({ prefix: "req_" }); let apiKey: string | undefined = undefined; let requestHeaders = await headers(); let responseHeaders = new Headers(); @@ -479,6 +481,7 @@ export const withWorkspace = ( if (workspace) { captureRequestLog({ + requestId, req: reqForLog, response, workspace, @@ -499,6 +502,7 @@ export const withWorkspace = ( if (workspace) { captureRequestLog({ + requestId, req: reqForLog, response: errorResponse, workspace,