diff --git a/apps/web/app/api/google/webhook/route.ts b/apps/web/app/api/google/webhook/route.ts index 99a769e95e..48c6374408 100644 --- a/apps/web/app/api/google/webhook/route.ts +++ b/apps/web/app/api/google/webhook/route.ts @@ -53,6 +53,7 @@ async function processWebhookAsync( } catch (error) { await handleWebhookError(error, { email: decodedData.emailAddress, + emailAccountId: "unknown", // TODO: add emailAccountId url: "/api/google/webhook", logger, }); diff --git a/apps/web/app/api/outlook/webhook/route.ts b/apps/web/app/api/outlook/webhook/route.ts index 0017262fab..90a12ff0ef 100644 --- a/apps/web/app/api/outlook/webhook/route.ts +++ b/apps/web/app/api/outlook/webhook/route.ts @@ -104,6 +104,7 @@ async function processNotificationsAsync( if (emailAccount?.email) { await handleWebhookError(error, { email: emailAccount.email, + emailAccountId: emailAccount.id, url: "/api/outlook/webhook", logger, }); diff --git a/apps/web/utils/actions/middleware.ts b/apps/web/utils/actions/middleware.ts deleted file mode 100644 index 671ca07c6a..0000000000 --- a/apps/web/utils/actions/middleware.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { - captureException, - withServerActionInstrumentation, -} from "@sentry/nextjs"; -import { - checkCommonErrors, - isAICallError, - isAWSThrottlingError, - type ActionError, - type ServerActionResponse, -} from "@/utils/error"; -import { logErrorToPosthog } from "@/utils/error.server"; -import { isDuplicateError } from "@/utils/prisma-helpers"; -import { createScopedLogger } from "@/utils/logger"; -import { env } from "@/env"; - -// NOTE: this file is not longer in use but we want to move over functionality to the `actionClient` - -const logger = createScopedLogger("action-middleware"); - -// Utility type to ensure we're dealing with object types only -type EnsureObject = T extends object ? T : never; - -/** - * Wraps an action in instrumentation to capture errors and send them to Sentry - * We also make sure to always return an object, so that if the client doesn't get a response, - * it's because there was an error, and likely it was a timeout because otherwise it would receive the response. - * We also handle thrown errors and convert them to an object with an error message. - * Actions are expected to return { error: string } objects when they fail, but if an error is thrown, we handle it here. - * NOTE: future updates: we can do more when errors are thrown like we do with `withError` middleware for API routes. - */ -export function withActionInstrumentation< - Args extends any[], - Result extends object | undefined = undefined, - Err extends object = Record, ->( - name: string, - action: (...args: Args) => Promise>, - options?: { recordResponse?: boolean }, -) { - return async ( - ...args: Args - ): Promise< - ServerActionResponse & { success: boolean }, Err> - > => { - try { - const result = await withServerActionInstrumentation( - name, - { - recordResponse: options?.recordResponse ?? true, - }, - async () => { - try { - logger.info(`Action: ${name}`, { action: name }); - const res = await action(...args); - - if (!res) { - return { success: true } as EnsureObject & { - success: boolean; - }; - } - - if ("error" in res) return res; - - return { - success: true, - ...res, - } as unknown as EnsureObject & { - success: true; - }; - } catch (error) { - if (isDuplicateError(error)) { - captureException(error, { extra: { actionName: name } }); - - logger.error("Duplicate item error", { action: name, error }); - - return { - error: "Duplicate item error", - success: false, - } as unknown as ActionError; - } - - if (isAWSThrottlingError(error)) { - captureException(error, { extra: { actionName: name } }); - - logger.error("AWS throttling error", { action: name, error }); - - return { - error: error.message, - success: false, - } as unknown as ActionError; - } - - if (isAICallError(error)) { - // Quick fix: log full error in development. TODO: handle properly - if (env.NODE_ENV === "development") { - // biome-ignore lint/suspicious/noConsole: helpful for debugging - console.error(error); - } - - logger.error("AI call error", { - action: name, - error: (error.data as any)?.message, - }); - return { - error: - (error.data as any)?.error?.message ?? - "An error occurred while calling the AI", - success: false, - } as unknown as ActionError; - } - - // don't throw known errors to Sentry - const apiError = checkCommonErrors(error, name); - if (apiError) { - await logErrorToPosthog("action", name, apiError.type); - - logger.error("API error", { action: name, error: apiError }); - - return { - error: apiError.message, - success: false, - } as unknown as ActionError; - } - - throw error; - } - }, - ); - - return result; - } catch (error) { - logger.error("Error in action", { action: name, error }); - - // Quick fix: log full error in development. TODO: handle properly - if (env.NODE_ENV === "development") { - } - - // error is already captured by Sentry in `withServerActionInstrumentation` - return { - error: "An error occurred", - success: false, - } as ActionError; - } - }; -} diff --git a/apps/web/utils/ai/assistant/process-user-request.ts b/apps/web/utils/ai/assistant/process-user-request.ts index 9c594c8823..3fd8064d30 100644 --- a/apps/web/utils/ai/assistant/process-user-request.ts +++ b/apps/web/utils/ai/assistant/process-user-request.ts @@ -157,7 +157,7 @@ ${stringifyEmailSimple(getEmailForLLM(originalEmail))} const modelOptions = getModel(emailAccount.user, "chat"); const generateText = createGenerateText({ - userEmail: emailAccount.email, + emailAccount, label: "Process user request", modelOptions, }); diff --git a/apps/web/utils/ai/calendar/availability.ts b/apps/web/utils/ai/calendar/availability.ts index 5005ebabd1..d2e574b0a5 100644 --- a/apps/web/utils/ai/calendar/availability.ts +++ b/apps/web/utils/ai/calendar/availability.ts @@ -90,7 +90,7 @@ ${threadContent} const modelOptions = getModel(emailAccount.user); const generateText = createGenerateText({ - userEmail: emailAccount.email, + emailAccount, label: "Calendar availability analysis", modelOptions, }); diff --git a/apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts b/apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts index dd4fe02ddd..60e819439e 100644 --- a/apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts +++ b/apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts @@ -89,7 +89,7 @@ ${formatCategoriesForPrompt(categories)} const modelOptions = getModel(emailAccount.user, "economy"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Categorize senders bulk", modelOptions, }); diff --git a/apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts b/apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts index e5bac0f9e8..7206beea0e 100644 --- a/apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts +++ b/apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts @@ -49,7 +49,7 @@ ${formatCategoriesForPrompt(categories)} const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Categorize sender", modelOptions, }); diff --git a/apps/web/utils/ai/choose-rule/ai-choose-args.ts b/apps/web/utils/ai/choose-rule/ai-choose-args.ts index 6fd7791945..c1975d5853 100644 --- a/apps/web/utils/ai/choose-rule/ai-choose-args.ts +++ b/apps/web/utils/ai/choose-rule/ai-choose-args.ts @@ -85,7 +85,7 @@ export async function aiGenerateArgs({ const generateObject = createGenerateObject({ label: "Args for rule", - userEmail: emailAccount.email, + emailAccount, modelOptions, }); diff --git a/apps/web/utils/ai/choose-rule/ai-choose-rule.ts b/apps/web/utils/ai/choose-rule/ai-choose-rule.ts index 143989c3b3..28d151e76e 100644 --- a/apps/web/utils/ai/choose-rule/ai-choose-rule.ts +++ b/apps/web/utils/ai/choose-rule/ai-choose-rule.ts @@ -73,7 +73,7 @@ async function getAiResponse(options: GetAiResponseOptions): Promise<{ const modelOptions = getModel(emailAccount.user, modelType); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Choose rule", modelOptions, }); diff --git a/apps/web/utils/ai/choose-rule/ai-detect-recurring-pattern.ts b/apps/web/utils/ai/choose-rule/ai-detect-recurring-pattern.ts index 803f427838..7042184b1a 100644 --- a/apps/web/utils/ai/choose-rule/ai-detect-recurring-pattern.ts +++ b/apps/web/utils/ai/choose-rule/ai-detect-recurring-pattern.ts @@ -97,7 +97,7 @@ ${getEmailListPrompt({ messages: emails, messageMaxLength: 500 })} const modelOptions = getModel(emailAccount.user, "chat"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Detect recurring pattern", modelOptions, }); diff --git a/apps/web/utils/ai/clean/ai-clean-select-labels.ts b/apps/web/utils/ai/clean/ai-clean-select-labels.ts index be8739cc11..b091d6a4ad 100644 --- a/apps/web/utils/ai/clean/ai-clean-select-labels.ts +++ b/apps/web/utils/ai/clean/ai-clean-select-labels.ts @@ -30,7 +30,7 @@ ${instructions} const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Clean - Select Labels", modelOptions, }); diff --git a/apps/web/utils/ai/clean/ai-clean.ts b/apps/web/utils/ai/clean/ai-clean.ts index a9117095d0..5acc3ea4b3 100644 --- a/apps/web/utils/ai/clean/ai-clean.ts +++ b/apps/web/utils/ai/clean/ai-clean.ts @@ -93,7 +93,7 @@ The current date is ${currentDate}. const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Clean", modelOptions, }); diff --git a/apps/web/utils/ai/digest/summarize-email-for-digest.ts b/apps/web/utils/ai/digest/summarize-email-for-digest.ts index 96292b8dd2..b385bcacdf 100644 --- a/apps/web/utils/ai/digest/summarize-email-for-digest.ts +++ b/apps/web/utils/ai/digest/summarize-email-for-digest.ts @@ -81,7 +81,7 @@ ${getUserInfoPrompt({ emailAccount })}`; const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Summarize email", modelOptions, }); diff --git a/apps/web/utils/ai/group/create-group.ts b/apps/web/utils/ai/group/create-group.ts index 92b3b39e0c..bb49878293 100644 --- a/apps/web/utils/ai/group/create-group.ts +++ b/apps/web/utils/ai/group/create-group.ts @@ -84,7 +84,7 @@ Key guidelines: const modelOptions = getModel(emailAccount.user); const generateText = createGenerateText({ - userEmail: emailAccount.email, + emailAccount, label: "Create group", modelOptions, }); @@ -154,7 +154,7 @@ Guidelines: const modelOptions = getModel(emailAccount.user); const generateText = createGenerateText({ - userEmail: emailAccount.email, + emailAccount, label: "Verify group criteria", modelOptions, }); diff --git a/apps/web/utils/ai/knowledge/extract-from-email-history.ts b/apps/web/utils/ai/knowledge/extract-from-email-history.ts index 3753a164dc..2fef89674c 100644 --- a/apps/web/utils/ai/knowledge/extract-from-email-history.ts +++ b/apps/web/utils/ai/knowledge/extract-from-email-history.ts @@ -90,7 +90,7 @@ export async function aiExtractFromEmailHistory({ const modelOptions = getModel(emailAccount.user, "economy"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Email history extraction", modelOptions, }); diff --git a/apps/web/utils/ai/knowledge/extract.ts b/apps/web/utils/ai/knowledge/extract.ts index 09b2bb871b..a35d098e96 100644 --- a/apps/web/utils/ai/knowledge/extract.ts +++ b/apps/web/utils/ai/knowledge/extract.ts @@ -91,7 +91,7 @@ export async function aiExtractRelevantKnowledge({ const modelOptions = getModel(emailAccount.user, "economy"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Knowledge extraction", modelOptions, }); diff --git a/apps/web/utils/ai/knowledge/persona.ts b/apps/web/utils/ai/knowledge/persona.ts index 5050990174..134a5561e3 100644 --- a/apps/web/utils/ai/knowledge/persona.ts +++ b/apps/web/utils/ai/knowledge/persona.ts @@ -96,7 +96,7 @@ ${getEmailListPrompt({ messages: emails, messageMaxLength: 1000 })} const modelOptions = getModel(emailAccount.user, "economy"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Persona Analysis", modelOptions, }); diff --git a/apps/web/utils/ai/knowledge/writing-style.ts b/apps/web/utils/ai/knowledge/writing-style.ts index 38a83c0d4f..cf240a27c9 100644 --- a/apps/web/utils/ai/knowledge/writing-style.ts +++ b/apps/web/utils/ai/knowledge/writing-style.ts @@ -68,7 +68,7 @@ ${getUserInfoPrompt({ emailAccount })}`; const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Writing Style Analysis", modelOptions, }); diff --git a/apps/web/utils/ai/mcp/mcp-agent.ts b/apps/web/utils/ai/mcp/mcp-agent.ts index ea8d124f1d..419c5e95c8 100644 --- a/apps/web/utils/ai/mcp/mcp-agent.ts +++ b/apps/web/utils/ai/mcp/mcp-agent.ts @@ -58,7 +58,7 @@ ${getEmailListPrompt({ messages, messageMaxLength: 1000, maxMessages: 5 })} const modelOptions = getModel(emailAccount.user, "economy"); const generateText = createGenerateText({ - userEmail: emailAccount.email, + emailAccount, label: "MCP Agent", modelOptions, }); diff --git a/apps/web/utils/ai/reply/check-if-needs-reply.ts b/apps/web/utils/ai/reply/check-if-needs-reply.ts index 068aff2bc7..53053854a9 100644 --- a/apps/web/utils/ai/reply/check-if-needs-reply.ts +++ b/apps/web/utils/ai/reply/check-if-needs-reply.ts @@ -55,7 +55,7 @@ Decide if the message we are sending needs a reply. Respond with a JSON object w const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Check if needs reply", modelOptions, }); diff --git a/apps/web/utils/ai/reply/determine-thread-status.ts b/apps/web/utils/ai/reply/determine-thread-status.ts index 04d8532b3d..acaa63be65 100644 --- a/apps/web/utils/ai/reply/determine-thread-status.ts +++ b/apps/web/utils/ai/reply/determine-thread-status.ts @@ -107,7 +107,7 @@ Based on the full thread context above, determine the current status of this thr const modelOptions = getModel(emailAccount.user, modelType); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Determine thread status", modelOptions, }); diff --git a/apps/web/utils/ai/reply/draft-with-knowledge.ts b/apps/web/utils/ai/reply/draft-with-knowledge.ts index bf415bfd5f..3fe5cb31b8 100644 --- a/apps/web/utils/ai/reply/draft-with-knowledge.ts +++ b/apps/web/utils/ai/reply/draft-with-knowledge.ts @@ -187,7 +187,7 @@ export async function aiDraftWithKnowledge({ const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Email draft with knowledge", modelOptions, }); diff --git a/apps/web/utils/ai/reply/generate-nudge.ts b/apps/web/utils/ai/reply/generate-nudge.ts index 69b5f3707e..7002f4478a 100644 --- a/apps/web/utils/ai/reply/generate-nudge.ts +++ b/apps/web/utils/ai/reply/generate-nudge.ts @@ -32,7 +32,7 @@ IMPORTANT: The person you're writing an email for is: ${messages.at(-1)?.from}.` const generateText = createGenerateText({ label: "Reply", - userEmail: emailAccount.email, + emailAccount, modelOptions, }); diff --git a/apps/web/utils/ai/reply/reply-context-collector.ts b/apps/web/utils/ai/reply/reply-context-collector.ts index 996ad9194d..645bd84994 100644 --- a/apps/web/utils/ai/reply/reply-context-collector.ts +++ b/apps/web/utils/ai/reply/reply-context-collector.ts @@ -87,7 +87,7 @@ ${getTodayForLLM()}`; const modelOptions = getModel(emailAccount.user, "economy"); const generateText = createGenerateText({ - userEmail: emailAccount.email, + emailAccount, label: "Reply context collector", modelOptions, }); diff --git a/apps/web/utils/ai/report/analyze-email-behavior.ts b/apps/web/utils/ai/report/analyze-email-behavior.ts index ae6d7dd85e..b8aaef5f7b 100644 --- a/apps/web/utils/ai/report/analyze-email-behavior.ts +++ b/apps/web/utils/ai/report/analyze-email-behavior.ts @@ -60,7 +60,7 @@ Analyze the email patterns and identify: const modelOptions = getModel(emailAccount.user, "economy"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "email-report-email-behavior", modelOptions, }); diff --git a/apps/web/utils/ai/report/analyze-label-optimization.ts b/apps/web/utils/ai/report/analyze-label-optimization.ts index 30fb983a06..7e2dd60f11 100644 --- a/apps/web/utils/ai/report/analyze-label-optimization.ts +++ b/apps/web/utils/ai/report/analyze-label-optimization.ts @@ -55,7 +55,7 @@ Each suggestion should include the reason and expected impact.`; const modelOptions = getModel(emailAccount.user, "economy"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "email-report-label-analysis", modelOptions, }); diff --git a/apps/web/utils/ai/report/build-user-persona.ts b/apps/web/utils/ai/report/build-user-persona.ts index 7fff7c5bb2..2383665bd4 100644 --- a/apps/web/utils/ai/report/build-user-persona.ts +++ b/apps/web/utils/ai/report/build-user-persona.ts @@ -67,7 +67,7 @@ Analyze the data and identify: const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "email-report-user-persona", modelOptions, }); diff --git a/apps/web/utils/ai/report/generate-actionable-recommendations.ts b/apps/web/utils/ai/report/generate-actionable-recommendations.ts index 2be62583bb..53add316d9 100644 --- a/apps/web/utils/ai/report/generate-actionable-recommendations.ts +++ b/apps/web/utils/ai/report/generate-actionable-recommendations.ts @@ -62,7 +62,7 @@ Focus on practical, implementable solutions that improve email organization and const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "email-report-actionable-recommendations", modelOptions, }); diff --git a/apps/web/utils/ai/report/generate-executive-summary.ts b/apps/web/utils/ai/report/generate-executive-summary.ts index 5c6e9668a1..89bf6c1d3c 100644 --- a/apps/web/utils/ai/report/generate-executive-summary.ts +++ b/apps/web/utils/ai/report/generate-executive-summary.ts @@ -140,7 +140,7 @@ Generate: const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "email-report-executive-summary", modelOptions, }); diff --git a/apps/web/utils/ai/report/response-patterns.ts b/apps/web/utils/ai/report/response-patterns.ts index c0973d0bd9..3ddc172ac4 100644 --- a/apps/web/utils/ai/report/response-patterns.ts +++ b/apps/web/utils/ai/report/response-patterns.ts @@ -91,7 +91,7 @@ Only suggest categories that are meaningful and provide clear organizational val const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "email-report-response-patterns", modelOptions, }); diff --git a/apps/web/utils/ai/report/summarize-emails.ts b/apps/web/utils/ai/report/summarize-emails.ts index 7b11934bd8..c04e934009 100644 --- a/apps/web/utils/ai/report/summarize-emails.ts +++ b/apps/web/utils/ai/report/summarize-emails.ts @@ -83,7 +83,7 @@ Return the analysis as a JSON array of objects.`; const modelOptions = getModel(emailAccount.user, "economy"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "email-report-summary-generation", modelOptions, }); diff --git a/apps/web/utils/ai/rule/create-rule.ts b/apps/web/utils/ai/rule/create-rule.ts index 1a83d7e609..aa8c25385c 100644 --- a/apps/web/utils/ai/rule/create-rule.ts +++ b/apps/web/utils/ai/rule/create-rule.ts @@ -17,7 +17,7 @@ export async function aiCreateRule( const modelOptions = getModel(emailAccount.user); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Categorize rule", modelOptions, }); diff --git a/apps/web/utils/ai/rule/diff-rules.ts b/apps/web/utils/ai/rule/diff-rules.ts index d0a8172f6b..50deb3842f 100644 --- a/apps/web/utils/ai/rule/diff-rules.ts +++ b/apps/web/utils/ai/rule/diff-rules.ts @@ -60,7 +60,7 @@ Return the result in JSON format. Do not include any other text in your response const modelOptions = getModel(emailAccount.user, "chat"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Diff rules", modelOptions, }); diff --git a/apps/web/utils/ai/rule/find-existing-rules.ts b/apps/web/utils/ai/rule/find-existing-rules.ts index 48be8e2159..a29f7a0873 100644 --- a/apps/web/utils/ai/rule/find-existing-rules.ts +++ b/apps/web/utils/ai/rule/find-existing-rules.ts @@ -47,7 +47,7 @@ Please return the existing rules that match the prompt rules in JSON format. const modelOptions = getModel(emailAccount.user, "chat"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Find existing rules", modelOptions, }); diff --git a/apps/web/utils/ai/rule/generate-rules-prompt.ts b/apps/web/utils/ai/rule/generate-rules-prompt.ts index a1eb4ea54a..624cdd5107 100644 --- a/apps/web/utils/ai/rule/generate-rules-prompt.ts +++ b/apps/web/utils/ai/rule/generate-rules-prompt.ts @@ -106,7 +106,7 @@ IMPORTANT: Do not create overly specific rules that only occur on a one off basi const modelOptions = getModel(emailAccount.user, "chat"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Generate rules prompt", modelOptions, }); diff --git a/apps/web/utils/ai/rule/prompt-to-rules-old.ts b/apps/web/utils/ai/rule/prompt-to-rules-old.ts index 80b8acf1bf..10d9a5a650 100644 --- a/apps/web/utils/ai/rule/prompt-to-rules-old.ts +++ b/apps/web/utils/ai/rule/prompt-to-rules-old.ts @@ -44,7 +44,7 @@ ${cleanedPromptFile} const modelOptions = getModel(emailAccount.user, "chat"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Prompt to rules", modelOptions, }); diff --git a/apps/web/utils/ai/rule/prompt-to-rules.ts b/apps/web/utils/ai/rule/prompt-to-rules.ts index b7a69861fa..41f6ad0544 100644 --- a/apps/web/utils/ai/rule/prompt-to-rules.ts +++ b/apps/web/utils/ai/rule/prompt-to-rules.ts @@ -31,7 +31,7 @@ ${cleanedPromptFile} const modelOptions = getModel(emailAccount.user, "chat"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Prompt to rules", modelOptions, }); diff --git a/apps/web/utils/ai/snippets/find-snippets.ts b/apps/web/utils/ai/snippets/find-snippets.ts index 961a3f44bc..c33dac91bf 100644 --- a/apps/web/utils/ai/snippets/find-snippets.ts +++ b/apps/web/utils/ai/snippets/find-snippets.ts @@ -51,7 +51,7 @@ ${getEmailListPrompt({ messages: sentEmails, messageMaxLength: 2000 })}`; const modelOptions = getModel(emailAccount.user, "chat"); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "ai-find-snippets", modelOptions, }); diff --git a/apps/web/utils/calendar/client.ts b/apps/web/utils/calendar/client.ts index 195f963f5b..9cfaa33273 100644 --- a/apps/web/utils/calendar/client.ts +++ b/apps/web/utils/calendar/client.ts @@ -47,7 +47,10 @@ export const getCalendarClientWithRefresh = async ({ expiresAt: number | null; emailAccountId: string; }): Promise => { - if (!refreshToken) throw new SafeError("No refresh token"); + if (!refreshToken) { + logger.error("No refresh token", { emailAccountId }); + throw new SafeError("No refresh token"); + } // Check if token is still valid if (expiresAt && expiresAt > Date.now()) { diff --git a/apps/web/utils/cold-email/is-cold-email.ts b/apps/web/utils/cold-email/is-cold-email.ts index 141641d577..ca3f84c2eb 100644 --- a/apps/web/utils/cold-email/is-cold-email.ts +++ b/apps/web/utils/cold-email/is-cold-email.ts @@ -142,7 +142,7 @@ ${stringifyEmail(email, 500)} const modelOptions = getModel(emailAccount.user, modelType); const generateObject = createGenerateObject({ - userEmail: emailAccount.email, + emailAccount, label: "Cold email check", modelOptions, }); diff --git a/apps/web/utils/error.server.ts b/apps/web/utils/error.server.ts index b7aed9a2b4..0510e4b88f 100644 --- a/apps/web/utils/error.server.ts +++ b/apps/web/utils/error.server.ts @@ -9,12 +9,19 @@ export async function logErrorToPosthog( type: "api" | "action", url: string, errorType: string, + emailAccountId: string, ) { try { const session = await auth(); if (session?.user.email) { setUser({ email: session.user.email }); - await trackError({ email: session.user.email, errorType, type, url }); + await trackError({ + email: session.user.email, + emailAccountId, + errorType, + type, + url, + }); } } catch (error) { logger.error("Error logging to PostHog:", { error }); diff --git a/apps/web/utils/gmail/client.ts b/apps/web/utils/gmail/client.ts index 42449e1a0c..6af17ff9e5 100644 --- a/apps/web/utils/gmail/client.ts +++ b/apps/web/utils/gmail/client.ts @@ -57,7 +57,10 @@ export const getGmailClientWithRefresh = async ({ expiresAt: number | null; emailAccountId: string; }): Promise => { - if (!refreshToken) throw new SafeError("No refresh token"); + if (!refreshToken) { + logger.error("No refresh token", { emailAccountId }); + throw new SafeError("No refresh token"); + } // we handle refresh ourselves so not passing in expiresAt const auth = getAuth({ accessToken, refreshToken }); diff --git a/apps/web/utils/llms/index.ts b/apps/web/utils/llms/index.ts index 13fd7fc0b1..a24ea7a7bd 100644 --- a/apps/web/utils/llms/index.ts +++ b/apps/web/utils/llms/index.ts @@ -15,7 +15,7 @@ import { import { jsonrepair } from "jsonrepair"; import type { LanguageModelV2 } from "@ai-sdk/provider"; import { saveAiUsage } from "@/utils/usage"; -import type { UserAIFields } from "@/utils/llms/types"; +import type { EmailAccountWithAI, UserAIFields } from "@/utils/llms/types"; import { addUserErrorMessage, ErrorType } from "@/utils/error-messages"; import { captureException, @@ -42,11 +42,11 @@ const commonOptions: { } = { experimental_telemetry: { isEnabled: true } }; export function createGenerateText({ - userEmail, + emailAccount, label, modelOptions, }: { - userEmail: string; + emailAccount: Pick; label: string; modelOptions: ReturnType; }): typeof generateText { @@ -71,7 +71,7 @@ export function createGenerateText({ if (result.usage) { await saveAiUsage({ - email: userEmail, + email: emailAccount.email, usage: result.usage, provider: modelOptions.provider, model: modelOptions.modelName, @@ -105,23 +105,35 @@ export function createGenerateText({ try { return await generate(modelOptions.backupModel); } catch (error) { - await handleError(error, userEmail, label, modelOptions.modelName); + await handleError( + error, + emailAccount.email, + emailAccount.id, + label, + modelOptions.modelName, + ); throw error; } } - await handleError(error, userEmail, label, modelOptions.modelName); + await handleError( + error, + emailAccount.email, + emailAccount.id, + label, + modelOptions.modelName, + ); throw error; } }; } export function createGenerateObject({ - userEmail, + emailAccount, label, modelOptions, }: { - userEmail: string; + emailAccount: Pick; label: string; modelOptions: ReturnType; }): typeof generateObject { @@ -158,7 +170,7 @@ export function createGenerateObject({ if (result.usage) { await saveAiUsage({ - email: userEmail, + email: emailAccount.email, usage: result.usage, provider: modelOptions.provider, model: modelOptions.modelName, @@ -173,7 +185,13 @@ export function createGenerateObject({ return result; } catch (error) { - await handleError(error, userEmail, label, modelOptions.modelName); + await handleError( + error, + emailAccount.email, + emailAccount.id, + label, + modelOptions.modelName, + ); throw error; } }; @@ -265,10 +283,17 @@ export async function chatCompletionStream({ async function handleError( error: unknown, userEmail: string, + emailAccountId: string, label: string, modelName: string, ) { - logger.error("Error in LLM call", { error, userEmail, label, modelName }); + logger.error("Error in LLM call", { + error, + userEmail, + emailAccountId, + label, + modelName, + }); if (APICallError.isInstance(error)) { if (isIncorrectOpenAIAPIKeyError(error)) { diff --git a/apps/web/utils/logger.ts b/apps/web/utils/logger.ts index 4cc912e1fa..2a523c308a 100644 --- a/apps/web/utils/logger.ts +++ b/apps/web/utils/logger.ts @@ -120,13 +120,44 @@ function createNullLogger() { function formatError(args?: Record) { if (env.NODE_ENV !== "production") return args; - const error = args?.error; - if (error) args.error = cleanError(error); - return args; + if (!args?.error) return args; + + const error = args.error; + const errorMessage = + error instanceof Error + ? error.message + : typeof error === "object" && error !== null && "message" in error + ? (error as { message: unknown }).message + : error; + + const errorFull = serializeError(error); + + return { + ...args, + error: errorMessage, + errorFull, + }; } -function cleanError(error: unknown) { - if (error instanceof Error) return error.message; +function serializeError(error: unknown): unknown { + if (error instanceof Error) { + // Convert Error instance to plain object so hashSensitiveFields can process it + const serialized: Record = { + name: error.name, + message: error.message, + stack: error.stack, + }; + + // Copy all enumerable properties + for (const key in error) { + if (Object.hasOwn(error, key)) { + serialized[key] = (error as any)[key]; + } + } + + return serialized; + } + return error; } @@ -167,6 +198,8 @@ const REDACTED_FIELD_NAMES = new Set([ "refresh_token", "idToken", "id_token", + "headers", + "authorization", ]); /** diff --git a/apps/web/utils/middleware.ts b/apps/web/utils/middleware.ts index 83c34625ea..3b242d2fe9 100644 --- a/apps/web/utils/middleware.ts +++ b/apps/web/utils/middleware.ts @@ -102,7 +102,7 @@ function withMiddleware( const apiError = checkCommonErrors(error, req.url); if (apiError) { - await logErrorToPosthog("api", req.url, apiError.type); + await logErrorToPosthog("api", req.url, apiError.type, "unknown"); // TODO: add emailAccountId return NextResponse.json( { error: apiError.message, isKnownError: true }, diff --git a/apps/web/utils/outlook/client.ts b/apps/web/utils/outlook/client.ts index 9c252d0520..ac14c79808 100644 --- a/apps/web/utils/outlook/client.ts +++ b/apps/web/utils/outlook/client.ts @@ -101,7 +101,10 @@ export const getOutlookClientWithRefresh = async ({ expiresAt: number | null; emailAccountId: string; }): Promise => { - if (!refreshToken) throw new SafeError("No refresh token"); + if (!refreshToken) { + logger.error("No refresh token", { emailAccountId }); + throw new SafeError("No refresh token"); + } // Check if token needs refresh const expiryDate = expiresAt ? expiresAt : null; diff --git a/apps/web/utils/outlook/thread.ts b/apps/web/utils/outlook/thread.ts index 7c85127beb..755f672227 100644 --- a/apps/web/utils/outlook/thread.ts +++ b/apps/web/utils/outlook/thread.ts @@ -113,7 +113,7 @@ export async function getThreadsWithNextPageToken({ } const response: { value: Message[]; "@odata.nextLink"?: string } = - await request.get(); + await withOutlookRetry(() => request.get()); // Group messages by conversationId to create thread-like structure const threadMap = new Map(); @@ -137,13 +137,15 @@ export async function getThreadsFromSender( sender: string, limit: number, ): Promise> { - const response: { value: Message[] } = await client - .getClient() - .api("/me/messages") - .filter(`from/emailAddress/address eq '${escapeODataString(sender)}'`) - .top(limit) - .select("id,conversationId,bodyPreview") - .get(); + const response: { value: Message[] } = await withOutlookRetry(() => + client + .getClient() + .api("/me/messages") + .filter(`from/emailAddress/address eq '${escapeODataString(sender)}'`) + .top(limit) + .select("id,conversationId,bodyPreview") + .get(), + ); // Group messages by conversationId const threadMap = new Map(); @@ -164,13 +166,15 @@ export async function getThreadsFromSenderWithSubject( sender: string, limit: number, ): Promise> { - const response: { value: Message[] } = await client - .getClient() - .api("/me/messages") - .filter(`from/emailAddress/address eq '${escapeODataString(sender)}'`) - .top(limit) - .select("id,conversationId,subject,bodyPreview") - .get(); + const response: { value: Message[] } = await withOutlookRetry(() => + client + .getClient() + .api("/me/messages") + .filter(`from/emailAddress/address eq '${escapeODataString(sender)}'`) + .top(limit) + .select("id,conversationId,subject,bodyPreview") + .get(), + ); // Group messages by conversationId const threadMap = new Map< diff --git a/apps/web/utils/posthog.ts b/apps/web/utils/posthog.ts index 9b4a41b58c..05b344e37c 100644 --- a/apps/web/utils/posthog.ts +++ b/apps/web/utils/posthog.ts @@ -1,6 +1,7 @@ import { PostHog } from "posthog-node"; import { env } from "@/env"; import { createScopedLogger } from "@/utils/logger"; +import { hash } from "@/utils/hash"; const logger = createScopedLogger("posthog"); @@ -70,6 +71,33 @@ export async function deletePosthogUser(options: { email: string }) { } } +export async function aliasPosthogUser({ + oldEmail, + newEmail, +}: { + oldEmail: string; + newEmail: string; +}) { + if (!env.NEXT_PUBLIC_POSTHOG_KEY) { + logger.warn("NEXT_PUBLIC_POSTHOG_KEY not set"); + return; + } + + try { + const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY); + // Alias links the old distinct ID to the new distinct ID + // This ensures all historical events remain connected + client.alias({ distinctId: newEmail, alias: oldEmail }); + await client.shutdown(); + logger.info("PostHog user aliased", { + oldEmail: hash(oldEmail), + newEmail: hash(newEmail), + }); + } catch (error) { + logger.error("Error aliasing PostHog user", { error }); + } +} + export async function posthogCaptureEvent( email: string, event: string, @@ -130,17 +158,19 @@ export async function trackStripeCheckoutCompleted(email: string) { export async function trackError({ email, + emailAccountId, errorType, type, url, }: { email: string; + emailAccountId: string; errorType: string; type: "api" | "action"; url: string; }) { return posthogCaptureEvent(email, errorType, { - $set: { isError: true, type, url }, + $set: { isError: true, type, url, emailAccountId }, }); } diff --git a/apps/web/utils/webhook/error-handler.ts b/apps/web/utils/webhook/error-handler.ts index 0de406d6e4..3ff0510da9 100644 --- a/apps/web/utils/webhook/error-handler.ts +++ b/apps/web/utils/webhook/error-handler.ts @@ -10,16 +10,18 @@ export async function handleWebhookError( error: unknown, options: { email: string; + emailAccountId: string; url: string; logger: Logger; }, ) { - const { email, url, logger } = options; + const { email, emailAccountId, url, logger } = options; const apiError = checkCommonErrors(error, url); if (apiError) { await trackError({ email, + emailAccountId, errorType: apiError.type, type: "api", url, diff --git a/version.txt b/version.txt index 1099837570..3cc122cbc0 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.18.5 +v2.18.6