-
Notifications
You must be signed in to change notification settings - Fork 5
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
Conversation
return withServerActionInstrumentation( | ||
"members/revalidateMemberPathsAndTags", | ||
{ | ||
headers: headers(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
}
There was a problem hiding this comment.
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.
62f50b5
to
67441b6
Compare
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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!
core/lib/serverActions.md
Outdated
There was a problem hiding this comment.
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
config/eslint/next.js
Outdated
"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", | ||
}, | ||
], |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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>>( |
There was a problem hiding this comment.
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)
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; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it!
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¬ification_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 |
See the server actions explainer for a description of the server actions-related changes.
This PR:
new Sentry.ReplayIntegration
toSentry.replayIntegration()
global-error.tsx
files to core and integration Next apps. This component should forward all unhandled, non- server action application errors to Sentry.defineServerAction
, which wraps server actions withSentry.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.ClientException
...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.ClientException
interface via a new hook calleduseServerAction
. This hook renders theClientException
error message and sentry id (if present).ClientException
instances where possible, and renders the error id in the exception's subsequent toast.Issue(s) Resolved
Resolves #299
Test Plan
if (env.NODE_ENV === "production")
check in thesentry.*.config.ts
files.Screenshots (if applicable)