diff --git a/apps/web/utils/actions/ai-rule.ts b/apps/web/utils/actions/ai-rule.ts index 08bcbe3fd1..7e69baada8 100644 --- a/apps/web/utils/actions/ai-rule.ts +++ b/apps/web/utils/actions/ai-rule.ts @@ -12,7 +12,6 @@ import { import { getGmailClient } from "@/utils/gmail/client"; import { aiCreateRule } from "@/utils/ai/rule/create-rule"; import { - type TestResult, runRulesOnMessage, testRulesOnMessage, } from "@/utils/ai/choose-rule/run-rules"; @@ -27,11 +26,8 @@ import { GroupName } from "@/utils/config"; import type { EmailForAction } from "@/utils/ai/actions"; import { executeAct } from "@/utils/ai/choose-rule/execute"; import { isDefined, type ParsedMessage } from "@/utils/types"; -import { - executeServerAction, - getSessionAndGmailClient, -} from "@/utils/actions/helpers"; -import { type ServerActionResponse, isActionError } from "@/utils/error"; +import { getSessionAndGmailClient } from "@/utils/actions/helpers"; +import { isActionError } from "@/utils/error"; import { saveRulesPromptBody, type SaveRulesPromptBody, @@ -41,197 +37,195 @@ import { aiDiffRules } from "@/utils/ai/rule/diff-rules"; import { aiFindExistingRules } from "@/utils/ai/rule/find-existing-rules"; import { aiGenerateRulesPrompt } from "@/utils/ai/rule/generate-rules-prompt"; import { getLabels } from "@/utils/gmail/label"; +import { withActionInstrumentation } from "@/utils/actions/middleware"; -export async function runRulesAction( - email: EmailForAction, - force: boolean, -): Promise { - const { gmail, user: u, error } = await getSessionAndGmailClient(); - if (error) return { error }; - if (!gmail) return { error: "Could not load Gmail" }; - - const user = await prisma.user.findUnique({ - where: { id: u.id }, - select: { - id: true, - email: true, - about: true, - aiProvider: true, - aiModel: true, - aiApiKey: true, - rules: { - where: { enabled: true }, - include: { actions: true }, - }, - }, - }); - if (!user?.email) return { error: "User email not found" }; - - const [gmailMessage, hasExistingRule] = await Promise.all([ - getMessage(email.messageId, gmail, "full"), - prisma.executedRule.findUnique({ - where: { - unique_user_thread_message: { - userId: user.id, - threadId: email.threadId, - messageId: email.messageId, +export const runRulesAction = withActionInstrumentation( + "runRules", + async (email: EmailForAction, force: boolean) => { + const { gmail, user: u, error } = await getSessionAndGmailClient(); + if (error) return { error }; + if (!gmail) return { error: "Could not load Gmail" }; + + const user = await prisma.user.findUnique({ + where: { id: u.id }, + select: { + id: true, + email: true, + about: true, + aiProvider: true, + aiModel: true, + aiApiKey: true, + rules: { + where: { enabled: true }, + include: { actions: true }, }, }, - select: { id: true }, - }), - ]); + }); + if (!user?.email) return { error: "User email not found" }; + + const [gmailMessage, hasExistingRule] = await Promise.all([ + getMessage(email.messageId, gmail, "full"), + prisma.executedRule.findUnique({ + where: { + unique_user_thread_message: { + userId: user.id, + threadId: email.threadId, + messageId: email.messageId, + }, + }, + select: { id: true }, + }), + ]); - // fetch after getting the message to avoid rate limiting - const gmailThread = await getThread(email.threadId, gmail); + // fetch after getting the message to avoid rate limiting + const gmailThread = await getThread(email.threadId, gmail); - if (hasExistingRule && !force) { - console.log("Skipping. Rule already exists."); - return; - } + if (hasExistingRule && !force) { + console.log("Skipping. Rule already exists."); + return; + } - const message = parseMessage(gmailMessage); - const isThread = !!gmailThread.messages && gmailThread.messages.length > 1; + const message = parseMessage(gmailMessage); + const isThread = !!gmailThread.messages && gmailThread.messages.length > 1; - await runRulesOnMessage({ - gmail, - message, - rules: user.rules, - user: { ...user, email: user.email }, - isThread, - }); -} + await runRulesOnMessage({ + gmail, + message, + rules: user.rules, + user: { ...user, email: user.email }, + isThread, + }); + }, +); -export async function testAiAction({ - messageId, - threadId, -}: { - messageId: string; - threadId: string; -}): Promise> { - const { gmail, user: u, error } = await getSessionAndGmailClient(); - if (error) return { error }; - if (!gmail) return { error: "Could not load Gmail" }; - - const user = await prisma.user.findUnique({ - where: { id: u.id }, - select: { - id: true, - email: true, - about: true, - aiProvider: true, - aiModel: true, - aiApiKey: true, - rules: { - where: { enabled: true }, - include: { actions: true }, +export const testAiAction = withActionInstrumentation( + "testAi", + async ({ messageId, threadId }: { messageId: string; threadId: string }) => { + const { gmail, user: u, error } = await getSessionAndGmailClient(); + if (error) return { error }; + if (!gmail) return { error: "Could not load Gmail" }; + + const user = await prisma.user.findUnique({ + where: { id: u.id }, + select: { + id: true, + email: true, + about: true, + aiProvider: true, + aiModel: true, + aiApiKey: true, + rules: { + where: { enabled: true }, + include: { actions: true }, + }, }, - }, - }); - if (!user) return { error: "User not found" }; - - const [gmailMessage, gmailThread] = await Promise.all([ - getMessage(messageId, gmail, "full"), - getThread(threadId, gmail), - ]); - - const message = parseMessage(gmailMessage); - const isThread = !!gmailThread?.messages && gmailThread.messages.length > 1; - - const result = await testRulesOnMessage({ - gmail, - message, - rules: user.rules, - user: { ...user, email: user.email }, - isThread, - }); + }); + if (!user) return { error: "User not found" }; - return result; -} + const [gmailMessage, gmailThread] = await Promise.all([ + getMessage(messageId, gmail, "full"), + getThread(threadId, gmail), + ]); -export async function testAiCustomContentAction({ - content, -}: { - content: string; -}): Promise> { - const session = await auth(); - if (!session?.user.id) return { error: "Not logged in" }; - const gmail = getGmailClient(session); - - const user = await prisma.user.findUnique({ - where: { id: session.user.id }, - select: { - id: true, - email: true, - about: true, - aiProvider: true, - aiModel: true, - aiApiKey: true, - rules: { - where: { enabled: true }, - include: { actions: true }, + const message = parseMessage(gmailMessage); + const isThread = !!gmailThread?.messages && gmailThread.messages.length > 1; + + const result = await testRulesOnMessage({ + gmail, + message, + rules: user.rules, + user: { ...user, email: user.email }, + isThread, + }); + + return result; + }, +); + +export const testAiCustomContentAction = withActionInstrumentation( + "testAiCustomContent", + async ({ content }: { content: string }) => { + const session = await auth(); + if (!session?.user.id) return { error: "Not logged in" }; + const gmail = getGmailClient(session); + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { + id: true, + email: true, + about: true, + aiProvider: true, + aiModel: true, + aiApiKey: true, + rules: { + where: { enabled: true }, + include: { actions: true }, + }, }, - }, - }); - if (!user) return { error: "User not found" }; - - const result = await testRulesOnMessage({ - gmail, - message: { - id: "", - threadId: "", - snippet: content, - textPlain: content, - headers: { - date: new Date().toISOString(), - from: "", - to: "", - subject: "", + }); + if (!user) return { error: "User not found" }; + + const result = await testRulesOnMessage({ + gmail, + message: { + id: "", + threadId: "", + snippet: content, + textPlain: content, + headers: { + date: new Date().toISOString(), + from: "", + to: "", + subject: "", + }, + historyId: "", + inline: [], + internalDate: new Date().toISOString(), }, - historyId: "", - inline: [], - internalDate: new Date().toISOString(), - }, - rules: user.rules, - user, - isThread: false, - }); + rules: user.rules, + user, + isThread: false, + }); - return result; -} + return result; + }, +); -export async function createAutomationAction( - prompt: string, -): Promise> { - const session = await auth(); - const userId = session?.user.id; - if (!userId) return { error: "Not logged in" }; - - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - aiProvider: true, - aiModel: true, - aiApiKey: true, - email: true, - }, - }); - if (!user) return { error: "User not found" }; - if (!user.email) return { error: "User email not found" }; +export const createAutomationAction = withActionInstrumentation( + "createAutomation", + async (prompt: string) => { + const session = await auth(); + const userId = session?.user.id; + if (!userId) return { error: "Not logged in" }; - let result: Awaited>; + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + aiProvider: true, + aiModel: true, + aiApiKey: true, + email: true, + }, + }); + if (!user) return { error: "User not found" }; + if (!user.email) return { error: "User email not found" }; - try { - result = await aiCreateRule(prompt, user, user.email); - } catch (error: any) { - return { error: `AI error creating rule. ${error.message}` }; - } + let result: Awaited>; - if (!result) return { error: "AI error creating rule." }; + try { + result = await aiCreateRule(prompt, user, user.email); + } catch (error: any) { + return { error: `AI error creating rule. ${error.message}` }; + } - const groupIdResult = await getGroupId(result, userId); - if (isActionError(groupIdResult)) return groupIdResult; - return await safeCreateRule(result, userId, groupIdResult); -} + if (!result) return { error: "AI error creating rule." }; + + const groupIdResult = await getGroupId(result, userId); + if (isActionError(groupIdResult)) return groupIdResult; + return await safeCreateRule(result, userId, groupIdResult); + }, +); async function createRule( result: NonNullable>>, @@ -398,85 +392,87 @@ async function getGroupId( return groupId; } -export async function deleteRuleAction( - ruleId: string, -): Promise { - const session = await auth(); - if (!session?.user.id) return { error: "Not logged in" }; - - await prisma.rule.delete({ - where: { id: ruleId, userId: session.user.id }, - }); -} - -export async function setRuleAutomatedAction( - ruleId: string, - automate: boolean, -): Promise { - const session = await auth(); - if (!session?.user.id) return { error: "Not logged in" }; - - await prisma.rule.update({ - where: { id: ruleId, userId: session.user.id }, - data: { automate }, - }); -} +export const deleteRuleAction = withActionInstrumentation( + "deleteRule", + async (ruleId: string) => { + const session = await auth(); + if (!session?.user.id) return { error: "Not logged in" }; -export async function setRuleRunOnThreadsAction( - ruleId: string, - runOnThreads: boolean, -): Promise { - const session = await auth(); - if (!session?.user.id) return { error: "Not logged in" }; - - await prisma.rule.update({ - where: { id: ruleId, userId: session.user.id }, - data: { runOnThreads }, - }); -} - -export async function approvePlanAction( - executedRuleId: string, - message: ParsedMessage, -): Promise { - const session = await auth(); - if (!session?.user.email) return { error: "Not logged in" }; - - const gmail = getGmailClient(session); + await prisma.rule.delete({ + where: { id: ruleId, userId: session.user.id }, + }); + }, +); + +export const setRuleAutomatedAction = withActionInstrumentation( + "setRuleAutomated", + async (ruleId: string, automate: boolean) => { + const session = await auth(); + if (!session?.user.id) return { error: "Not logged in" }; + + await prisma.rule.update({ + where: { id: ruleId, userId: session.user.id }, + data: { automate }, + }); + }, +); + +export const setRuleRunOnThreadsAction = withActionInstrumentation( + "setRuleRunOnThreads", + async (ruleId: string, runOnThreads: boolean) => { + const session = await auth(); + if (!session?.user.id) return { error: "Not logged in" }; + + await prisma.rule.update({ + where: { id: ruleId, userId: session.user.id }, + data: { runOnThreads }, + }); + }, +); - const executedRule = await prisma.executedRule.findUnique({ - where: { id: executedRuleId }, - include: { actionItems: true }, - }); - if (!executedRule) return { error: "Item not found" }; - - await executeAct({ - gmail, - email: { - messageId: executedRule.messageId, - threadId: executedRule.threadId, - from: message.headers.from, - subject: message.headers.subject, - references: message.headers.references, - replyTo: message.headers["reply-to"], - headerMessageId: message.headers["message-id"] || "", - }, - executedRule, - userEmail: session.user.email, - }); -} +export const approvePlanAction = withActionInstrumentation( + "approvePlan", + async (executedRuleId: string, message: ParsedMessage) => { + const session = await auth(); + if (!session?.user.email) return { error: "Not logged in" }; -export async function rejectPlanAction( - executedRuleId: string, -): Promise { - const session = await auth(); - if (!session?.user.id) return { error: "Not logged in" }; + const gmail = getGmailClient(session); - await prisma.executedRule.updateMany({ - where: { id: executedRuleId, userId: session.user.id }, - data: { status: ExecutedRuleStatus.REJECTED }, - }); -} + const executedRule = await prisma.executedRule.findUnique({ + where: { id: executedRuleId }, + include: { actionItems: true }, + }); + if (!executedRule) return { error: "Item not found" }; + + await executeAct({ + gmail, + email: { + messageId: executedRule.messageId, + threadId: executedRule.threadId, + from: message.headers.from, + subject: message.headers.subject, + references: message.headers.references, + replyTo: message.headers["reply-to"], + headerMessageId: message.headers["message-id"] || "", + }, + executedRule, + userEmail: session.user.email, + }); + }, +); + +export const rejectPlanAction = withActionInstrumentation( + "rejectPlan", + async (executedRuleId: string) => { + const session = await auth(); + if (!session?.user.id) return { error: "Not logged in" }; + + await prisma.executedRule.updateMany({ + where: { id: executedRuleId, userId: session.user.id }, + data: { status: ExecutedRuleStatus.REJECTED }, + }); + }, +); /** * Saves the user's rules prompt and updates the rules accordingly. @@ -492,21 +488,15 @@ export async function rejectPlanAction( * 7. Update user's rules prompt in the database * 8. Return counts of created, edited, and removed rules */ -export async function saveRulesPromptAction( - unsafeData: SaveRulesPromptBody, -): Promise< - ServerActionResponse<{ - createdRules: number; - editedRules: number; - removedRules: number; - }> -> { - const session = await auth(); - if (!session?.user.email) return { error: "Not logged in" }; - setUser({ email: session.user.email }); - - return executeServerAction(async () => { - const data = saveRulesPromptBody.parse(unsafeData); +export const saveRulesPromptAction = withActionInstrumentation( + "saveRulesPrompt", + async (unsafeData: SaveRulesPromptBody) => { + const session = await auth(); + if (!session?.user.email) return { error: "Not logged in" }; + setUser({ email: session.user.email }); + + const { data, success, error } = saveRulesPromptBody.safeParse(unsafeData); + if (!success) return { error: error.message }; const user = await prisma.user.findUnique({ where: { id: session.user.id }, @@ -665,8 +655,8 @@ export async function saveRulesPromptAction( editedRules: editRulesCount, removedRules: removeRulesCount, }; - }, "Error saving rules"); -} + }, +); function shouldAutomate(actions: Pick[]) { const types = new Set(actions.map((action) => action.type)); @@ -691,52 +681,57 @@ function shouldAutomate(actions: Pick[]) { * 3. Calls an AI function to generate rule suggestions based on this data * 4. Returns the generated rules prompt as a string */ -export async function generateRulesPromptAction(): Promise< - ServerActionResponse<{ rulesPrompt: string }> -> { - const session = await auth(); - if (!session?.user.id) return { error: "Not logged in" }; - - const user = await prisma.user.findUnique({ - where: { id: session.user.id }, - select: { aiProvider: true, aiModel: true, aiApiKey: true, email: true }, - }); +export const generateRulesPromptAction = withActionInstrumentation( + "generateRulesPrompt", + async () => { + const session = await auth(); + if (!session?.user.id) return { error: "Not logged in" }; + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { aiProvider: true, aiModel: true, aiApiKey: true, email: true }, + }); - if (!user) return { error: "User not found" }; - if (!user.email) return { error: "User email not found" }; + if (!user) return { error: "User not found" }; + if (!user.email) return { error: "User email not found" }; - const gmail = getGmailClient(session); - const lastSent = await getMessages(gmail, { - query: "in:sent", - maxResults: 20, - }); - const gmailLabels = await getLabels(gmail); - const userLabels = gmailLabels?.filter((label) => label.type === "user"); - const lastSentMessages = await Promise.all( - lastSent.messages?.map(async (message) => { - const gmailMessage = await getMessage(message.id!, gmail); - return parseMessage(gmailMessage); - }) || [], - ); - const lastSentEmails = lastSentMessages?.map((message) => { - return emailToContent( - { - textHtml: message.textHtml || null, - textPlain: message.textPlain || null, - snippet: message.snippet || null, - }, - { maxLength: 500 }, - ); - }); + const gmail = getGmailClient(session); + const lastSent = await getMessages(gmail, { + query: "in:sent", + maxResults: 20, + }); + const gmailLabels = await getLabels(gmail); + const userLabels = gmailLabels?.filter((label) => label.type === "user"); + const lastSentMessages = ( + await Promise.all( + lastSent.messages?.map(async (message) => { + if (!message.id) return null; + const gmailMessage = await getMessage(message.id, gmail); + return parseMessage(gmailMessage); + }) || [], + ) + ).filter(isDefined); + const lastSentEmails = lastSentMessages?.map((message) => { + return emailToContent( + { + textHtml: message.textHtml || null, + textPlain: message.textPlain || null, + snippet: message.snippet || null, + }, + { maxLength: 500 }, + ); + }); - const result = await aiGenerateRulesPrompt({ - user: { ...user, email: user.email }, - lastSentEmails, - userLabels: userLabels?.map((label) => label.name).filter(isDefined) || [], - }); + const result = await aiGenerateRulesPrompt({ + user: { ...user, email: user.email }, + lastSentEmails, + userLabels: + userLabels?.map((label) => label.name).filter(isDefined) || [], + }); - if (isActionError(result)) return { error: result.error }; - if (!result) return { error: "Error generating rules prompt" }; + if (isActionError(result)) return { error: result.error }; + if (!result) return { error: "Error generating rules prompt" }; - return { rulesPrompt: result.join("\n\n") }; -} + return { rulesPrompt: result.join("\n\n") }; + }, +); diff --git a/apps/web/utils/actions/api-key.ts b/apps/web/utils/actions/api-key.ts index 4a8b12f6b1..da4809a58d 100644 --- a/apps/web/utils/actions/api-key.ts +++ b/apps/web/utils/actions/api-key.ts @@ -10,55 +10,57 @@ import type { CreateApiKeyBody, DeactivateApiKeyBody, } from "@/utils/actions/validation"; -import type { ServerActionResponse } from "@/utils/error"; import prisma from "@/utils/prisma"; import { generateSecureApiKey, hashApiKey } from "@/utils/api-key"; +import { withActionInstrumentation } from "@/utils/actions/middleware"; -export async function createApiKeyAction( - unsafeData: CreateApiKeyBody, -): Promise> { - const session = await auth(); - const userId = session?.user.id; - if (!userId) return { error: "Not logged in" }; +export const createApiKeyAction = withActionInstrumentation( + "createApiKey", + async (unsafeData: CreateApiKeyBody) => { + const session = await auth(); + const userId = session?.user.id; + if (!userId) return { error: "Not logged in" }; - const data = createApiKeyBody.safeParse(unsafeData); - if (!data.success) return { error: "Invalid data" }; + const { data, success, error } = createApiKeyBody.safeParse(unsafeData); + if (!success) return { error: error.message }; - console.log(`Creating API key for ${userId}`); + console.log(`Creating API key for ${userId}`); - const secretKey = generateSecureApiKey(); - const hashedKey = hashApiKey(secretKey); + const secretKey = generateSecureApiKey(); + const hashedKey = hashApiKey(secretKey); - await prisma.apiKey.create({ - data: { - userId, - name: data.data.name || "Secret key", - hashedKey, - isActive: true, - }, - }); + await prisma.apiKey.create({ + data: { + userId, + name: data.name || "Secret key", + hashedKey, + isActive: true, + }, + }); - revalidatePath("/settings"); + revalidatePath("/settings"); - return { secretKey }; -} + return { secretKey }; + }, +); -export async function deactivateApiKeyAction( - unsafeData: DeactivateApiKeyBody, -): Promise { - const session = await auth(); - const userId = session?.user.id; - if (!userId) return { error: "Not logged in" }; +export const deactivateApiKeyAction = withActionInstrumentation( + "deactivateApiKey", + async (unsafeData: DeactivateApiKeyBody) => { + const session = await auth(); + const userId = session?.user.id; + if (!userId) return { error: "Not logged in" }; - const data = deactivateApiKeyBody.safeParse(unsafeData); - if (!data.success) return { error: "Invalid data" }; + const { data, success, error } = deactivateApiKeyBody.safeParse(unsafeData); + if (!success) return { error: error.message }; - console.log(`Deactivating API key for ${userId}`); + console.log(`Deactivating API key for ${userId}`); - await prisma.apiKey.update({ - where: { id: data.data.id, userId }, - data: { isActive: false }, - }); + await prisma.apiKey.update({ + where: { id: data.id, userId }, + data: { isActive: false }, + }); - revalidatePath("/settings"); -} + revalidatePath("/settings"); + }, +); diff --git a/apps/web/utils/actions/categorize.ts b/apps/web/utils/actions/categorize.ts index 62b4f1d104..17228cf460 100644 --- a/apps/web/utils/actions/categorize.ts +++ b/apps/web/utils/actions/categorize.ts @@ -6,56 +6,57 @@ import { categoriseBodyWithHtml, } from "@/app/api/ai/categorise/validation"; import { getSessionAndGmailClient } from "@/utils/actions/helpers"; -import type { ServerActionResponse } from "@/utils/error"; import { hasPreviousEmailsFromSender } from "@/utils/gmail/message"; import { emailToContent } from "@/utils/mail"; import { findUnsubscribeLink } from "@/utils/parse/parseHtml.server"; import { truncate } from "@/utils/string"; import prisma from "@/utils/prisma"; - -export async function categorizeAction( - unsafeBody: CategoriseBodyWithHtml, -): Promise> { - const { gmail, user: u, error } = await getSessionAndGmailClient(); - if (error) return { error }; - if (!gmail) return { error: "Could not load Gmail" }; - - const { - success, - data, - error: parseError, - } = categoriseBodyWithHtml.safeParse(unsafeBody); - if (!success) return { error: parseError.message }; - - const content = emailToContent(data); - - const user = await prisma.user.findUnique({ - where: { id: u.id }, - select: { - aiProvider: true, - aiModel: true, - aiApiKey: true, - }, - }); - - if (!user) return { error: "User not found" }; - - const unsubscribeLink = findUnsubscribeLink(data.textHtml); - const hasPreviousEmail = await hasPreviousEmailsFromSender(gmail, data); - - const res = await categorise( - { - ...data, - content, - snippet: data.snippet || truncate(content, 300), - aiApiKey: user.aiApiKey, - aiProvider: user.aiProvider, - aiModel: user.aiModel, - unsubscribeLink, - hasPreviousEmail, - }, - { email: u.email! }, - ); - - return res; -} +import { withActionInstrumentation } from "@/utils/actions/middleware"; + +export const categorizeAction = withActionInstrumentation( + "categorize", + async (unsafeData: CategoriseBodyWithHtml) => { + const { gmail, user: u, error } = await getSessionAndGmailClient(); + if (error) return { error }; + if (!gmail) return { error: "Could not load Gmail" }; + + const { + success, + data, + error: parseError, + } = categoriseBodyWithHtml.safeParse(unsafeData); + if (!success) return { error: parseError.message }; + + const content = emailToContent(data); + + const user = await prisma.user.findUnique({ + where: { id: u.id }, + select: { + aiProvider: true, + aiModel: true, + aiApiKey: true, + }, + }); + + if (!user) return { error: "User not found" }; + + const unsubscribeLink = findUnsubscribeLink(data.textHtml); + const hasPreviousEmail = await hasPreviousEmailsFromSender(gmail, data); + + const res = await categorise( + { + ...data, + content, + snippet: data.snippet || truncate(content, 300), + aiApiKey: user.aiApiKey, + aiProvider: user.aiProvider, + aiModel: user.aiModel, + unsubscribeLink, + hasPreviousEmail, + }, + { email: u.email! }, + ); + + return res; + }, +); diff --git a/apps/web/utils/actions/helpers.ts b/apps/web/utils/actions/helpers.ts index 57f8579a92..843ceb4ed3 100644 --- a/apps/web/utils/actions/helpers.ts +++ b/apps/web/utils/actions/helpers.ts @@ -1,5 +1,5 @@ import { auth } from "@/app/api/auth/[...nextauth]/auth"; -import { captureException, type ServerActionResponse } from "@/utils/error"; +import { captureException } from "@/utils/error"; import { getGmailClient } from "@/utils/gmail/client"; // do not return functions to the client or we'll get an error @@ -13,24 +13,12 @@ export async function getSessionAndGmailClient() { } export function handleError( + actionName: string, error: unknown, message: string, - userEmail?: string, + userEmail: string, ) { - captureException(error, undefined, userEmail); + captureException(error, { extra: { message, actionName } }, userEmail); console.error(message, error); return { error: message }; } - -export async function executeServerAction( - action: () => Promise, - errorMessage: string, - userEmail?: string, -): Promise> { - try { - const result = await action(); - return result; - } catch (error) { - return handleError(error, errorMessage, userEmail); - } -} diff --git a/apps/web/utils/actions/mail.ts b/apps/web/utils/actions/mail.ts index 3a34d8ef96..74f9740f81 100644 --- a/apps/web/utils/actions/mail.ts +++ b/apps/web/utils/actions/mail.ts @@ -26,6 +26,7 @@ import { } from "@/utils/actions/helpers"; async function executeGmailAction( + actionName: string, action: ( gmail: gmail_v1.Gmail, user: { id: string; email: string }, @@ -40,11 +41,11 @@ async function executeGmailAction( try { const res = await action(gmail, user); return !isStatusOk(res.status) - ? handleError(res, errorMessage, user.email) + ? handleError(actionName, res, errorMessage, user.email) : undefined; } catch (error) { if (onError?.(error)) return; - return handleError(error, errorMessage, user.email); + return handleError(actionName, error, errorMessage, user.email); } } @@ -52,6 +53,7 @@ export async function archiveThreadAction( threadId: string, ): Promise { return executeGmailAction( + "archiveThread", async (gmail, user) => archiveThread({ gmail, @@ -67,6 +69,7 @@ export async function trashThreadAction( threadId: string, ): Promise { return executeGmailAction( + "trashThread", async (gmail, user) => trashThread({ gmail, @@ -82,6 +85,7 @@ export async function trashMessageAction( messageId: string, ): Promise { return executeGmailAction( + "trashMessage", async (gmail) => trashMessage({ gmail, messageId }), "Failed to delete message", ); @@ -92,6 +96,7 @@ export async function markReadThreadAction( read: boolean, ): Promise { return executeGmailAction( + "markReadThread", async (gmail) => markReadThread({ gmail, threadId, read }), "Failed to mark thread as read", ); @@ -102,6 +107,7 @@ export async function markImportantMessageAction( important: boolean, ): Promise { return executeGmailAction( + "markImportantMessage", async (gmail) => markImportantMessage({ gmail, messageId, important }), "Failed to mark message as important", ); @@ -111,6 +117,7 @@ export async function markSpamThreadAction( threadId: string, ): Promise { return executeGmailAction( + "markSpamThread", async (gmail) => markSpam({ gmail, threadId }), "Failed to mark thread as spam", ); @@ -121,6 +128,7 @@ export async function createAutoArchiveFilterAction( gmailLabelId?: string, ): Promise { return executeGmailAction( + "createAutoArchiveFilter", async (gmail) => createAutoArchiveFilter({ gmail, from, gmailLabelId }), "Failed to create auto archive filter", (error) => { @@ -136,6 +144,7 @@ export async function createFilterAction( gmailLabelId: string, ): Promise { return executeGmailAction( + "createFilter", async (gmail) => createFilter({ gmail, from, addLabelIds: [gmailLabelId] }), "Failed to create filter", ); @@ -145,6 +154,7 @@ export async function deleteFilterAction( id: string, ): Promise { return executeGmailAction( + "deleteFilter", async (gmail) => deleteFilter({ gmail, id }), "Failed to delete filter", ); diff --git a/apps/web/utils/actions/middleware.ts b/apps/web/utils/actions/middleware.ts new file mode 100644 index 0000000000..66221c7a82 --- /dev/null +++ b/apps/web/utils/actions/middleware.ts @@ -0,0 +1,17 @@ +import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { type ServerActionResponse } from "@/utils/error"; + +export function withActionInstrumentation( + name: string, + action: (...args: Args) => Promise>, + options?: { recordResponse?: boolean }, +) { + return (...args: Args) => + withServerActionInstrumentation( + name, + { + recordResponse: options?.recordResponse ?? true, + }, + () => action(...args), + ); +}