Report all errors to Sentry

This commit is contained in:
Stan Wohlwend 2024-04-14 13:32:30 +02:00
parent 9d733177d3
commit 6fe5ca45eb
24 changed files with 127 additions and 70 deletions

View File

@ -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",

View File

@ -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"]])});
},
});

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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;

View File

@ -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();

View File

@ -1,3 +1,5 @@
import '../polyfills';
import type { Metadata } from 'next';
import {GeistSans} from 'geist/font/sans';
import {GeistMono} from "geist/font/mono";

View File

@ -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!!!!");
}
});
}}

View File

@ -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<Overview | null>(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) {

View File

@ -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,

View File

@ -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);
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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");

View File

@ -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();

View File

@ -1,3 +1,5 @@
import './polyfills';
import { StackServerApp } from '@stackframe/stack';
export const stackServerApp = new StackServerApp({

View File

@ -1,4 +1,5 @@
import React from "react";
import { captureError } from "../utils/errors";
export function useAsyncCallback<A extends any[], R>(
callback: (...args: A) => Promise<R>,
@ -33,7 +34,7 @@ export function useAsyncCallbackWithLoggedError<A extends any[], R>(
try {
return await callback(...args);
} catch (e) {
console.error("Uncaught error in async callback", e);
captureError("async-callback", e);
throw e;
}
}, deps);

View File

@ -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,

View File

@ -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<typeof KnownError> {

View File

@ -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<string, any>, 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,

View File

@ -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);
}
});
}

View File

@ -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 });
}
}

View File

@ -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<HasTokenStore extends boolean, ProjectId extends strin
this._uniqueIdentifier = options.uniqueIdentifier ?? generateUuid();
if (allClientApps.has(this._uniqueIdentifier)) {
throw new Error("A Stack client app with the same unique identifier already exists");
throw new StackAssertionError("A Stack client app with the same unique identifier already exists");
}
allClientApps.set(this._uniqueIdentifier, [options.checkString ?? "default check string", this]);
@ -714,8 +714,7 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
if (existing) {
const [existingCheckString, clientApp] = existing;
if (existingCheckString !== providedCheckString) {
console.error("The provided app JSON does not match the configuration of the existing client app with the same unique identifier", { providedObj: json, existingString: existingCheckString });
throw new Error("The provided app JSON does not match the configuration of the existing client app with the same unique identifier");
throw new StackAssertionError("The provided app JSON does not match the configuration of the existing client app with the same unique identifier", { providedObj: json, existingString: existingCheckString });
}
return clientApp as any;
}