From 3bebd3f4d1523ce6ccc647cffed3b869bf108128 Mon Sep 17 00:00:00 2001 From: Stan Wohlwend Date: Sat, 27 Jul 2024 13:24:42 -0700 Subject: [PATCH] Better RetryErrors --- apps/backend/src/app/health/route.tsx | 14 ++++ apps/backend/src/middleware.tsx | 18 ++--- apps/dashboard/src/app/health/route.tsx | 14 ++++ apps/dashboard/src/app/providers.tsx | 14 ++-- .../fern/docs/pages/getting-started/users.mdx | 9 ++- .../src/interface/clientInterface.ts | 77 +++++++++++++++++-- packages/stack-shared/src/utils/results.tsx | 13 +++- 7 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 apps/backend/src/app/health/route.tsx create mode 100644 apps/dashboard/src/app/health/route.tsx diff --git a/apps/backend/src/app/health/route.tsx b/apps/backend/src/app/health/route.tsx new file mode 100644 index 000000000..04a824d17 --- /dev/null +++ b/apps/backend/src/app/health/route.tsx @@ -0,0 +1,14 @@ +import { NextRequest } from "next/server"; + +export async function GET(req: NextRequest) { + return Response.json({ + status: "ok", + }, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Expose-Headers": "*", + }, + }); +} diff --git a/apps/backend/src/middleware.tsx b/apps/backend/src/middleware.tsx index 26f2e583c..102183513 100644 --- a/apps/backend/src/middleware.tsx +++ b/apps/backend/src/middleware.tsx @@ -13,7 +13,7 @@ const corsAllowedRequestHeaders = [ 'x-stack-client-version', // Project auth - 'x-stack-access-type', + //'x-stack-access-type', 'x-stack-publishable-client-key', 'x-stack-secret-server-key', 'x-stack-super-secret-admin-key', @@ -23,7 +23,7 @@ const corsAllowedRequestHeaders = [ 'x-stack-refresh-token', 'x-stack-access-token', - // Others + // Sentry 'baggage', 'sentry-trace', ]; @@ -40,17 +40,15 @@ export async function middleware(request: NextRequest) { const isApiRequest = url.pathname.startsWith('/api/'); // default headers - const responseInit: ResponseInit = { + const responseInit: ResponseInit | undefined = isApiRequest ? { headers: { // CORS headers - ...!isApiRequest ? {} : { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", - "Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '), - "Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '), - }, + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", + "Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '), + "Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '), }, - }; + } : undefined; // we want to allow preflight requests to pass through // even if the API route does not implement OPTIONS diff --git a/apps/dashboard/src/app/health/route.tsx b/apps/dashboard/src/app/health/route.tsx new file mode 100644 index 000000000..04a824d17 --- /dev/null +++ b/apps/dashboard/src/app/health/route.tsx @@ -0,0 +1,14 @@ +import { NextRequest } from "next/server"; + +export async function GET(req: NextRequest) { + return Response.json({ + status: "ok", + }, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Expose-Headers": "*", + }, + }); +} diff --git a/apps/dashboard/src/app/providers.tsx b/apps/dashboard/src/app/providers.tsx index 914e0a55f..aedc9ed36 100644 --- a/apps/dashboard/src/app/providers.tsx +++ b/apps/dashboard/src/app/providers.tsx @@ -5,11 +5,13 @@ import { PostHogProvider } from 'posthog-js/react'; import { Suspense, useEffect, useState } from 'react'; if (typeof window !== 'undefined') { - posthog.init("phc_vIUFi0HzHo7oV26OsaZbUASqxvs8qOmap1UBYAutU4k", { - api_host: "/consume", - ui_host: "https://eu.i.posthog.com", - person_profiles: 'identified_only', - }); + const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY ?? "phc_vIUFi0HzHo7oV26OsaZbUASqxvs8qOmap1UBYAutU4k"; + if (postHogKey.length > 5) { + posthog.init(postHogKey, { + api_host: "/consume", + ui_host: "https://eu.i.posthog.com", + }); + } } export function CSPostHogProvider({ children }: { children: React.ReactNode }) { return {children}; @@ -27,7 +29,7 @@ function UserIdentityInner() { if (user && user.id !== lastUserId) { posthog.identify(user.id, { primaryEmail: user.primaryEmail, - displayName: user.displayName, + displayName: user.displayName ?? user.primaryEmail ?? user.id, }); setLastUserId(user.id); } else if (!user && lastUserId) { diff --git a/docs/fern/docs/pages/getting-started/users.mdx b/docs/fern/docs/pages/getting-started/users.mdx index 2a4ce88ff..3c4c31ed3 100644 --- a/docs/fern/docs/pages/getting-started/users.mdx +++ b/docs/fern/docs/pages/getting-started/users.mdx @@ -59,13 +59,18 @@ Middleware can be used whenever it is easy to tell whether a page should be prot ```tsx title="middleware.tsx" export async function middleware(request: NextRequest) { - // You can add your own route protection logic here const user = await stackServerApp.getUser(); if (!user) { return NextResponse.redirect(new URL('/handler/sign-in', request.url)); } return NextResponse.next(); } + + export const config = { + // You can add your own route protection logic here + // Make sure not to protect the root URL, as it would prevent users from accessing static Next.js files or Stack's /handler path + matcher: '/protected/:path*', + }; ``` @@ -259,4 +264,4 @@ For more examples on how to use the `User` object, check the [CurrentUser object ## Next steps -In the next guide, we will show you how to put [your application into production](./production.mdx). \ No newline at end of file +In the next guide, we will show you how to put [your application into production](./production.mdx). diff --git a/packages/stack-shared/src/interface/clientInterface.ts b/packages/stack-shared/src/interface/clientInterface.ts index 3a3aa1647..ae230fbab 100644 --- a/packages/stack-shared/src/interface/clientInterface.ts +++ b/packages/stack-shared/src/interface/clientInterface.ts @@ -8,6 +8,7 @@ import { StackAssertionError, throwErr } from '../utils/errors'; import { globalVar } from '../utils/globals'; import { ReadonlyJson } from '../utils/json'; import { Result } from "../utils/results"; +import { deindent } from '../utils/strings'; import { CurrentUserCrud } from './crud/current-user'; import { ProviderAccessTokenCrud } from './crud/oauth'; import { InternalProjectsCrud, ProjectsCrud } from './crud/projects'; @@ -37,6 +38,54 @@ export class StackClientInterface { return this.options.baseUrl + "/api/v1"; } + public async runNetworkDiagnostics(session?: InternalSession | null, requestType?: "client" | "server" | "admin") { + const tryRequest = async (cb: () => Promise) => { + try { + await cb(); + return "OK"; + } catch (e) { + return `${e}`; + } + }; + const cfTrace = await tryRequest(async () => { + const res = await fetch("https://1.1.1.1/cdn-cgi/trace"); + if (!res.ok) { + throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`); + } + }); + const apiRoot = session !== undefined && requestType !== undefined ? await tryRequest(async () => { + const res = await this.sendClientRequestInner("/", {}, session!, requestType); + if (res.status === "error") { + throw res.error; + } + }) : "Not tested"; + const baseUrlBackend = await tryRequest(async () => { + const res = await fetch(new URL("/health", this.getApiUrl())); + if (!res.ok) { + throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`); + } + }); + const prodDashboard = await tryRequest(async () => { + const res = await fetch("https://app.stackframe.com/health"); + if (!res.ok) { + throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`); + } + }); + const prodBackend = await tryRequest(async () => { + const res = await fetch("https://api.stackframe.com/health"); + if (!res.ok) { + throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`); + } + }); + return { + cfTrace, + apiRoot, + baseUrlBackend, + prodDashboard, + prodBackend, + }; + } + public async fetchNewAccessToken(refreshToken: RefreshToken) { if (!('publishableClientKey' in this.options)) { // TODO support it @@ -98,13 +147,29 @@ export class StackClientInterface { }); - return await Result.orThrowAsync( - Result.retry( - () => this.sendClientRequestInner(path, requestOptions, session!, requestType), - 5, - { exponentialDelayBase: 1000 }, - ) + const retriedResult = await Result.retry( + () => this.sendClientRequestInner(path, requestOptions, session!, requestType), + 5, + { exponentialDelayBase: 1000 }, ); + + // try to diagnose the error for the user + if (retriedResult.status === "error") { + if (!navigator.onLine) { + throw new Error("Failed to send Stack request. It seems like you are offline. (window.navigator.onLine is falsy)", { cause: retriedResult.error }); + } + throw new Error(deindent` + Stack is unable to connect to the server. Please check your internet connection and try again. + + If the problem persists, please contact Stack support and provide a screenshot of your entire browser console. + + ${retriedResult.error} + + ${JSON.stringify(await this.runNetworkDiagnostics(session, requestType), null, 2)} + `, { cause: retriedResult.error }); + } + + return retriedResult.data; } public createSession(options: Omit[0], "refreshAccessTokenCallback">): InternalSession { diff --git a/packages/stack-shared/src/utils/results.tsx b/packages/stack-shared/src/utils/results.tsx index 85c4ea3d7..8984e1b6b 100644 --- a/packages/stack-shared/src/utils/results.tsx +++ b/packages/stack-shared/src/utils/results.tsx @@ -114,15 +114,20 @@ function mapResult(result: AsyncResult, class RetryError extends AggregateError { constructor(public readonly errors: unknown[]) { + const strings = errors.map(e => String(e)); + const isAllSame = strings.length > 1 && strings.every(s => s === strings[0]); super( errors, deindent` Error after retrying ${errors.length} times. - ${errors.map((e, i) => deindent` - Attempt ${i + 1}: - ${e} - `).join("\n\n")} + ${isAllSame ? deindent` + Attempts 1-${errors.length}: + ${errors[0]} + ` : errors.map((e, i) => deindent` + Attempt ${i + 1}: + ${e} + `).join("\n\n")} `, { cause: errors[errors.length - 1] } );