From 6fe5ca45eba980c0d7718aabdf8d251d1b7f7fcf Mon Sep 17 00:00:00 2001 From: Stan Wohlwend Date: Sun, 14 Apr 2024 13:32:30 +0200 Subject: [PATCH] Report all errors to Sentry --- package.json | 2 +- .../src/app/api/sentry-example-api/route.js | 25 ++++++++++----- .../v1/auth/authorize/[provider]/route.tsx | 4 +-- .../api/v1/auth/callback/[provider]/route.tsx | 4 +-- .../app/api/v1/auth/forgot-password/route.tsx | 4 +-- .../v1/auth/send-verification-email/route.tsx | 9 ++---- .../src/app/api/v1/auth/signin/route.tsx | 6 ++-- packages/stack-server/src/app/layout.tsx | 2 ++ .../src/app/sentry-example-page/page.jsx | 2 +- .../src/components/page-overview.tsx | 6 ++-- packages/stack-server/src/lib/projects.tsx | 14 ++++----- .../stack-server/src/lib/route-handlers.tsx | 6 ++-- packages/stack-server/src/middleware.tsx | 2 ++ packages/stack-server/src/oauth/model.tsx | 7 +++-- .../stack-server/src/oauth/oauth-base.tsx | 3 +- packages/stack-server/src/polyfills.tsx | 13 ++++++++ packages/stack-server/src/stack.tsx | 2 ++ .../src/hooks/use-async-callback.tsx | 3 +- .../src/interface/clientInterface.ts | 24 ++++++-------- packages/stack-shared/src/known-errors.tsx | 6 ++-- packages/stack-shared/src/utils/errors.tsx | 31 +++++++++++++++++++ packages/stack-shared/src/utils/promises.tsx | 9 ++++-- packages/stack/src/lib/auth.ts | 6 ++-- packages/stack/src/lib/stack-app.ts | 7 ++--- 24 files changed, 127 insertions(+), 70 deletions(-) create mode 100644 packages/stack-server/src/polyfills.tsx diff --git a/package.json b/package.json index f2b4f3f33..2363b657d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build:docs": "turbo run build --no-cache --filter=stack-docs...", "build:server": "turbo run build --no-cache --filter=@stackframe/stack-server...", "build:demo": "turbo run build --no-cache --filter=demo-app...", - "clean": "turbo run clean --no-cache && rimraf node_modules", + "clean": "turbo run clean --no-cache && rimraf .turbo && rimraf node_modules", "codegen": "turbo run codegen --no-cache", "psql:server": "pnpm run --filter=@stackframe/stack-server psql", "prisma:server": "pnpm run --filter=@stackframe/stack-server prisma", diff --git a/packages/stack-server/src/app/api/sentry-example-api/route.js b/packages/stack-server/src/app/api/sentry-example-api/route.js index f486f3d1d..7384e6cea 100644 --- a/packages/stack-server/src/app/api/sentry-example-api/route.js +++ b/packages/stack-server/src/app/api/sentry-example-api/route.js @@ -1,9 +1,18 @@ -import { NextResponse } from "next/server"; +import { smartRouteHandler } from "@/lib/route-handlers"; +import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import * as yup from "yup"; -export const dynamic = "force-dynamic"; - -// A faulty API route to test Sentry's error monitoring -export function GET() { - throw new Error("Sentry Example API Route Error"); - return NextResponse.json({ data: "Testing Sentry Error..." }); -} +export const GET = smartRouteHandler({ + request: yup.object({ + method: yup.string().oneOf(["GET"]).required(), + }), + response: yup.object({ + statusCode: yup.number().oneOf([200]).required(), + bodyType: yup.string().oneOf(["text"]).required(), + body: yup.string().required(), + }), + handler: async (req) => { + console.error("hiya"); + throw new StackAssertionError("This is a test error", {abc: "smth", def: new Error("This is an error"), map: new Map([["key", "value"]])}); + }, +}); diff --git a/packages/stack-server/src/app/api/v1/auth/authorize/[provider]/route.tsx b/packages/stack-server/src/app/api/v1/auth/authorize/[provider]/route.tsx index d53a5792e..b22e2c371 100644 --- a/packages/stack-server/src/app/api/v1/auth/authorize/[provider]/route.tsx +++ b/packages/stack-server/src/app/api/v1/auth/authorize/[provider]/route.tsx @@ -3,7 +3,7 @@ import * as yup from "yup"; import { generators } from "openid-client"; import { cookies } from "next/headers"; import { encryptJWT } from "@stackframe/stack-shared/dist/utils/jwt"; -import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { deprecatedSmartRouteHandler, deprecatedParseRequest } from "@/lib/route-handlers"; import { getAuthorizationUrl } from "@/oauth"; import { getProject } from "@/lib/projects"; @@ -50,7 +50,7 @@ export const GET = deprecatedSmartRouteHandler(async (req: NextRequest, options: if (!project) { // This should never happen, make typescript happy - throw new Error("Project not found"); + throw new StackAssertionError("Project not found"); } const provider = project.evaluatedConfig.oauthProviders.find((p) => p.id === providerId); diff --git a/packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx b/packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx index 0ad2c3f49..16088b23c 100644 --- a/packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx +++ b/packages/stack-server/src/app/api/v1/auth/callback/[provider]/route.tsx @@ -2,7 +2,7 @@ import * as yup from "yup"; import { cookies } from "next/headers"; import { Request as OAuthRequest, Response as OAuthResponse } from "@node-oauth/oauth2-server"; import { NextRequest } from "next/server"; -import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { decryptJWT } from "@stackframe/stack-shared/dist/utils/jwt"; import { deprecatedSmartRouteHandler, deprecatedParseRequest as deprecatedParseRequest } from "@/lib/route-handlers"; import { getAuthorizationCallback, oauthServer } from "@/oauth"; @@ -69,7 +69,7 @@ export const GET = deprecatedSmartRouteHandler(async (req: NextRequest, options: if (!project) { // This should never happen, make typescript happy - throw new Error("Project not found"); + throw new StackAssertionError("Project not found"); } const provider = project.evaluatedConfig.oauthProviders.find((p) => p.id === providerId); diff --git a/packages/stack-server/src/app/api/v1/auth/forgot-password/route.tsx b/packages/stack-server/src/app/api/v1/auth/forgot-password/route.tsx index 279a77e34..1e33f3a85 100644 --- a/packages/stack-server/src/app/api/v1/auth/forgot-password/route.tsx +++ b/packages/stack-server/src/app/api/v1/auth/forgot-password/route.tsx @@ -6,7 +6,7 @@ import { sendPasswordResetEmail } from "@/email"; import { getApiKeySet, publishableClientKeyHeaderSchema } from "@/lib/api-keys"; import { getProject } from "@/lib/projects"; import { validateUrl } from "@/utils/url"; -import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { KnownErrors } from "@stackframe/stack-shared"; const postSchema = yup.object({ @@ -38,7 +38,7 @@ export const POST = deprecatedSmartRouteHandler(async (req: NextRequest) => { const project = await getProject(projectId); if (!project) { - throw new Error("Project not found"); // This should never happen, make typescript happy + throw new StackAssertionError("Project not found"); // This should never happen, make typescript happy } if (!project.evaluatedConfig.credentialEnabled) { diff --git a/packages/stack-server/src/app/api/v1/auth/send-verification-email/route.tsx b/packages/stack-server/src/app/api/v1/auth/send-verification-email/route.tsx index 12d71a93f..d58bbc5c6 100644 --- a/packages/stack-server/src/app/api/v1/auth/send-verification-email/route.tsx +++ b/packages/stack-server/src/app/api/v1/auth/send-verification-email/route.tsx @@ -54,12 +54,9 @@ const handler = deprecatedSmartRouteHandler(async (req: NextRequest) => { if (user.primaryEmailVerified) { throw new KnownErrors.EmailAlreadyVerified(); } - try { - await sendVerificationEmail(projectId, userId, emailVerificationRedirectUrl); - } catch (e) { - console.error(e); - throw e; - } + + await sendVerificationEmail(projectId, userId, emailVerificationRedirectUrl); + return NextResponse.json({}); }); export const POST = handler; diff --git a/packages/stack-server/src/app/api/v1/auth/signin/route.tsx b/packages/stack-server/src/app/api/v1/auth/signin/route.tsx index bddeaeb40..6b65dbbe4 100644 --- a/packages/stack-server/src/app/api/v1/auth/signin/route.tsx +++ b/packages/stack-server/src/app/api/v1/auth/signin/route.tsx @@ -7,7 +7,7 @@ import { deprecatedParseRequest, deprecatedSmartRouteHandler } from "@/lib/route import { encodeAccessToken } from "@/lib/access-token"; import { getApiKeySet, publishableClientKeyHeaderSchema } from "@/lib/api-keys"; import { getProject } from "@/lib/projects"; -import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { KnownErrors } from "@stackframe/stack-shared"; const postSchema = yup.object({ @@ -39,7 +39,7 @@ export const POST = deprecatedSmartRouteHandler(async (req: NextRequest) => { const project = await getProject(projectId); if (!project) { - throw new Error("Project not found"); // This should never happen, make typescript happy + throw new StackAssertionError("Project not found"); // This should never happen, make typescript happy } if (!project.evaluatedConfig.credentialEnabled) { @@ -58,7 +58,7 @@ export const POST = deprecatedSmartRouteHandler(async (req: NextRequest) => { } if (!user) { - throw new Error("This should never happen (the comparePassword call should've already caused this to fail)"); + throw new StackAssertionError("This should never happen (the comparePassword call should've already caused this to fail)"); } const refreshToken = generateSecureRandomString(); diff --git a/packages/stack-server/src/app/layout.tsx b/packages/stack-server/src/app/layout.tsx index e7dc5e52c..10ccb1e56 100644 --- a/packages/stack-server/src/app/layout.tsx +++ b/packages/stack-server/src/app/layout.tsx @@ -1,3 +1,5 @@ +import '../polyfills'; + import type { Metadata } from 'next'; import {GeistSans} from 'geist/font/sans'; import {GeistMono} from "geist/font/mono"; diff --git a/packages/stack-server/src/app/sentry-example-page/page.jsx b/packages/stack-server/src/app/sentry-example-page/page.jsx index ebdfe3336..4f0577dca 100644 --- a/packages/stack-server/src/app/sentry-example-page/page.jsx +++ b/packages/stack-server/src/app/sentry-example-page/page.jsx @@ -55,7 +55,7 @@ export default function Page() { }, async () => { const res = await fetch("/api/sentry-example-api"); if (!res.ok) { - throw new Error("Sentry Example Frontend Error"); + throw new Error("Sentry Example Frontend Error!!!!"); } }); }} diff --git a/packages/stack-server/src/components/page-overview.tsx b/packages/stack-server/src/components/page-overview.tsx index 02b4f920d..4c0160a69 100644 --- a/packages/stack-server/src/components/page-overview.tsx +++ b/packages/stack-server/src/components/page-overview.tsx @@ -1,8 +1,8 @@ import { Box } from "@mui/joy"; import { useCallback, useRef, useState } from "react"; -import { throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { deepPlainEquals } from "@stackframe/stack-shared/dist/utils/objects"; import { useMutationObserver } from "@/hooks/use-mutation-observer"; +import { throwStackErr } from "@stackframe/stack-shared/dist/utils/errors"; type Level = 1 | 2 | 3 | 4 | 5 | 6; function isLevel(level: number): level is Level { @@ -26,14 +26,14 @@ export function PageOverview(props: { children: React.ReactNode, onOverviewChang const [lastOverview, setLastOverview] = useState(null); const mutationCallback = useCallback((mutations: MutationRecord[] | "init") => { - const node = ref?.current ?? throwErr("mutation callback should never be called when ref is null!"); + const node = ref?.current ?? throwStackErr("mutation callback should never be called when ref is null!"); const headings = [...node.querySelectorAll("h1, h2, h3, h4, h5, h6")] as HTMLHeadingElement[]; const headingsWithId = headings.filter((heading) => !!heading.id); const sections: Section[] = []; for (const heading of headingsWithId) { const headingLevel = parseInt(heading.tagName[1]); - if (!isLevel(headingLevel)) throwErr("Invalid heading tag name " + heading.tagName); + if (!isLevel(headingLevel)) throwStackErr("Invalid heading tag name " + heading.tagName); let curSections = sections; while (curSections.length > 0) { diff --git a/packages/stack-server/src/lib/projects.tsx b/packages/stack-server/src/lib/projects.tsx index 5fca052eb..1e60dff42 100644 --- a/packages/stack-server/src/lib/projects.tsx +++ b/packages/stack-server/src/lib/projects.tsx @@ -7,7 +7,7 @@ import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; import { EmailConfigJson, SharedProvider, StandardProvider, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/interface/clientInterface"; import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; import { OAuthProviderUpdateOptions, ProjectUpdateOptions } from "@stackframe/stack-shared/dist/interface/adminInterface"; -import { throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError, captureError, throwStackErr } from "@stackframe/stack-shared/dist/utils/errors"; function toDBSharedProvider(type: SharedProvider): ProxiedOAuthProviderType { @@ -111,11 +111,11 @@ export async function isProjectAdmin(projectId: string, adminAccessToken: string function listProjectIds(projectUser: ServerUserJson) { const serverMetadata = projectUser.serverMetadata; if (typeof serverMetadata !== "object" || !(!serverMetadata || "managedProjectIds" in serverMetadata)) { - throw new Error("Invalid server metadata, did something go wrong?"); + throw new StackAssertionError("Invalid server metadata, did something go wrong?", { serverMetadata }); } const managedProjectIds = serverMetadata?.managedProjectIds ?? []; if (!isStringArray(managedProjectIds)) { - throw new Error("Invalid server metadata, did something go wrong? Expected string array"); + throw new StackAssertionError("Invalid server metadata, did something go wrong? Expected string array", { managedProjectIds }); } return managedProjectIds; @@ -263,7 +263,7 @@ export async function updateProject( const providerMap = new Map(oldProviders.map((provider) => [ provider.id, { - providerUpdate: oauthProvidersUpdate.find((p) => p.id === provider.id) ?? throwErr(`Missing provider update for provider '${provider.id}'`), + providerUpdate: oauthProvidersUpdate.find((p) => p.id === provider.id) ?? throwStackErr(`Missing provider update for provider '${provider.id}'`), oldProvider: provider, } ])); @@ -314,7 +314,7 @@ export async function updateProject( }, }; } else { - console.error(`Invalid provider type '${providerUpdate.type}'`); + throw new StackAssertionError(`Invalid provider type '${providerUpdate.type}'`, { providerUpdate }); } transaction.push(prismaClient.oAuthProviderConfig.update({ @@ -351,7 +351,7 @@ export async function updateProject( }, }; } else { - console.error(`Invalid provider type '${provider.update.type}'`); + throw new StackAssertionError(`Invalid provider type '${provider.update.type}'`, { provider }); } transaction.push(prismaClient.oAuthProviderConfig.create({ @@ -459,7 +459,7 @@ function projectJsonFromDbType(project: ProjectDB): ProjectJson { tenantId: provider.standardOAuthConfig.tenantId || undefined, }]; } - console.error(`Exactly one of the provider configs should be set on provider config '${provider.id}' of project '${project.id}'. Ignoring it`, { project }); + captureError("projectJsonFromDbType", new StackAssertionError(`Exactly one of the provider configs should be set on provider config '${provider.id}' of project '${project.id}'. Ignoring it`, { project })); return []; }), emailConfig, diff --git a/packages/stack-server/src/lib/route-handlers.tsx b/packages/stack-server/src/lib/route-handlers.tsx index 4dd51ea10..643c7856a 100644 --- a/packages/stack-server/src/lib/route-handlers.tsx +++ b/packages/stack-server/src/lib/route-handlers.tsx @@ -1,5 +1,7 @@ +import "../polyfills"; + import { NextRequest } from "next/server"; -import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import * as yup from "yup"; import { DeepPartial } from "@stackframe/stack-shared/dist/utils/objects"; import { Json } from "@stackframe/stack-shared/dist/utils/json"; @@ -204,7 +206,7 @@ function catchError(error: unknown): StatusError { } if (error instanceof StatusError) return error; - console.error(`Unhandled error in route handler:`, error); + captureError(`route-handler`, error); return new StatusError(StatusError.InternalServerError); } diff --git a/packages/stack-server/src/middleware.tsx b/packages/stack-server/src/middleware.tsx index 81267619b..4cf764d0e 100644 --- a/packages/stack-server/src/middleware.tsx +++ b/packages/stack-server/src/middleware.tsx @@ -1,3 +1,5 @@ +import './polyfills'; + import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { deindent } from '@stackframe/stack-shared/dist/utils/strings'; diff --git a/packages/stack-server/src/oauth/model.tsx b/packages/stack-server/src/oauth/model.tsx index 3d3501768..35fcd8a0d 100644 --- a/packages/stack-server/src/oauth/model.tsx +++ b/packages/stack-server/src/oauth/model.tsx @@ -6,6 +6,7 @@ import { decodeAccessToken, encodeAccessToken } from "@/lib/access-token"; import { validateUrl } from "@/utils/url"; import { checkApiKeySet } from "@/lib/api-keys"; import { getProject } from "@/lib/projects"; +import { captureError } from "@stackframe/stack-shared/dist/utils/errors"; const enabledScopes = ["openid"]; @@ -102,7 +103,7 @@ export class OAuthModel implements AuthorizationCodeModel { try { decoded = await decodeAccessToken(accessToken); } catch (e) { - console.error(e); + captureError("getAccessToken", e); return false; } @@ -222,8 +223,8 @@ export class OAuthModel implements AuthorizationCodeModel { return !!deletedCode; } catch (e) { - if (! (e instanceof PrismaClientKnownRequestError)) { - console.error(e); + if (!(e instanceof PrismaClientKnownRequestError)) { + throw e; } return false; } diff --git a/packages/stack-server/src/oauth/oauth-base.tsx b/packages/stack-server/src/oauth/oauth-base.tsx index 75e1aa384..c53a20dc3 100644 --- a/packages/stack-server/src/oauth/oauth-base.tsx +++ b/packages/stack-server/src/oauth/oauth-base.tsx @@ -86,8 +86,7 @@ export abstract class OAuthBaseProvider { tokenSet = await this.oauthClient.oauthCallback(this.redirectUri, callbackParams, params); } } catch (error) { - console.error("OAuth callback failed", error); - throw new Error("OAuth callback failed"); + throw new Error("OAuth callback failed", { cause: error }); } if (!tokenSet.access_token) { throw new Error("No access token received"); diff --git a/packages/stack-server/src/polyfills.tsx b/packages/stack-server/src/polyfills.tsx new file mode 100644 index 000000000..1e7e02324 --- /dev/null +++ b/packages/stack-server/src/polyfills.tsx @@ -0,0 +1,13 @@ +import { registerErrorSink } from "@stackframe/stack-shared/dist/utils/errors"; +import * as Sentry from "@sentry/nextjs"; + +const sentryErrorSink = (location: string, error: unknown) => { + console.log("YAAAA"); + Sentry.captureException(error, { extra: { location } }); +}; + +export function ensurePolyfilled() { + registerErrorSink(sentryErrorSink); +} + +ensurePolyfilled(); diff --git a/packages/stack-server/src/stack.tsx b/packages/stack-server/src/stack.tsx index a134eac45..d0c44d8b8 100644 --- a/packages/stack-server/src/stack.tsx +++ b/packages/stack-server/src/stack.tsx @@ -1,3 +1,5 @@ +import './polyfills'; + import { StackServerApp } from '@stackframe/stack'; export const stackServerApp = new StackServerApp({ diff --git a/packages/stack-shared/src/hooks/use-async-callback.tsx b/packages/stack-shared/src/hooks/use-async-callback.tsx index b85f9f8f7..ee25f681a 100644 --- a/packages/stack-shared/src/hooks/use-async-callback.tsx +++ b/packages/stack-shared/src/hooks/use-async-callback.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { captureError } from "../utils/errors"; export function useAsyncCallback( callback: (...args: A) => Promise, @@ -33,7 +34,7 @@ export function useAsyncCallbackWithLoggedError( try { return await callback(...args); } catch (e) { - console.error("Uncaught error in async callback", e); + captureError("async-callback", e); throw e; } }, deps); diff --git a/packages/stack-shared/src/interface/clientInterface.ts b/packages/stack-shared/src/interface/clientInterface.ts index e3dfcbd66..0f4d729e7 100644 --- a/packages/stack-shared/src/interface/clientInterface.ts +++ b/packages/stack-shared/src/interface/clientInterface.ts @@ -5,6 +5,7 @@ import { Result } from "../utils/results"; import { ReadonlyJson } from '../utils/json'; import { AsyncStore, ReadonlyAsyncStore } from '../utils/stores'; import { KnownError, KnownErrors } from '../known-errors'; +import { StackAssertionError } from '../utils/errors'; export type UserCustomizableJson = { readonly projectId: string, @@ -202,16 +203,14 @@ export class StackClientInterface { let challenges: oauth.WWWAuthenticateChallenge[] | undefined; if ((challenges = oauth.parseWwwAuthenticateChallenges(response.data))) { - for (const challenge of challenges) { - console.error('WWW-Authenticate Challenge', challenge); - } - throw new Error(); // Handle WWW-Authenticate Challenges as needed + // TODO Handle WWW-Authenticate Challenges as needed + throw new StackAssertionError("OAuth WWW-Authenticate challenge not implemented", { challenges }); } const result = await oauth.processRefreshTokenResponse(as, client, response.data); if (oauth.isOAuth2Error(result)) { - console.error('Error Response', result); - throw new Error(); // Handle OAuth 2.0 response body error + // TODO Handle OAuth 2.0 response body error + throw new StackAssertionError("OAuth error", { result }); } tokenStore.update(old => ({ @@ -602,8 +601,7 @@ export class StackClientInterface { }; const params = oauth.validateAuthResponse(as, client, oauthParams, state); if (oauth.isOAuth2Error(params)) { - console.error('Error validating OAuth response', params); - throw new Error("Error validating OAuth response"); // Handle OAuth 2.0 redirect error + throw new StackAssertionError("Error validating OAuth response", { params }); // Handle OAuth 2.0 redirect error } const response = await oauth.authorizationCodeGrantRequest( as, @@ -615,16 +613,14 @@ export class StackClientInterface { let challenges: oauth.WWWAuthenticateChallenge[] | undefined; if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { - for (const challenge of challenges) { - console.error('WWW-Authenticate Challenge', challenge); - } - throw new Error("WWW-Authenticate challenge not implemented"); // Handle WWW-Authenticate Challenges as needed + // TODO Handle WWW-Authenticate Challenges as needed + throw new StackAssertionError("OAuth WWW-Authenticate challenge not implemented", { challenges }); } const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response); if (oauth.isOAuth2Error(result)) { - console.error('Error Response', result); - throw new Error(); // Handle OAuth 2.0 response body error + // TODO Handle OAuth 2.0 response body error + throw new StackAssertionError("OAuth error", { result }); } tokenStore.update(old => ({ accessToken: result.access_token ?? null, diff --git a/packages/stack-shared/src/known-errors.tsx b/packages/stack-shared/src/known-errors.tsx index fd86c8652..1278f6fd7 100644 --- a/packages/stack-shared/src/known-errors.tsx +++ b/packages/stack-shared/src/known-errors.tsx @@ -1,5 +1,5 @@ -import { StatusError, throwErr } from "./utils/errors"; -import { identity, identityArgs } from "./utils/functions"; +import { StatusError, throwErr, throwStackErr } from "./utils/errors"; +import { identityArgs } from "./utils/functions"; import { Json } from "./utils/json"; export type KnownErrorJson = { @@ -48,7 +48,7 @@ export abstract class KnownError extends StatusError { } get errorCode(): string { - return (this.constructor as any).errorCode ?? throwErr(`Can't find error code for this KnownError. Is its constructor a KnownErrorConstructor? ${this}`); + return (this.constructor as any).errorCode ?? throwStackErr(`Can't find error code for this KnownError. Is its constructor a KnownErrorConstructor? ${this}`); } public static constructorArgsFromJson(json: KnownErrorJson): ConstructorParameters { diff --git a/packages/stack-shared/src/utils/errors.tsx b/packages/stack-shared/src/utils/errors.tsx index 5998a897c..19be58a95 100644 --- a/packages/stack-shared/src/utils/errors.tsx +++ b/packages/stack-shared/src/utils/errors.tsx @@ -1,5 +1,6 @@ import { Json } from "./json"; + export function throwErr(errorMessage: string): never; export function throwErr(error: Error): never; export function throwErr(...args: StatusErrorConstructorParameters): never; @@ -14,6 +15,36 @@ export function throwErr(...args: any[]): never { } } + +export class StackAssertionError extends Error { + constructor(message: string, public readonly extraData?: Record, options?: ErrorOptions) { + super(`${message}\n\nThis is likely an error in Stack. Please report it.`, options); + } +} + +export function throwStackErr(message: string, extraData?: any): never { + throw new StackAssertionError(message, extraData); +} + + +const errorSinks = new Set<(location: string, error: unknown) => void>(); +export function registerErrorSink(sink: (location: string, error: unknown) => void): void { + if (errorSinks.has(sink)) { + console.log("Error sink already registered", sink); + return; + } + console.log("Registering error sink", sink); + errorSinks.add(sink); +} +registerErrorSink((location, ...args) => console.error(`Error in ${location}:`, ...args)); + +export function captureError(location: string, error: unknown): void { + for (const sink of errorSinks) { + sink(location, error); + } +} + + type Status = { statusCode: number, message: string, diff --git a/packages/stack-shared/src/utils/promises.tsx b/packages/stack-shared/src/utils/promises.tsx index 6ce642738..e75b640d1 100644 --- a/packages/stack-shared/src/utils/promises.tsx +++ b/packages/stack-shared/src/utils/promises.tsx @@ -1,3 +1,4 @@ +import { StackAssertionError, captureError } from "./errors"; import { Result } from "./results"; import { generateUuid } from "./uuids"; import type { RejectedThenable, FulfilledThenable, PendingThenable } from "react"; @@ -105,15 +106,17 @@ export function runAsynchronously( } const duringError = new ErrorDuringRunAsynchronously(); promiseOrFunc?.catch(error => { - const newError = new Error( + const newError = new StackAssertionError( "Uncaught error in asynchronous function: " + error.toString(), + { + duringError, + }, { cause: error, } ); if (!options.ignoreErrors) { - console.error(newError); - console.error(duringError); + captureError("runAsynchronously", newError); } }); } diff --git a/packages/stack/src/lib/auth.ts b/packages/stack/src/lib/auth.ts index c270db2ea..a51cc5adb 100644 --- a/packages/stack/src/lib/auth.ts +++ b/packages/stack/src/lib/auth.ts @@ -3,6 +3,7 @@ import { saveVerifierAndState, getVerifierAndState } from "./cookie"; import { constructRedirectUrl } from "../utils/url"; import { TokenStore } from "@stackframe/stack-shared/dist/interface/clientInterface"; import { neverResolve } from "@stackframe/stack-shared/dist/utils/promises"; +import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; export async function signInWithOAuth( iface: StackClientInterface, @@ -73,7 +74,7 @@ export async function callOAuthCallback( // callOAuthCallback is called multiple times in parallel const { codeVerifier, state } = getVerifierAndState(); if (!codeVerifier || !state) { - throw new Error("Invalid OAuth callback URL"); + throw new Error("Invalid OAuth callback URL parameters. It seems like the OAuth flow was interrupted, so please try again."); } const originalUrl = consumeOAuthCallbackQueryParams(state); if (!originalUrl) return null; @@ -89,7 +90,6 @@ export async function callOAuthCallback( tokenStore, ); } catch (e) { - console.error("Error signing in during OAuth callback", e); - throw new Error("Error signing in. Please try again."); + throw new StackAssertionError("Error signing in during OAuth callback. Please try again.", { cause: e }); } } diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index ca282be15..dfff57282 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -1,7 +1,7 @@ import React, { use, useCallback, useMemo } from "react"; import { KnownErrors, OAuthProviderConfigJson, ServerUserCustomizableJson, ServerUserJson, StackAdminInterface, StackClientInterface, StackServerInterface } from "@stackframe/stack-shared"; import { getCookie, setOrDeleteCookie } from "./cookie"; -import { throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; import { AsyncResult, Result } from "@stackframe/stack-shared/dist/utils/results"; import { suspendIfSsr } from "@stackframe/stack-shared/dist/utils/react"; @@ -297,7 +297,7 @@ class _StackClientAppImpl