Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
});

Expand Down
11 changes: 11 additions & 0 deletions apps/web/app/(app)/sentry-identify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Comment on lines +14 to +20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove the else branch that attempts to clear the tag with undefined.

Sentry does not provide an API to remove individual tags. Instead, only set the tag when emailAccountId is truthy:

useEffect(() => {
  if (emailAccountId) {
    Sentry.setTag("emailAccountId", emailAccountId);
  }
}, [emailAccountId]);
🤖 Prompt for AI Agents
In apps/web/app/(app)/sentry-identify.tsx around lines 14 to 20, the useEffect
currently sets the Sentry tag to undefined in the else branch to try to clear
it; Sentry doesn't support removing individual tags, so remove the else branch
and only call Sentry.setTag("emailAccountId", emailAccountId) when
emailAccountId is truthy (i.e., wrap the setTag in an if (emailAccountId) check
and delete the else block) so the tag is only set when present.


return null;
}
16 changes: 11 additions & 5 deletions apps/web/app/api/google/webhook/process-history.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 });
}
}
Expand Down
17 changes: 12 additions & 5 deletions apps/web/app/api/outlook/webhook/process-history.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/resend/digest/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
2 changes: 1 addition & 1 deletion apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});

Expand Down
2 changes: 1 addition & 1 deletion apps/web/providers/ChatProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
},
onError: (error) => {
console.error(error);
captureException(error, { extra: { emailAccountId } });
captureException(error);
Comment thread
elie222 marked this conversation as resolved.
toastError({
title: "An error occured!",
description: error.message || "",
Expand Down
27 changes: 14 additions & 13 deletions apps/web/utils/actions/safe-action.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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.";
},
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions apps/web/utils/ai/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ const move_folder: ActionFunction<{ folderId?: string | null }> = async ({

const notify_sender: ActionFunction<Record<string, unknown>> = async ({
email,
emailAccountId,
userEmail,
logger,
}) => {
Expand Down Expand Up @@ -355,10 +356,10 @@ const notify_sender: ActionFunction<Record<string, unknown>> = async ({
captureException(
new Error(result.error ?? "Cold email notification failed"),
{
extra: { actionType: ActionType.NOTIFY_SENDER, senderEmail },
emailAccountId,
extra: { actionType: ActionType.NOTIFY_SENDER },
Comment thread
elie222 marked this conversation as resolved.
sampleRate: 0.01,
},
userEmail,
);
return;
}
Expand Down
8 changes: 4 additions & 4 deletions apps/web/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ async function postSignUp({
userId,
error,
});
captureException(error, undefined, email);
captureException(error, { userEmail: email });
});

await createLoopsContact(
Expand All @@ -188,7 +188,7 @@ async function postSignUp({
email,
error,
});
captureException(error, undefined, email);
captureException(error, { userEmail: email });
}
});
};
Expand All @@ -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(
Expand All @@ -207,7 +207,7 @@ async function postSignUp({
email,
error,
});
captureException(error, undefined, email);
captureException(error, { userEmail: email });
},
);

Expand Down
4 changes: 2 additions & 2 deletions apps/web/utils/email/watch-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ async function watchEmails({
});
} else {
logger.error("Error watching inbox", { error });
captureException(error);
captureException(error, { emailAccountId });
}

return { success: false, error };
Expand All @@ -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 });
}
}

Expand Down
31 changes: 26 additions & 5 deletions apps/web/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>;
sampleRate?: number;
};

export function captureException(
error: unknown,
additionalInfo?: { extra?: Record<string, any>; 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" &&
Expand All @@ -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<E extends object = Record<string, unknown>> = {
Expand Down
18 changes: 6 additions & 12 deletions apps/web/utils/llms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,10 @@ export async function chatCompletionStream({
error,
});
logger.trace("Result", { result });
captureException(
error,
{
extra: { label },
},
captureException(error, {
userEmail,
);
extra: { label },
});
}
},
onError: (error) => {
Expand All @@ -286,13 +283,10 @@ export async function chatCompletionStream({
userEmail,
error,
});
captureException(
error,
{
extra: { label },
},
captureException(error, {
userEmail,
);
extra: { label },
});
},
});

Expand Down
4 changes: 4 additions & 0 deletions apps/web/utils/middleware.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 };
Expand Down
4 changes: 2 additions & 2 deletions apps/web/utils/outlook/subscription-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -115,7 +115,7 @@ export class OutlookSubscriptionManager {
});
}

captureException(error);
captureException(error, { emailAccountId: this.emailAccountId });
return null;
}
}
Expand Down
Loading
Loading