Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap server actions in sentry HOF; add custom global-error files #306

Merged
merged 12 commits into from
Apr 9, 2024

Conversation

3mcd
Copy link
Member

@3mcd 3mcd commented Apr 4, 2024

See the server actions explainer for a description of the server actions-related changes.

This PR:

  • Updates Sentry to fix a very bizarre issue with only certain server actions.
  • Converts deprecated new Sentry.ReplayIntegration to Sentry.replayIntegration()
  • Adds root global-error.tsx files to core and integration Next apps. This component should forward all unhandled, non- server action application errors to Sentry.
  • Introduces defineServerAction, which wraps server actions with Sentry.withServerActionInstrumentation to properly forward uncaught errors in server actions to Sentry. Note, as of now, Sentry's higher-order-function isn't doing much since we're try/catching the result of the server action ourselves.
  • Provides a pattern for caught errors in server actions to be forwarded to Sentry with ClientException...
  • Adds a new interface called ClientException, which are exceptions sent from server actions to the client when something goes wrong in a server action. These objects should help us standardize how we structure error responses and render them on the client.
  • Refactors existing destructive/error toasts to use the newly added ClientException interface via a new hook called useServerAction. This hook renders the ClientException error message and sentry id (if present).
  • Adds the Sentry error id to ClientException instances where possible, and renders the error id in the exception's subsequent toast.

Issue(s) Resolved

Resolves #299

Test Plan

  • Checkout the branch and add some predictable errors in a few server action bodies, and in a couple of routes/pages.
  • Comment out the if (env.NODE_ENV === "production") check in the sentry.*.config.ts files.
  • Start the app.
  • Trigger the errors.
    • For server actions, you should see a destructive (red) toast when an error occurs. Depending on the server action, you may also see a Sentry error id that looks like the screenshot below.
    • For pages/routes, you should see the default Next error screen.
    • For both, you should see the errors reported in Sentry. Be sure to resolve/close your test errors in Sentry.

Screenshots (if applicable)

image

@3mcd 3mcd marked this pull request as ready for review April 4, 2024 15:05
@3mcd 3mcd requested review from kalilsn, tefkah and qweliant April 4, 2024 15:05
return withServerActionInstrumentation(
"members/revalidateMemberPathsAndTags",
{
headers: headers(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annoyingly, Sentry's withServerActionInstrumentation accepts a formData object (that must conform to the FormData API) but not an arbitrary payload. This option is completely useless to us since most of our server actions accept primitive/POJO parameters.

I did forward Next's headers along for some additional context, but that's about the best I could do.

Copy link
Member

@kalilsn kalilsn Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that we actually need to add anything in there right now, but is there a reason we couldn't just construct a formData object? like:

const formData = new FormData();
for (const [k, v] of arbitraryPayload.values()) {
	formData.append(k, v)
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would work, but since we pass multiple parameters and not just single objects with many keys, it could get tricky/annoying to have to name each one.

@3mcd 3mcd force-pushed the em/sentry-improvements branch from 62f50b5 to 67441b6 Compare April 4, 2024 20:52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll mark the files here going forward to ask about why not use defineServerAction in these places. if there is a good reason, no problem, but i think if we are going to introduce this pattern (which i think is good!) we implement it all in this PR unless we shouldn't for some reason

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the reason is "these are integrations and i don't care that much" that's a valid enough reason haha

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, the reason is "it's only defined in core", carry on

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, a little bit of both!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, this makes me feel somewhat more confident that we won't miss any errors!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for including specific documentation! very helpful

Comment on lines 7 to 14
"no-restricted-syntax": [
"error",
{
selector:
"CallExpression[callee.name='defineServerAction'] > :nth-child(1):not(FunctionExpression[id.name][async=true])",
message: "You can only pass named, async functions into defineServerAction",
},
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i added the eslint rule to this PR instead of doing a follow up, seems easier

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See here: https://eslint.org/play/#eyJ0ZXh0IjoiLyogZXNsaW50IG5vLXJlc3RyaWN0ZWQtc3ludGF4OiBbXCJlcnJvclwiLCB7IFwic2VsZWN0b3JcIjogXCJDYWxsRXhwcmVzc2lvbltjYWxsZWUubmFtZT0nZGVmaW5lU2VydmVyQWN0aW9uJ10gPiA6bnRoLWNoaWxkKDEpOm5vdChGdW5jdGlvbkV4cHJlc3Npb25baWQubmFtZV1bYXN5bmM9dHJ1ZV0pXCIsIFwibWVzc2FnZVwiOiBcIllvdSBjYW4gb25seSBwYXNzIG5hbWVkLCBhc3luYyBmdW5jdGlvbnMgaW50byBkZWZpbmVTZXJ2ZXJBY3Rpb25cIn1dICovXG5cbmNvbnN0IGNyZWF0ZVN0YWdlID0gZGVmaW5lU2VydmVyQWN0aW9uKGFzeW5jIChjb21tdW5pdHlJZCk9PiB7XG5cdHRyeSB7XG5cdFx0YXdhaXQgZGIuc3RhZ2UuY3JlYXRlKHtcblx0XHRcdGRhdGE6IHtcblx0XHRcdFx0bmFtZTogXCJVbnRpdGxlZCBTdGFnZVwiLFxuXHRcdFx0XHRvcmRlcjogXCJhYVwiLFxuXHRcdFx0XHRjb21tdW5pdHk6IHtcblx0XHRcdFx0XHRjb25uZWN0OiB7XG5cdFx0XHRcdFx0XHRpZDogY29tbXVuaXR5SWQsXG5cdFx0XHRcdFx0fSxcblx0XHRcdFx0fSxcblx0XHRcdH0sXG5cdFx0fSk7XG5cdH0gY2F0Y2ggKGVycm9yKSB7XG5cdFx0cmV0dXJuIG1ha2VDbGllbnRFeGNlcHRpb24oXCJGYWlsZWQgdG8gY3JlYXRlIHN0YWdlXCIsIGNhcHR1cmVFeGNlcHRpb24oZXJyb3IpKTtcblx0fSBmaW5hbGx5IHtcblx0XHRyZXZhbGlkYXRlVGFnKGBjb21tdW5pdHktc3RhZ2VzXyR7Y29tbXVuaXR5SWR9YCk7XG5cdH1cbn0pOyBcblxuY29uc3QgY3JlYXRlU3RhZ2UyID0gZGVmaW5lU2VydmVyQWN0aW9uKGFzeW5jIGZ1bmN0aW9uIGNyZWF0ZVN0YWdlKGNvbW11bml0eUlkKSB7XG5cdHRyeSB7XG5cdFx0YXdhaXQgZGIuc3RhZ2UuY3JlYXRlKHtcblx0XHRcdGRhdGE6IHtcblx0XHRcdFx0bmFtZTogXCJVbnRpdGxlZCBTdGFnZVwiLFxuXHRcdFx0XHRvcmRlcjogXCJhYVwiLFxuXHRcdFx0XHRjb21tdW5pdHk6IHtcblx0XHRcdFx0XHRjb25uZWN0OiB7XG5cdFx0XHRcdFx0XHRpZDogY29tbXVuaXR5SWQsXG5cdFx0XHRcdFx0fSxcblx0XHRcdFx0fSxcblx0XHRcdH0sXG5cdFx0fSk7XG5cdH0gY2F0Y2ggKGVycm9yKSB7XG5cdFx0cmV0dXJuIG1ha2VDbGllbnRFeGNlcHRpb24oXCJGYWlsZWQgdG8gY3JlYXRlIHN0YWdlXCIsIGNhcHR1cmVFeGNlcHRpb24oZXJyb3IpKTtcblx0fSBmaW5hbGx5IHtcblx0XHRyZXZhbGlkYXRlVGFnKGBjb21tdW5pdHktc3RhZ2VzXyR7Y29tbXVuaXR5SWR9YCk7XG5cdH1cbn0pO1xuXG5jb25zdCBjcmVhdGVTdGFnZTMgPSBkZWZpbmVTZXJ2ZXJBY3Rpb24oYXN5bmMgZnVuY3Rpb24oY29tbXVuaXR5SWQpIHtcblx0dHJ5IHtcblx0XHRhd2FpdCBkYi5zdGFnZS5jcmVhdGUoe1xuXHRcdFx0ZGF0YToge1xuXHRcdFx0XHRuYW1lOiBcIlVudGl0bGVkIFN0YWdlXCIsXG5cdFx0XHRcdG9yZGVyOiBcImFhXCIsXG5cdFx0XHRcdGNvbW11bml0eToge1xuXHRcdFx0XHRcdGNvbm5lY3Q6IHtcblx0XHRcdFx0XHRcdGlkOiBjb21tdW5pdHlJZCxcblx0XHRcdFx0XHR9LFxuXHRcdFx0XHR9LFxuXHRcdFx0fSxcblx0XHR9KTtcblx0fSBjYXRjaCAoZXJyb3IpIHtcblx0XHRyZXR1cm4gbWFrZUNsaWVudEV4Y2VwdGlvbihcIkZhaWxlZCB0byBjcmVhdGUgc3RhZ2VcIiwgY2FwdHVyZUV4Y2VwdGlvbihlcnJvcikpO1xuXHR9IGZpbmFsbHkge1xuXHRcdHJldmFsaWRhdGVUYWcoYGNvbW11bml0eS1zdGFnZXNfJHtjb21tdW5pdHlJZH1gKTtcblx0fVxufSk7XG5cbmNvbnN0IGNyZWF0ZVN0YWdlNCA9IGRlZmluZVNlcnZlckFjdGlvbihmdW5jdGlvbihjb21tdW5pdHlJZCkge1xuXHR0cnkge1xuXHRcdGRiLnN0YWdlLmNyZWF0ZSh7XG5cdFx0XHRkYXRhOiB7XG5cdFx0XHRcdG5hbWU6IFwiVW50aXRsZWQgU3RhZ2VcIixcblx0XHRcdFx0b3JkZXI6IFwiYWFcIixcblx0XHRcdFx0Y29tbXVuaXR5OiB7XG5cdFx0XHRcdFx0Y29ubmVjdDoge1xuXHRcdFx0XHRcdFx0aWQ6IGNvbW11bml0eUlkLFxuXHRcdFx0XHRcdH0sXG5cdFx0XHRcdH0sXG5cdFx0XHR9LFxuXHRcdH0pO1xuXHR9IGNhdGNoIChlcnJvcikge1xuXHRcdHJldHVybiBtYWtlQ2xpZW50RXhjZXB0aW9uKFwiRmFpbGVkIHRvIGNyZWF0ZSBzdGFnZVwiLCBjYXB0dXJlRXhjZXB0aW9uKGVycm9yKSk7XG5cdH0gZmluYWxseSB7XG5cdFx0cmV2YWxpZGF0ZVRhZyhgY29tbXVuaXR5LXN0YWdlc18ke2NvbW11bml0eUlkfWApO1xuXHR9XG59KTtcblxuLy8gd2UgbGV0IHR5cGVzY3JpcHQgaGFuZGxlIHRoaXNcbmNvbnN0IGNyZWF0ZVN0YWdlNSA9IGRlZmluZVNlcnZlckFjdGlvbihjcmVhdGVTdGFnZTIpIiwib3B0aW9ucyI6eyJydWxlcyI6e30sImxhbmd1YWdlT3B0aW9ucyI6eyJzb3VyY2VUeXBlIjoic2NyaXB0IiwicGFyc2VyT3B0aW9ucyI6eyJlY21hRmVhdHVyZXMiOnt9fX19fQ==

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(i also resolved the merge conflict that this created)

* @param serverActionFn
* @returns
*/
export const defineServerAction = <T extends (...args: unknown[]) => Promise<unknown>>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this into Promise<unknown> instead of unknown, to enforce the async-ness of the passed function on the type level as well. (ofc you can pass a non-async-promise-returning function as well, but it's at least a bit closer)

Comment on lines +38 to +55
export function useServerAction<T extends unknown[], U>(action: (...args: T) => Promise<U>) {
const runServerAction = useCallback(
async function runServerAction(...args: T) {
const result = await action(...args);
if (isClientException(result)) {
toast({
title: result.title ?? "Error",
variant: "destructive",
description: `${result.error}${result.id ? ` (Error ID: ${result.id})` : ""}`,
});
}
return result;
},
[action, toast]
);
return runServerAction;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it!

@tefkah
Copy link
Contributor

tefkah commented Apr 9, 2024

Seems to work perfectly!

I tested this by manually removing a stage in the DB and then trying to remove it in the editor UI, which generated this Sentry error (https://kfg.sentry.io/issues/5162958068/?referrer=alert_email&alert_type=email&alert_timestamp=1712656911734&alert_rule_id=14700197&notification_uuid=318664ac-f227-408e-af8f-e0c666a07456&environment=development) and showed a toast as intended.

Might be a bit hard to test every single server action, but I have faith that this is working as it should!

Page errors are also properly sent to sentry

Both were tested by just putting a throw new Error somewhere in the body of the page in a client and server component respectively

@3mcd 3mcd merged commit c204ca2 into main Apr 9, 2024
5 checks passed
@3mcd 3mcd deleted the em/sentry-improvements branch April 9, 2024 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants