diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index a26d2d77ef..08c1c7bc8c 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -62,7 +62,7 @@ export default async function AppLayout({ }); } catch (error) { logger.error("Failed to update last login", { email, error }); - captureException(error, {}, email); + captureException(error, { userEmail: email }); } }); diff --git a/apps/web/app/(app)/sentry-identify.tsx b/apps/web/app/(app)/sentry-identify.tsx index 1962eb20b3..24480aa539 100644 --- a/apps/web/app/(app)/sentry-identify.tsx +++ b/apps/web/app/(app)/sentry-identify.tsx @@ -2,11 +2,22 @@ import { useEffect } from "react"; import * as Sentry from "@sentry/nextjs"; +import { useAccount } from "@/providers/EmailAccountProvider"; export function SentryIdentify({ email }: { email: string }) { + const { emailAccountId } = useAccount(); + useEffect(() => { Sentry.setUser({ email }); }, [email]); + useEffect(() => { + if (emailAccountId) { + Sentry.setTag("emailAccountId", emailAccountId); + } else { + Sentry.setTag("emailAccountId", undefined); + } + }, [emailAccountId]); + return null; } diff --git a/apps/web/app/api/google/webhook/process-history.ts b/apps/web/app/api/google/webhook/process-history.ts index abc7eb2a3d..cbcd4d9042 100644 --- a/apps/web/app/api/google/webhook/process-history.ts +++ b/apps/web/app/api/google/webhook/process-history.ts @@ -1,5 +1,6 @@ import uniqBy from "lodash/uniqBy"; import { NextResponse } from "next/server"; +import * as Sentry from "@sentry/nextjs"; import { getGmailClientWithRefresh } from "@/utils/gmail/client"; import { GmailLabel } from "@/utils/gmail/label"; import { captureException } from "@/utils/error"; @@ -48,6 +49,12 @@ export async function processHistoryForUser( hasAiAccess: userHasAiAccess, } = validation.data; + Sentry.setTag("emailAccountId", validatedEmailAccount.id); + Sentry.setUser({ + id: validatedEmailAccount.userId, + email: validatedEmailAccount.email, + }); + if ( !validatedEmailAccount.account?.access_token || !validatedEmailAccount.account?.refresh_token @@ -130,7 +137,7 @@ export async function processHistoryForUser( return NextResponse.json({ ok: true }); } - captureException(error, { extra: { decodedData } }, email); + captureException(error, { userEmail: email, extra: { decodedData } }); logger.error("Error processing webhook", { error: error instanceof Error @@ -198,11 +205,10 @@ async function processHistory(options: ProcessHistoryOptions, logger: Logger) { try { await processHistoryItem(event, options, log); } catch (error) { - captureException( - error, - { extra: { userEmail, messageId: event.item.message?.id } }, + captureException(error, { userEmail, - ); + extra: { messageId: event.item.message?.id }, + }); logger.error("Error processing history item", { error }); } } diff --git a/apps/web/app/api/outlook/webhook/process-history.ts b/apps/web/app/api/outlook/webhook/process-history.ts index 366110ffc9..f66164e7af 100644 --- a/apps/web/app/api/outlook/webhook/process-history.ts +++ b/apps/web/app/api/outlook/webhook/process-history.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import * as Sentry from "@sentry/nextjs"; import { captureException } from "@/utils/error"; import { createEmailProvider } from "@/utils/email/provider"; import type { OutlookResourceData } from "@/app/api/outlook/webhook/types"; @@ -43,6 +44,12 @@ export async function processHistoryForUser({ hasAiAccess: userHasAiAccess, } = validation.data; + Sentry.setTag("emailAccountId", validatedEmailAccount.id); + Sentry.setUser({ + id: validatedEmailAccount.userId, + email: validatedEmailAccount.email, + }); + const accountProvider = validatedEmailAccount.account?.provider || "microsoft"; @@ -75,11 +82,11 @@ export async function processHistoryForUser({ return NextResponse.json({ ok: true }); } - captureException( - error, - { extra: { subscriptionId, resourceData } }, - validatedEmailAccount.email, - ); + captureException(error, { + emailAccountId: validatedEmailAccount.id, + userEmail: validatedEmailAccount.email, + extra: { subscriptionId, resourceData }, + }); logger.error("Error processing webhook", { resourceData, email: validatedEmailAccount.email, diff --git a/apps/web/app/api/resend/digest/route.ts b/apps/web/app/api/resend/digest/route.ts index 114cb05cb1..1e6883ad4e 100644 --- a/apps/web/app/api/resend/digest/route.ts +++ b/apps/web/app/api/resend/digest/route.ts @@ -66,7 +66,7 @@ export const POST = verifySignatureAppRouter( return NextResponse.json(result); } catch (error) { logger.error("Error sending digest email", { error }); - captureException(error); + captureException(error, { emailAccountId }); return NextResponse.json( { success: false, error: "Error sending digest email" }, { status: 500 }, diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 82fe3ca5fb..460582d6f9 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -326,7 +326,7 @@ if (env.MICROSOFT_CLIENT_ID && !env.MICROSOFT_WEBHOOK_CLIENT_STATE) { const withSerwist = withSerwistInit({ swSrc: "app/sw.ts", swDest: "public/sw.js", - disable: env.NODE_ENV !== "production", + disable: process.env.NODE_ENV !== "production", maximumFileSizeToCacheInBytes: 3 * 1024 * 1024, // 3MB }); diff --git a/apps/web/providers/ChatProvider.tsx b/apps/web/providers/ChatProvider.tsx index c69c872223..41cb9a9c3e 100644 --- a/apps/web/providers/ChatProvider.tsx +++ b/apps/web/providers/ChatProvider.tsx @@ -76,7 +76,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) { }, onError: (error) => { console.error(error); - captureException(error, { extra: { emailAccountId } }); + captureException(error); toastError({ title: "An error occured!", description: error.message || "", diff --git a/apps/web/utils/actions/safe-action.ts b/apps/web/utils/actions/safe-action.ts index a30a958fdc..edaf64f675 100644 --- a/apps/web/utils/actions/safe-action.ts +++ b/apps/web/utils/actions/safe-action.ts @@ -1,4 +1,5 @@ import { createSafeActionClient } from "next-safe-action"; +import * as Sentry from "@sentry/nextjs"; import { withServerActionInstrumentation } from "@sentry/nextjs"; import { randomUUID } from "node:crypto"; import { z } from "zod"; @@ -35,20 +36,16 @@ const baseClient = createSafeActionClient({ } if (error instanceof SafeError) return error.message; - captureException( - error, - { - extra: { - metadata, - userId: context?.userId, - userEmail: context?.userEmail, - emailAccountId: context?.emailAccountId, - bindArgsClientInputs, - error: error.message, - }, + captureException(error, { + userId: context?.userId, + userEmail: context?.userEmail, + emailAccountId: context?.emailAccountId, + extra: { + metadata, + bindArgsClientInputs, + error: error.message, }, - context?.userEmail, - ); + }); return "An unknown error occurred."; }, @@ -104,6 +101,10 @@ export const actionClient = baseClient throw new SafeError("Unauthorized"); } + // Set Sentry context for this action + Sentry.setTag("emailAccountId", emailAccountId); + Sentry.setUser({ id: userId, email: userEmail }); + const logger = ctx.logger.with({ userId, userEmail, diff --git a/apps/web/utils/ai/actions.ts b/apps/web/utils/ai/actions.ts index edd766545a..3f460dad9d 100644 --- a/apps/web/utils/ai/actions.ts +++ b/apps/web/utils/ai/actions.ts @@ -328,6 +328,7 @@ const move_folder: ActionFunction<{ folderId?: string | null }> = async ({ const notify_sender: ActionFunction> = async ({ email, + emailAccountId, userEmail, logger, }) => { @@ -355,10 +356,10 @@ const notify_sender: ActionFunction> = async ({ captureException( new Error(result.error ?? "Cold email notification failed"), { - extra: { actionType: ActionType.NOTIFY_SENDER, senderEmail }, + emailAccountId, + extra: { actionType: ActionType.NOTIFY_SENDER }, sampleRate: 0.01, }, - userEmail, ); return; } diff --git a/apps/web/utils/auth.ts b/apps/web/utils/auth.ts index e5e79b8acc..8295e6b544 100644 --- a/apps/web/utils/auth.ts +++ b/apps/web/utils/auth.ts @@ -173,7 +173,7 @@ async function postSignUp({ userId, error, }); - captureException(error, undefined, email); + captureException(error, { userEmail: email }); }); await createLoopsContact( @@ -188,7 +188,7 @@ async function postSignUp({ email, error, }); - captureException(error, undefined, email); + captureException(error, { userEmail: email }); } }); }; @@ -198,7 +198,7 @@ async function postSignUp({ email, error, }); - captureException(error, undefined, email); + captureException(error, { userEmail: email }); }); const dub = trackDubSignUp({ id: userId, email, name, image }, logger).catch( @@ -207,7 +207,7 @@ async function postSignUp({ email, error, }); - captureException(error, undefined, email); + captureException(error, { userEmail: email }); }, ); diff --git a/apps/web/utils/email/watch-manager.ts b/apps/web/utils/email/watch-manager.ts index e82544a805..606772b350 100644 --- a/apps/web/utils/email/watch-manager.ts +++ b/apps/web/utils/email/watch-manager.ts @@ -252,7 +252,7 @@ async function watchEmails({ }); } else { logger.error("Error watching inbox", { error }); - captureException(error); + captureException(error, { emailAccountId }); } return { success: false, error }; @@ -279,7 +279,7 @@ export async function unwatchEmails({ logger.warn("Error unwatching emails, invalid grant"); } else { logger.error("Error unwatching emails", { error }); - captureException(error); + captureException(error, { emailAccountId }); } } diff --git a/apps/web/utils/error.ts b/apps/web/utils/error.ts index fadc551d75..d2c40cb84d 100644 --- a/apps/web/utils/error.ts +++ b/apps/web/utils/error.ts @@ -30,18 +30,30 @@ export function isGmailError( ); } +export type CaptureExceptionContext = { + // emailAccountId is set automatically via: + // - Frontend: SentryIdentify component + // - API routes: emailAccountMiddleware + // - Server actions: actionClient + // Only pass explicitly for code outside these contexts (e.g., cron jobs). + emailAccountId?: string | null; + userId?: string | null; + userEmail?: string; + extra?: Record; + sampleRate?: number; +}; + export function captureException( error: unknown, - additionalInfo?: { extra?: Record; sampleRate?: number }, - userEmail?: string, + context: CaptureExceptionContext = {}, ) { if (isKnownApiError(error)) { const logger = createScopedLogger("captureException"); - logger.warn("Known API error", { error, additionalInfo }); + logger.warn("Known API error", { error, context }); return; } - const sampleRate = additionalInfo?.sampleRate; + const { sampleRate, userEmail, emailAccountId, userId, extra } = context; if ( Number.isFinite(sampleRate) && process.env.NODE_ENV === "production" && @@ -51,7 +63,16 @@ export function captureException( } if (userEmail) setUser({ email: userEmail }); - sentryCaptureException(error, additionalInfo); + + const sentryExtra = { + ...extra, + ...(emailAccountId && { emailAccountId }), + ...(userId && { userId }), + }; + + sentryCaptureException(error, { + extra: Object.keys(sentryExtra).length > 0 ? sentryExtra : undefined, + }); } export type ActionError> = { diff --git a/apps/web/utils/llms/index.ts b/apps/web/utils/llms/index.ts index 2cdbda307e..4bd1f765ed 100644 --- a/apps/web/utils/llms/index.ts +++ b/apps/web/utils/llms/index.ts @@ -271,13 +271,10 @@ export async function chatCompletionStream({ error, }); logger.trace("Result", { result }); - captureException( - error, - { - extra: { label }, - }, + captureException(error, { userEmail, - ); + extra: { label }, + }); } }, onError: (error) => { @@ -286,13 +283,10 @@ export async function chatCompletionStream({ userEmail, error, }); - captureException( - error, - { - extra: { label }, - }, + captureException(error, { userEmail, - ); + extra: { label }, + }); }, }); diff --git a/apps/web/utils/middleware.ts b/apps/web/utils/middleware.ts index 4cb5d85c83..0e33edcf40 100644 --- a/apps/web/utils/middleware.ts +++ b/apps/web/utils/middleware.ts @@ -1,6 +1,7 @@ import { ZodError } from "zod"; import { type NextRequest, NextResponse, after } from "next/server"; import { randomUUID } from "node:crypto"; +import * as Sentry from "@sentry/nextjs"; import { captureException, checkCommonErrors, SafeError } from "@/utils/error"; import { env } from "@/env"; import { logErrorToPosthog } from "@/utils/error.server"; @@ -273,6 +274,9 @@ async function emailAccountMiddleware( ); } + Sentry.setTag("emailAccountId", emailAccountId); + Sentry.setUser({ id: userId, email }); + // Create a new request with email account info const emailAccountReq = req.clone() as RequestWithEmailAccount; emailAccountReq.auth = { userId, emailAccountId, email }; diff --git a/apps/web/utils/outlook/subscription-manager.ts b/apps/web/utils/outlook/subscription-manager.ts index d046efdd1e..c77a1d3acf 100644 --- a/apps/web/utils/outlook/subscription-manager.ts +++ b/apps/web/utils/outlook/subscription-manager.ts @@ -78,7 +78,7 @@ export class OutlookSubscriptionManager { : null; } catch (error) { this.logger.error("Failed to create subscription", { error }); - captureException(error); + captureException(error, { emailAccountId: this.emailAccountId }); return null; } } @@ -115,7 +115,7 @@ export class OutlookSubscriptionManager { }); } - captureException(error); + captureException(error, { emailAccountId: this.emailAccountId }); return null; } } diff --git a/apps/web/utils/user/delete.ts b/apps/web/utils/user/delete.ts index 176f4a7651..f4b5ea3c8c 100644 --- a/apps/web/utils/user/delete.ts +++ b/apps/web/utils/user/delete.ts @@ -65,12 +65,12 @@ export async function deleteUser({ error, userId, }); - captureException(error, { extra: { userId } }, userId); + captureException(error); }); clearCachedPerplexityResearchForUser(userId).catch((error) => { logger.error("Error clearing cached Perplexity research", { error }); - captureException(error, { extra: { userId } }, userId); + captureException(error); }); // Then proceed with the regular deletion process @@ -89,13 +89,13 @@ export async function deleteUser({ const customError = new Error("User deletion error"); customError.cause = originalError; - captureException(customError, { extra: { failures, userId } }); + captureException(customError, { extra: { failures } }); } } catch (error) { logger.error("Error during user resources deletion process", { error, }); - captureException(error, { extra: { userId } }, userId); + captureException(error); } } @@ -143,7 +143,7 @@ async function deleteResources({ logger.error("Error during database user deletion process", { error, }); - captureException(error, { extra: { emailAccountId } }, email); + captureException(error, { emailAccountId, userEmail: email }); throw error; } diff --git a/version.txt b/version.txt index 5f712777e6..f45c3ceb33 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.25.5 +v2.25.6