diff --git a/apps/web/__tests__/ai-choose-args.test.ts b/apps/web/__tests__/ai-choose-args.test.ts index 9db79e9900..571f19e31f 100644 --- a/apps/web/__tests__/ai-choose-args.test.ts +++ b/apps/web/__tests__/ai-choose-args.test.ts @@ -3,9 +3,12 @@ import type { ParsedMessage } from "@/utils/types"; import { getActionItemsWithAiArgs } from "@/utils/ai/choose-rule/choose-args"; import { getEmailAccount, getAction, getRule } from "@/__tests__/helpers"; import { ActionType } from "@prisma/client"; +import { createScopedLogger } from "@/utils/logger"; // pnpm test-ai ai-choose-args +const logger = createScopedLogger("test"); + const isAiTest = process.env.RUN_AI_TESTS === "true"; const TIMEOUT = 15_000; @@ -26,6 +29,7 @@ describe.runIf(isAiTest)("getActionItemsWithAiArgs", () => { selectedRule: rule, client: {} as any, modelType: "default", + logger: logger, }); expect(result).toEqual(actions); @@ -49,6 +53,7 @@ describe.runIf(isAiTest)("getActionItemsWithAiArgs", () => { selectedRule: rule, client: {} as any, modelType: "default", + logger: logger, }); expect(result).toHaveLength(1); @@ -79,6 +84,7 @@ describe.runIf(isAiTest)("getActionItemsWithAiArgs", () => { selectedRule: rule, client: {} as any, modelType: "default", + logger: logger, }); expect(result).toHaveLength(1); @@ -109,6 +115,7 @@ describe.runIf(isAiTest)("getActionItemsWithAiArgs", () => { selectedRule: rule, client: {} as any, modelType: "default", + logger: logger, }); expect(result).toHaveLength(2); @@ -147,6 +154,7 @@ Matt`, selectedRule: rule, client: {} as any, modelType: "default", + logger: logger, }); expect(result).toHaveLength(2); @@ -189,6 +197,7 @@ Matt`, selectedRule: rule, client: {} as any, modelType: "default", + logger: logger, }); expect(result).toHaveLength(1); diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts index 0af68609c0..95507c0253 100644 --- a/apps/web/app/api/chat/route.ts +++ b/apps/web/app/api/chat/route.ts @@ -4,7 +4,7 @@ import { withEmailAccount } from "@/utils/middleware"; import { getEmailAccountWithAi } from "@/utils/user/get"; import { NextResponse } from "next/server"; import { aiProcessAssistantChat } from "@/utils/ai/assistant/chat"; -import { createScopedLogger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import prisma from "@/utils/prisma"; import type { Prisma } from "@prisma/client"; import { convertToUIMessages } from "@/components/assistant-chat/helpers"; @@ -13,8 +13,6 @@ import { messageContextSchema } from "@/app/api/chat/validation"; export const maxDuration = 120; -const logger = createScopedLogger("api/chat"); - const textPartSchema = z.object({ text: z.string().min(1).max(3000), type: z.enum(["text"]), @@ -30,7 +28,7 @@ const assistantInputSchema = z.object({ context: messageContextSchema.optional(), }); -export const POST = withEmailAccount(async (request) => { +export const POST = withEmailAccount("chat", async (request) => { const emailAccountId = request.auth.emailAccountId; const user = await getEmailAccountWithAi({ emailAccountId }); @@ -44,7 +42,11 @@ export const POST = withEmailAccount(async (request) => { const chat = (await getChatById(data.id)) || - (await createNewChat({ emailAccountId, chatId: data.id })); + (await createNewChat({ + emailAccountId, + chatId: data.id, + logger: request.logger, + })); if (!chat) { return NextResponse.json( @@ -80,11 +82,11 @@ export const POST = withEmailAccount(async (request) => { return result.toUIMessageStreamResponse({ onFinish: async ({ messages }) => { - await saveChatMessages(messages, chat.id); + await saveChatMessages(messages, chat.id, request.logger); }, }); } catch (error) { - logger.error("Error in assistant chat", { error }); + request.logger.error("Error in assistant chat", { error }); return NextResponse.json( { error: "Error in assistant chat" }, { status: 500 }, @@ -95,9 +97,11 @@ export const POST = withEmailAccount(async (request) => { async function createNewChat({ emailAccountId, chatId, + logger, }: { emailAccountId: string; chatId: string; + logger: Logger; }) { try { const newChat = await prisma.chat.create({ @@ -124,7 +128,11 @@ async function saveChatMessage(message: Prisma.ChatMessageCreateInput) { return prisma.chatMessage.create({ data: message }); } -async function saveChatMessages(messages: UIMessage[], chatId: string) { +async function saveChatMessages( + messages: UIMessage[], + chatId: string, + logger: Logger, +) { try { return prisma.chatMessage.createMany({ data: messages.map((message) => ({ diff --git a/apps/web/app/api/clean/gmail/route.ts b/apps/web/app/api/clean/gmail/route.ts index af6af1d8a0..ca8f4750fb 100644 --- a/apps/web/app/api/clean/gmail/route.ts +++ b/apps/web/app/api/clean/gmail/route.ts @@ -1,18 +1,16 @@ -import { type NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { verifySignatureAppRouter } from "@upstash/qstash/nextjs"; import { z } from "zod"; -import { withError } from "@/utils/middleware"; +import { withError, type RequestWithLogger } from "@/utils/middleware"; import { getGmailClientWithRefresh } from "@/utils/gmail/client"; import { GmailLabel, labelThread } from "@/utils/gmail/label"; import { SafeError } from "@/utils/error"; import prisma from "@/utils/prisma"; import { isDefined } from "@/utils/types"; -import { createScopedLogger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import { CleanAction } from "@prisma/client"; import { updateThread } from "@/utils/redis/clean"; -const logger = createScopedLogger("api/clean/gmail"); - const cleanGmailSchema = z.object({ emailAccountId: z.string(), threadId: z.string(), @@ -34,7 +32,8 @@ async function performGmailAction({ processedLabelId, jobId, action, -}: CleanGmailBody) { + logger, +}: CleanGmailBody & { logger: Logger }) { const account = await prisma.emailAccount.findUnique({ where: { id: emailAccountId }, select: { @@ -138,11 +137,15 @@ async function saveToDatabase({ } export const POST = withError( - verifySignatureAppRouter(async (request: NextRequest) => { + "clean/gmail", + verifySignatureAppRouter(async (request: Request) => { const json = await request.json(); const body = cleanGmailSchema.parse(json); - await performGmailAction(body); + await performGmailAction({ + ...body, + logger: (request as RequestWithLogger).logger, + }); return NextResponse.json({ success: true }); }), diff --git a/apps/web/app/api/clean/route.ts b/apps/web/app/api/clean/route.ts index 559f0b6e18..b520fb7afd 100644 --- a/apps/web/app/api/clean/route.ts +++ b/apps/web/app/api/clean/route.ts @@ -1,13 +1,13 @@ import { verifySignatureAppRouter } from "@upstash/qstash/nextjs"; import { z } from "zod"; import { NextResponse } from "next/server"; -import { withError } from "@/utils/middleware"; +import { withError, type RequestWithLogger } from "@/utils/middleware"; import { publishToQstash } from "@/utils/upstash"; import { getThreadMessages } from "@/utils/gmail/thread"; import { getGmailClientWithRefresh } from "@/utils/gmail/client"; import type { CleanGmailBody } from "@/app/api/clean/gmail/route"; import { SafeError } from "@/utils/error"; -import { createScopedLogger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import { aiClean } from "@/utils/ai/clean/ai-clean"; import { getEmailForLLM } from "@/utils/get-email-from-message"; import { @@ -25,8 +25,6 @@ import { CleanAction } from "@prisma/client"; import type { ParsedMessage } from "@/utils/types"; import { isActivePremium } from "@/utils/premium"; -const logger = createScopedLogger("api/clean"); - const cleanThreadBody = z.object({ emailAccountId: z.string(), threadId: z.string(), @@ -56,7 +54,8 @@ async function cleanThread({ action, instructions, skips, -}: CleanThreadBody) { + logger, +}: CleanThreadBody & { logger: Logger }) { // 1. get thread with messages // 2. process thread with ai / fixed logic // 3. add to gmail action queue @@ -112,6 +111,7 @@ async function cleanThread({ processedLabelId, jobId, action, + logger, }); function isStarred(message: ParsedMessage) { @@ -234,6 +234,7 @@ function getPublish({ processedLabelId, jobId, action, + logger, }: { emailAccountId: string; threadId: string; @@ -241,6 +242,7 @@ function getPublish({ processedLabelId: string; jobId: string; action: CleanAction; + logger: Logger; }) { return async ({ markDone }: { markDone: boolean }) => { // max rate: @@ -296,7 +298,10 @@ export const POST = withError( const json = await request.json(); const body = cleanThreadBody.parse(json); - await cleanThread(body); + await cleanThread({ + ...body, + logger: (request as RequestWithLogger).logger, + }); return NextResponse.json({ success: true }); }), diff --git a/apps/web/app/api/google/watch/route.ts b/apps/web/app/api/google/watch/route.ts index a78bcb87cb..e74d421dd6 100644 --- a/apps/web/app/api/google/watch/route.ts +++ b/apps/web/app/api/google/watch/route.ts @@ -27,6 +27,7 @@ export const GET = withAuth("google/watch", async (request) => { const emailProvider = await createEmailProvider({ emailAccountId, provider: "google", + logger: request.logger, }); const expirationDate = await watchEmails({ emailAccountId, diff --git a/apps/web/app/api/google/webhook/process-history-item.ts b/apps/web/app/api/google/webhook/process-history-item.ts index fc428aa9fb..861f1d1a73 100644 --- a/apps/web/app/api/google/webhook/process-history-item.ts +++ b/apps/web/app/api/google/webhook/process-history-item.ts @@ -28,6 +28,7 @@ export async function processHistoryItem( const provider = await createEmailProvider({ emailAccountId, provider: "google", + logger, }); // Handle Google-specific label events diff --git a/apps/web/app/api/outlook/webhook/process-history.ts b/apps/web/app/api/outlook/webhook/process-history.ts index 426d64d907..366110ffc9 100644 --- a/apps/web/app/api/outlook/webhook/process-history.ts +++ b/apps/web/app/api/outlook/webhook/process-history.ts @@ -49,6 +49,7 @@ export async function processHistoryForUser({ const provider = await createEmailProvider({ emailAccountId: validatedEmailAccount.id, provider: accountProvider, + logger, }); try { diff --git a/apps/web/app/api/resend/digest/route.ts b/apps/web/app/api/resend/digest/route.ts index bbb8a1c3ed..0571faf329 100644 --- a/apps/web/app/api/resend/digest/route.ts +++ b/apps/web/app/api/resend/digest/route.ts @@ -123,6 +123,7 @@ async function sendEmail({ const emailProvider = await createEmailProvider({ emailAccountId, provider: emailAccount.account.provider, + logger, }); const digestScheduleData = await getDigestSchedule({ emailAccountId }); diff --git a/apps/web/app/api/scheduled-actions/execute/route.ts b/apps/web/app/api/scheduled-actions/execute/route.ts index 6499c9366c..3fc7aebf73 100644 --- a/apps/web/app/api/scheduled-actions/execute/route.ts +++ b/apps/web/app/api/scheduled-actions/execute/route.ts @@ -93,10 +93,12 @@ export const POST = verifySignatureAppRouter( const provider = await createEmailProvider({ emailAccountId: scheduledAction.emailAccountId, provider: scheduledAction.emailAccount.account.provider, + logger, }); const executionResult = await executeScheduledAction( scheduledAction, provider, + logger, ); if (executionResult.success) { diff --git a/apps/web/app/api/user/no-reply/route.ts b/apps/web/app/api/user/no-reply/route.ts index 3d8edaa672..f368cb0149 100644 --- a/apps/web/app/api/user/no-reply/route.ts +++ b/apps/web/app/api/user/no-reply/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { isDefined } from "@/utils/types"; import { withEmailProvider } from "@/utils/middleware"; import { createEmailProvider } from "@/utils/email/provider"; +import type { Logger } from "@/utils/logger"; export type NoReplyResponse = Awaited>; @@ -9,14 +10,17 @@ async function getNoReply({ emailAccountId, userEmail, provider, + logger, }: { emailAccountId: string; userEmail: string; provider: string; + logger: Logger; }) { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const sentEmails = await emailProvider.getSentMessages(50); @@ -53,6 +57,7 @@ export const GET = withEmailProvider("user/no-reply", async (request) => { emailAccountId, userEmail, provider: request.emailProvider.name, + logger: request.logger, }); return NextResponse.json(result); diff --git a/apps/web/app/api/user/rules/[id]/example/controller.ts b/apps/web/app/api/user/rules/[id]/example/controller.ts index 5cb529d9f3..d89b149899 100644 --- a/apps/web/app/api/user/rules/[id]/example/controller.ts +++ b/apps/web/app/api/user/rules/[id]/example/controller.ts @@ -10,10 +10,12 @@ import { fetchPaginatedMessages } from "@/app/api/user/group/[groupId]/messages/ import { isGroupRule, isAIRule, isStaticRule } from "@/utils/condition"; import { LogicalOperator } from "@prisma/client"; import type { EmailProvider } from "@/utils/email/types"; +import type { Logger } from "@/utils/logger"; export async function fetchExampleMessages( rule: RuleWithGroup, emailProvider: EmailProvider, + logger: Logger, ) { const isStatic = isStaticRule(rule); const isGroup = isGroupRule(rule); @@ -32,7 +34,7 @@ export async function fetchExampleMessages( ) return []; - if (isStatic) return fetchStaticExampleMessages(rule, emailProvider); + if (isStatic) return fetchStaticExampleMessages(rule, emailProvider, logger); if (isGroup) { if (!rule.group) return []; @@ -50,6 +52,7 @@ export async function fetchExampleMessages( async function fetchStaticExampleMessages( rule: RuleWithGroup, emailProvider: EmailProvider, + logger: Logger, ): Promise { // Build structured query options instead of provider-specific query strings const options: Parameters[0] = { @@ -70,6 +73,6 @@ async function fetchStaticExampleMessages( // search might include messages that don't match the rule, so we filter those out return response.messages.filter((message) => - matchesStaticRule(rule, message), + matchesStaticRule(rule, message, logger), ); } diff --git a/apps/web/app/api/user/rules/[id]/example/route.ts b/apps/web/app/api/user/rules/[id]/example/route.ts index c556b62e6f..5c8aac7f73 100644 --- a/apps/web/app/api/user/rules/[id]/example/route.ts +++ b/apps/web/app/api/user/rules/[id]/example/route.ts @@ -4,6 +4,7 @@ import { withEmailProvider } from "@/utils/middleware"; import { fetchExampleMessages } from "@/app/api/user/rules/[id]/example/controller"; import { SafeError } from "@/utils/error"; import { createEmailProvider } from "@/utils/email/provider"; +import type { Logger } from "@/utils/logger"; export type ExamplesResponse = Awaited>; @@ -11,10 +12,12 @@ async function getExamples({ ruleId, emailAccountId, provider, + logger, }: { ruleId: string; emailAccountId: string; provider: string; + logger: Logger; }) { const rule = await prisma.rule.findUnique({ where: { id: ruleId, emailAccountId }, @@ -26,9 +29,14 @@ async function getExamples({ const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); - const exampleMessages = await fetchExampleMessages(rule, emailProvider); + const exampleMessages = await fetchExampleMessages( + rule, + emailProvider, + logger, + ); return exampleMessages; } @@ -42,7 +50,12 @@ export const GET = withEmailProvider( const { id } = await params; if (!id) return NextResponse.json({ error: "Missing rule id" }); - const result = await getExamples({ ruleId: id, emailAccountId, provider }); + const result = await getExamples({ + ruleId: id, + emailAccountId, + provider, + logger: request.logger, + }); return NextResponse.json(result); }, diff --git a/apps/web/app/api/user/stats/day/route.ts b/apps/web/app/api/user/stats/day/route.ts index 3fae4e58f1..b7d53c37bc 100644 --- a/apps/web/app/api/user/stats/day/route.ts +++ b/apps/web/app/api/user/stats/day/route.ts @@ -101,7 +101,11 @@ export const GET = withEmailProvider("user/stats/day", async (request) => { const type = searchParams.get("type"); const query = statsByDayQuery.parse({ type }); - const emailProvider = await createEmailProvider({ emailAccountId, provider }); + const emailProvider = await createEmailProvider({ + emailAccountId, + provider, + logger: request.logger, + }); const result = await getPastSevenDayStats({ ...query, diff --git a/apps/web/app/api/user/stats/newsletters/route.ts b/apps/web/app/api/user/stats/newsletters/route.ts index cbd4084d33..a7f9772429 100644 --- a/apps/web/app/api/user/stats/newsletters/route.ts +++ b/apps/web/app/api/user/stats/newsletters/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; import { withEmailProvider } from "@/utils/middleware"; import { extractEmailAddress } from "@/utils/email"; -import { createScopedLogger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import prisma from "@/utils/prisma"; import { Prisma } from "@prisma/client"; import type { EmailProvider } from "@/utils/email/types"; @@ -13,8 +13,6 @@ import { filterNewsletters, } from "@/app/api/user/stats/newsletters/helpers"; -const logger = createScopedLogger("api/user/stats/newsletters"); - const newsletterStatsQuery = z.object({ limit: z.coerce.number().nullish(), fromDate: z.coerce.number().nullish(), @@ -71,15 +69,17 @@ async function getEmailMessages( options: { emailAccountId: string; emailProvider: EmailProvider; + logger: Logger; } & NewsletterStatsQuery, ) { - const { emailAccountId, emailProvider } = options; + const { emailAccountId, emailProvider, logger } = options; const types = getTypeFilters(options.types); const [counts, autoArchiveFilters, userNewsletters] = await Promise.all([ getNewsletterCounts({ ...options, ...types, + logger, }), getAutoArchiveFilters(emailProvider), findNewsletterStatus({ emailAccountId }), @@ -137,8 +137,10 @@ async function getNewsletterCounts( unarchived?: boolean; all?: boolean; andClause?: boolean; + logger: Logger; }, ): Promise { + const { logger } = options; // Build WHERE conditions using Prisma.sql for type safety const whereConditions: Prisma.Sql[] = []; @@ -266,6 +268,7 @@ export const GET = withEmailProvider( ...params, emailAccountId, emailProvider, + logger: request.logger, }); return NextResponse.json(result); diff --git a/apps/web/utils/actions/ai-rule.ts b/apps/web/utils/actions/ai-rule.ts index fe9b4e7fbd..b4c9814957 100644 --- a/apps/web/utils/actions/ai-rule.ts +++ b/apps/web/utils/actions/ai-rule.ts @@ -21,10 +21,8 @@ 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 { aiFindSnippets } from "@/utils/ai/snippets/find-snippets"; -import type { CreateOrUpdateRuleSchema } from "@/utils/ai/rule/create-rule-schema"; import { createRule, updateRule, deleteRule } from "@/utils/rule/rule"; import { actionClient } from "@/utils/actions/safe-action"; -import type { Logger } from "@/utils/logger"; import { getEmailAccountWithAi } from "@/utils/user/get"; import { SafeError } from "@/utils/error"; import { createEmailProvider } from "@/utils/email/provider"; @@ -33,7 +31,7 @@ import type { CreateRuleResult } from "@/utils/rule/types"; export const runRulesAction = actionClient .metadata({ name: "runRules" }) - .schema(runRulesBody) + .inputSchema(runRulesBody) .action( async ({ ctx: { emailAccountId, provider, logger: ctxLogger }, @@ -49,6 +47,7 @@ export const runRulesAction = actionClient const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const message = await emailProvider.getMessage(messageId); @@ -99,6 +98,7 @@ export const runRulesAction = actionClient message, rules, emailAccount, + logger, modelType: "chat", }); @@ -108,9 +108,12 @@ export const runRulesAction = actionClient export const testAiCustomContentAction = actionClient .metadata({ name: "testAiCustomContent" }) - .schema(testAiCustomContentBody) + .inputSchema(testAiCustomContentBody) .action( - async ({ ctx: { emailAccountId, provider }, parsedInput: { content } }) => { + async ({ + ctx: { emailAccountId, provider, logger }, + parsedInput: { content }, + }) => { const emailAccount = await getEmailAccountWithAi({ emailAccountId }); if (!emailAccount) throw new SafeError("Email account not found"); @@ -118,6 +121,7 @@ export const testAiCustomContentAction = actionClient const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const rules = await prisma.rule.findMany({ @@ -132,6 +136,7 @@ export const testAiCustomContentAction = actionClient const result = await runRules({ isTest: true, provider: emailProvider, + logger, message: { id: "testMessageId", threadId: "testThreadId", @@ -160,7 +165,7 @@ export const testAiCustomContentAction = actionClient export const setRuleRunOnThreadsAction = actionClient .metadata({ name: "setRuleRunOnThreads" }) - .schema(z.object({ ruleId: z.string(), runOnThreads: z.boolean() })) + .inputSchema(z.object({ ruleId: z.string(), runOnThreads: z.boolean() })) .action( async ({ ctx: { emailAccountId }, @@ -189,7 +194,7 @@ export const setRuleRunOnThreadsAction = actionClient */ export const saveRulesPromptAction = actionClient .metadata({ name: "saveRulesPrompt" }) - .schema(saveRulesPromptBody) + .inputSchema(saveRulesPromptBody) .action( async ({ ctx: { emailAccountId, logger }, @@ -428,7 +433,7 @@ export const saveRulesPromptAction = actionClient export const createRulesAction = actionClient .metadata({ name: "createRules" }) - .schema(createRulesBody) + .inputSchema(createRulesBody) .action( async ({ ctx: { emailAccountId, logger }, parsedInput: { prompt } }) => { const emailAccount = await prisma.emailAccount.findUnique({ @@ -523,7 +528,7 @@ export const createRulesAction = actionClient */ export const generateRulesPromptAction = actionClient .metadata({ name: "generateRulesPrompt" }) - .schema(z.object({})) + .inputSchema(z.object({})) .action(async ({ ctx: { emailAccountId, provider } }) => { const emailAccount = await getEmailAccountWithAi({ emailAccountId }); diff --git a/apps/web/utils/actions/assess.ts b/apps/web/utils/actions/assess.ts index 1ecb036625..6224be1db4 100644 --- a/apps/web/utils/actions/assess.ts +++ b/apps/web/utils/actions/assess.ts @@ -12,10 +12,11 @@ import { SafeError } from "@/utils/error"; // to help with onboarding and provide the best flow to new users export const assessAction = actionClient .metadata({ name: "assessUser" }) - .action(async ({ ctx: { emailAccountId, provider } }) => { + .action(async ({ ctx: { emailAccountId, provider, logger } }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const emailAccount = await prisma.emailAccount.findUnique({ @@ -36,7 +37,7 @@ export const assessAction = actionClient export const analyzeWritingStyleAction = actionClient .metadata({ name: "analyzeWritingStyle" }) - .action(async ({ ctx: { emailAccountId, provider } }) => { + .action(async ({ ctx: { emailAccountId, provider, logger } }) => { const emailAccount = await prisma.emailAccount.findUnique({ where: { id: emailAccountId }, select: { @@ -58,6 +59,7 @@ export const analyzeWritingStyleAction = actionClient const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const sentMessages = await emailProvider.getSentMessages(20); diff --git a/apps/web/utils/actions/categorize.ts b/apps/web/utils/actions/categorize.ts index 97455e22c0..ba04cd9f91 100644 --- a/apps/web/utils/actions/categorize.ts +++ b/apps/web/utils/actions/categorize.ts @@ -105,7 +105,7 @@ export const categorizeSenderAction = actionClient .schema(z.object({ senderAddress: z.string() })) .action( async ({ - ctx: { emailAccountId, provider }, + ctx: { emailAccountId, provider, logger }, parsedInput: { senderAddress }, }) => { const userResult = await validateUserAndAiAccess({ emailAccountId }); @@ -114,6 +114,7 @@ export const categorizeSenderAction = actionClient const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const result = await categorizeSender( diff --git a/apps/web/utils/actions/clean.ts b/apps/web/utils/actions/clean.ts index eddb34f3a7..f52c9b8616 100644 --- a/apps/web/utils/actions/clean.ts +++ b/apps/web/utils/actions/clean.ts @@ -51,6 +51,7 @@ export const cleanInboxAction = actionClient const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const [markedDoneLabel, processedLabel] = await Promise.all([ diff --git a/apps/web/utils/actions/cold-email.ts b/apps/web/utils/actions/cold-email.ts index 468f33c12b..9f549aaae2 100644 --- a/apps/web/utils/actions/cold-email.ts +++ b/apps/web/utils/actions/cold-email.ts @@ -18,12 +18,16 @@ import { SystemType } from "@prisma/client"; export const markNotColdEmailAction = actionClient .metadata({ name: "markNotColdEmail" }) - .schema(markNotColdEmailBody) + .inputSchema(markNotColdEmailBody) .action( - async ({ ctx: { emailAccountId, provider }, parsedInput: { sender } }) => { + async ({ + ctx: { emailAccountId, provider, logger }, + parsedInput: { sender }, + }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); await Promise.all([ @@ -93,10 +97,10 @@ async function removeColdEmailLabelFromSender( export const testColdEmailAction = actionClient .metadata({ name: "testColdEmail" }) - .schema(coldEmailBlockerBody) + .inputSchema(coldEmailBlockerBody) .action( async ({ - ctx: { emailAccountId, provider }, + ctx: { emailAccountId, provider, logger }, parsedInput: { from, subject, @@ -125,6 +129,7 @@ export const testColdEmailAction = actionClient const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const content = emailToContent({ diff --git a/apps/web/utils/actions/email-account.ts b/apps/web/utils/actions/email-account.ts index a8563fe8be..a0f61bd196 100644 --- a/apps/web/utils/actions/email-account.ts +++ b/apps/web/utils/actions/email-account.ts @@ -37,7 +37,7 @@ export const updateEmailAccountRoleAction = actionClient export const analyzePersonaAction = actionClient .metadata({ name: "analyzePersona" }) - .action(async ({ ctx: { emailAccountId, provider } }) => { + .action(async ({ ctx: { emailAccountId, provider, logger } }) => { const existingPersona = await prisma.emailAccount.findUnique({ where: { id: emailAccountId }, select: { personaAnalysis: true }, @@ -58,6 +58,7 @@ export const analyzePersonaAction = actionClient const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const messagesResponse = await emailProvider.getMessagesWithPagination({ @@ -106,10 +107,11 @@ export const updateReferralSignatureAction = actionClient export const fetchSignaturesFromProviderAction = actionClient .metadata({ name: "fetchSignaturesFromProvider" }) - .action(async ({ ctx: { emailAccountId, provider } }) => { + .action(async ({ ctx: { emailAccountId, provider, logger } }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const signatures = await emailProvider.getSignatures(); diff --git a/apps/web/utils/actions/mail-bulk-action.ts b/apps/web/utils/actions/mail-bulk-action.ts index a7f1e456d5..d4fbd0495a 100644 --- a/apps/web/utils/actions/mail-bulk-action.ts +++ b/apps/web/utils/actions/mail-bulk-action.ts @@ -13,12 +13,13 @@ export const bulkArchiveAction = actionClient ) .action( async ({ - ctx: { emailAccountId, provider, emailAccount }, + ctx: { emailAccountId, provider, emailAccount, logger }, parsedInput: { froms }, }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); await emailProvider.bulkArchiveFromSenders( @@ -38,12 +39,13 @@ export const bulkTrashAction = actionClient ) .action( async ({ - ctx: { emailAccountId, provider, emailAccount }, + ctx: { emailAccountId, provider, emailAccount, logger }, parsedInput: { froms }, }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); await emailProvider.bulkTrashFromSenders( diff --git a/apps/web/utils/actions/mail.ts b/apps/web/utils/actions/mail.ts index efe6f30ebb..e2c34adbba 100644 --- a/apps/web/utils/actions/mail.ts +++ b/apps/web/utils/actions/mail.ts @@ -12,15 +12,18 @@ const isStatusOk = (status: number) => status >= 200 && status < 300; export const archiveThreadAction = actionClient .metadata({ name: "archiveThread" }) - .schema(z.object({ threadId: z.string(), labelId: z.string().optional() })) + .inputSchema( + z.object({ threadId: z.string(), labelId: z.string().optional() }), + ) .action( async ({ - ctx: { emailAccountId, emailAccount, provider }, + ctx: { emailAccountId, emailAccount, provider, logger }, parsedInput: { threadId, labelId }, }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); await emailProvider.archiveThreadWithLabel( @@ -33,15 +36,16 @@ export const archiveThreadAction = actionClient export const trashThreadAction = actionClient .metadata({ name: "trashThread" }) - .schema(z.object({ threadId: z.string() })) + .inputSchema(z.object({ threadId: z.string() })) .action( async ({ - ctx: { emailAccountId, emailAccount, provider }, + ctx: { emailAccountId, emailAccount, provider, logger }, parsedInput: { threadId }, }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); await emailProvider.trashThread(threadId, emailAccount.email, "user"); @@ -50,15 +54,16 @@ export const trashThreadAction = actionClient export const markReadThreadAction = actionClient .metadata({ name: "markReadThread" }) - .schema(z.object({ threadId: z.string(), read: z.boolean() })) + .inputSchema(z.object({ threadId: z.string(), read: z.boolean() })) .action( async ({ - ctx: { emailAccountId, provider }, + ctx: { emailAccountId, provider, logger }, parsedInput: { threadId, read }, }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); await emailProvider.markReadThread(threadId, read); @@ -67,7 +72,7 @@ export const markReadThreadAction = actionClient export const createAutoArchiveFilterAction = actionClient .metadata({ name: "createAutoArchiveFilter" }) - .schema( + .inputSchema( z.object({ from: z.string(), gmailLabelId: z.string().optional(), @@ -76,12 +81,13 @@ export const createAutoArchiveFilterAction = actionClient ) .action( async ({ - ctx: { emailAccountId, provider }, + ctx: { emailAccountId, provider, logger }, parsedInput: { from, gmailLabelId, labelName }, }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); await emailProvider.createAutoArchiveFilter({ @@ -94,15 +100,16 @@ export const createAutoArchiveFilterAction = actionClient export const createFilterAction = actionClient .metadata({ name: "createFilter" }) - .schema(z.object({ from: z.string(), gmailLabelId: z.string() })) + .inputSchema(z.object({ from: z.string(), gmailLabelId: z.string() })) .action( async ({ - ctx: { emailAccountId, provider }, + ctx: { emailAccountId, provider, logger }, parsedInput: { from, gmailLabelId }, }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const res = await emailProvider.createFilter({ @@ -119,12 +126,16 @@ export const createFilterAction = actionClient export const deleteFilterAction = actionClient .metadata({ name: "deleteFilter" }) - .schema(z.object({ id: z.string() })) + .inputSchema(z.object({ id: z.string() })) .action( - async ({ ctx: { emailAccountId, provider }, parsedInput: { id } }) => { + async ({ + ctx: { emailAccountId, provider, logger }, + parsedInput: { id }, + }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const res = await emailProvider.deleteFilter(id); @@ -136,15 +147,18 @@ export const deleteFilterAction = actionClient export const createLabelAction = actionClient .metadata({ name: "createLabel" }) - .schema(z.object({ name: z.string(), description: z.string().optional() })) + .inputSchema( + z.object({ name: z.string(), description: z.string().optional() }), + ) .action( async ({ - ctx: { emailAccountId, provider }, + ctx: { emailAccountId, provider, logger }, parsedInput: { name, description }, }) => { const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const label = await emailProvider.createLabel(name, description); return label; @@ -153,7 +167,7 @@ export const createLabelAction = actionClient export const updateLabelsAction = actionClient .metadata({ name: "updateLabels" }) - .schema( + .inputSchema( z.object({ labels: z.array( z.object({ @@ -200,18 +214,21 @@ export const updateLabelsAction = actionClient export const sendEmailAction = actionClient .metadata({ name: "sendEmail" }) - .schema(sendEmailBody) - .action(async ({ ctx: { emailAccountId, provider }, parsedInput }) => { - const emailProvider = await createEmailProvider({ - emailAccountId, - provider, - }); - - const result = await emailProvider.sendEmailWithHtml(parsedInput); - - return { - success: true, - messageId: result.messageId, - threadId: result.threadId, - }; - }); + .inputSchema(sendEmailBody) + .action( + async ({ ctx: { emailAccountId, provider, logger }, parsedInput }) => { + const emailProvider = await createEmailProvider({ + emailAccountId, + provider, + logger, + }); + + const result = await emailProvider.sendEmailWithHtml(parsedInput); + + return { + success: true, + messageId: result.messageId, + threadId: result.threadId, + }; + }, + ); diff --git a/apps/web/utils/actions/rule.ts b/apps/web/utils/actions/rule.ts index 9e789a4837..97c915b26e 100644 --- a/apps/web/utils/actions/rule.ts +++ b/apps/web/utils/actions/rule.ts @@ -674,6 +674,7 @@ async function getActionsFromCategoryAction({ const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); const { label: labelName, labelId } = await resolveLabelNameAndId({ diff --git a/apps/web/utils/actions/stats.ts b/apps/web/utils/actions/stats.ts index ef48bf7310..04fa8005ca 100644 --- a/apps/web/utils/actions/stats.ts +++ b/apps/web/utils/actions/stats.ts @@ -45,6 +45,7 @@ export const loadEmailStatsAction = actionClient const emailProvider = await createEmailProvider({ emailAccountId, provider: emailAccount.account.provider, + logger, }); await loadEmails( diff --git a/apps/web/utils/actions/whitelist.ts b/apps/web/utils/actions/whitelist.ts index 1b7486472d..dd7667cdb2 100644 --- a/apps/web/utils/actions/whitelist.ts +++ b/apps/web/utils/actions/whitelist.ts @@ -8,13 +8,14 @@ import { createEmailProvider } from "@/utils/email/provider"; export const whitelistInboxZeroAction = actionClient .metadata({ name: "whitelistInboxZero" }) - .action(async ({ ctx: { emailAccountId, provider } }) => { + .action(async ({ ctx: { emailAccountId, provider, logger } }) => { if (!env.WHITELIST_FROM) return; if (!isGoogleProvider(provider)) return; const emailProvider = await createEmailProvider({ emailAccountId, provider, + logger, }); await emailProvider.createFilter({ diff --git a/apps/web/utils/ai/actions.ts b/apps/web/utils/ai/actions.ts index c273330925..99f2c63547 100644 --- a/apps/web/utils/ai/actions.ts +++ b/apps/web/utils/ai/actions.ts @@ -1,5 +1,5 @@ import { ActionType, type ExecutedRule } from "@prisma/client"; -import { createScopedLogger } from "@/utils/logger"; +import { createScopedLogger, type Logger } from "@/utils/logger"; import { callWebhook } from "@/utils/webhook"; import type { ActionItem, EmailForAction } from "@/utils/ai/types"; import type { EmailProvider } from "@/utils/email/types"; @@ -8,7 +8,7 @@ import { filterNullProperties } from "@/utils"; import { labelMessageAndSync } from "@/utils/label.server"; import { hasVariables } from "@/utils/template"; -const logger = createScopedLogger("ai-actions"); +const MODULE = "ai-actions"; type ActionFunction>> = (options: { client: EmailProvider; @@ -18,6 +18,7 @@ type ActionFunction>> = (options: { userId: string; emailAccountId: string; executedRule: ExecutedRule; + logger: Logger; }) => Promise; export const runActionFunction = async (options: { @@ -28,19 +29,23 @@ export const runActionFunction = async (options: { userId: string; emailAccountId: string; executedRule: ExecutedRule; + logger: Logger; }) => { - const { action, userEmail } = options; - logger.info("Running action", { + const { action, userEmail, logger } = options; + const log = logger.with({ module: MODULE }); + + log.info("Running action", { actionType: action.type, userEmail, id: action.id, }); - logger.trace("Running action", () => filterNullProperties(action)); + log.trace("Running action", () => filterNullProperties(action)); const { type, ...args } = action; const opts = { ...options, args, + logger: log, }; switch (type) { case ActionType.ARCHIVE: @@ -81,7 +86,7 @@ const archive: ActionFunction> = async ({ const label: ActionFunction<{ label?: string | null; labelId?: string | null; -}> = async ({ client, email, args, emailAccountId }) => { +}> = async ({ client, email, args, emailAccountId, logger }) => { let labelIdToUse = args.labelId; // Lazy migration: If no labelId but label name exists, look it up diff --git a/apps/web/utils/ai/choose-rule/choose-args.ts b/apps/web/utils/ai/choose-rule/choose-args.ts index dcd5e5c127..d84f0d8210 100644 --- a/apps/web/utils/ai/choose-rule/choose-args.ts +++ b/apps/web/utils/ai/choose-rule/choose-args.ts @@ -13,10 +13,10 @@ import { type ActionArgResponse, aiGenerateArgs, } from "@/utils/ai/choose-rule/ai-choose-args"; -import { createScopedLogger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import type { EmailProvider } from "@/utils/email/types"; -const logger = createScopedLogger("choose-args"); +const MODULE = "choose-args"; export async function getActionItemsWithAiArgs({ message, @@ -24,13 +24,16 @@ export async function getActionItemsWithAiArgs({ selectedRule, client, modelType, + logger, }: { message: ParsedMessage; emailAccount: EmailAccountWithAI; selectedRule: RuleWithActions; client: EmailProvider; modelType: ModelType; + logger: Logger; }): Promise { + const log = logger.with({ module: MODULE }); // Draft content is handled via its own AI call // We provide a lot more context to the AI to draft the content const draftEmailActions = selectedRule.actions.filter( @@ -41,7 +44,7 @@ export async function getActionItemsWithAiArgs({ if (draftEmailActions.length) { try { - logger.info("Generating draft", { + log.info("Generating draft", { email: emailAccount.email, threadId: message.threadId, }); @@ -52,12 +55,12 @@ export async function getActionItemsWithAiArgs({ client, ); - logger.info("Draft generated", { + log.info("Draft generated", { email: emailAccount.email, threadId: message.threadId, }); } catch (error) { - logger.error("Failed to generate draft", { + log.error("Failed to generate draft", { email: emailAccount.email, threadId: message.threadId, error, diff --git a/apps/web/utils/ai/choose-rule/execute.ts b/apps/web/utils/ai/choose-rule/execute.ts index ba2732da44..aa0409cfee 100644 --- a/apps/web/utils/ai/choose-rule/execute.ts +++ b/apps/web/utils/ai/choose-rule/execute.ts @@ -2,11 +2,13 @@ import { runActionFunction } from "@/utils/ai/actions"; import prisma from "@/utils/prisma"; import type { Prisma } from "@prisma/client"; import { ExecutedRuleStatus, ActionType } from "@prisma/client"; -import { createScopedLogger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import type { ParsedMessage } from "@/utils/types"; import { updateExecutedActionWithDraftId } from "@/utils/ai/choose-rule/draft-management"; import type { EmailProvider } from "@/utils/email/types"; +const MODULE = "ai-execute-act"; + type ExecutedRuleWithActionItems = Prisma.ExecutedRuleGetPayload<{ include: { actionItems: true }; }>; @@ -18,6 +20,7 @@ export async function executeAct({ userId, emailAccountId, message, + logger, }: { client: EmailProvider; executedRule: ExecutedRuleWithActionItems; @@ -25,10 +28,10 @@ export async function executeAct({ userEmail: string; userId: string; emailAccountId: string; + logger: Logger; }) { - const logger = createScopedLogger("ai-execute-act").with({ - email: userEmail, - emailAccountId, + const log = logger.with({ + module: MODULE, executedRuleId: executedRule.id, ruleId: executedRule.ruleId, threadId: executedRule.threadId, @@ -45,6 +48,7 @@ export async function executeAct({ userId, emailAccountId, executedRule, + logger: log, }); if (action.type === ActionType.DRAFT_EMAIL && actionResult?.draftId) { @@ -55,7 +59,7 @@ export async function executeAct({ }); } } catch (error) { - logger.error("Error executing action", { error }); + log.error("Error executing action", { error }); await prisma.executedRule.update({ where: { id: executedRule.id }, data: { status: ExecutedRuleStatus.ERROR }, @@ -70,6 +74,6 @@ export async function executeAct({ data: { status: ExecutedRuleStatus.APPLIED }, }) .catch((error) => { - logger.error("Failed to update executed rule", { error }); + log.error("Failed to update executed rule", { error }); }); } diff --git a/apps/web/utils/ai/choose-rule/match-rules.test.ts b/apps/web/utils/ai/choose-rule/match-rules.test.ts index c2a9135a54..b99156bac8 100644 --- a/apps/web/utils/ai/choose-rule/match-rules.test.ts +++ b/apps/web/utils/ai/choose-rule/match-rules.test.ts @@ -28,10 +28,13 @@ import { isColdEmailRuleEnabled, } from "@/utils/cold-email/cold-email-rule"; import { isColdEmail } from "@/utils/cold-email/is-cold-email"; +import { createScopedLogger } from "@/utils/logger"; // Run with: // pnpm test match-rules.test.ts +const logger = createScopedLogger("test"); + const provider = { isReplyInThread: vi.fn().mockReturnValue(false), } as unknown as EmailProvider; @@ -59,7 +62,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ from: "test@gmail.com" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should not match when wildcard pattern doesn't match domain", () => { @@ -68,7 +71,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ from: "test@yahoo.com" }), }); - expect(matchesStaticRule(rule, message)).toBe(false); + expect(matchesStaticRule(rule, message, logger)).toBe(false); }); it("should handle multiple wildcards in pattern", () => { @@ -77,7 +80,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "This is important message" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should handle invalid regex patterns gracefully", () => { @@ -86,7 +89,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ from: "test@example.com" }), }); - expect(matchesStaticRule(rule, message)).toBe(false); + expect(matchesStaticRule(rule, message, logger)).toBe(false); }); it("should return false when no conditions are provided", () => { @@ -95,7 +98,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ from: "test@example.com" }), }); - expect(matchesStaticRule(rule, message)).toBe(false); + expect(matchesStaticRule(rule, message, logger)).toBe(false); }); it("should match body content with wildcard", () => { @@ -105,7 +108,7 @@ describe("matchesStaticRule", () => { textPlain: "Click here to unsubscribe from our newsletter", }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match @domain.com", () => { @@ -114,7 +117,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ from: "test@domain.com" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match Creator Message subject pattern", () => { @@ -125,7 +128,7 @@ describe("matchesStaticRule", () => { }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match exact Creator Message subject", () => { @@ -138,7 +141,7 @@ describe("matchesStaticRule", () => { }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match parentheses in subject", () => { @@ -147,7 +150,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Invoice (PDF)" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match plus sign in email address", () => { @@ -156,7 +159,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ from: "user+tag@gmail.com" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match dots in subject", () => { @@ -165,7 +168,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Order #123.456" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match dollar signs in subject", () => { @@ -174,7 +177,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Payment $100" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match curly braces in subject", () => { @@ -183,7 +186,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Template {name}" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match pipe symbol in subject", () => { @@ -192,7 +195,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Alert | System" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match question mark in subject", () => { @@ -201,7 +204,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Are you ready?" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match caret symbol in subject", () => { @@ -210,7 +213,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Version ^1.0" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match wildcards with special characters", () => { @@ -219,7 +222,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "URGENT [Important] Notice" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match common notification patterns", () => { @@ -228,7 +231,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ from: "noreply-notification@company.com" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match receipt patterns", () => { @@ -237,7 +240,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Your receipt from store" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should be case sensitive", () => { @@ -246,7 +249,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "urgent" }), }); - expect(matchesStaticRule(rule, message)).toBe(false); + expect(matchesStaticRule(rule, message, logger)).toBe(false); }); it("should handle empty header values gracefully", () => { @@ -255,7 +258,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ from: "" }), }); - expect(matchesStaticRule(rule, message)).toBe(false); + expect(matchesStaticRule(rule, message, logger)).toBe(false); }); it("should match backslash characters", () => { @@ -264,7 +267,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Path: C:\\Users\\Name" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should match multiple domains separated by pipe characters", () => { @@ -276,25 +279,25 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ from: "user@company-a.com" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); // Should match middle domain const message2 = getMessage({ headers: getHeaders({ from: "contact@startup-x.io" }), }); - expect(matchesStaticRule(rule, message2)).toBe(true); + expect(matchesStaticRule(rule, message2, logger)).toBe(true); // Should match last domain const message3 = getMessage({ headers: getHeaders({ from: "info@brand-z.co" }), }); - expect(matchesStaticRule(rule, message3)).toBe(true); + expect(matchesStaticRule(rule, message3, logger)).toBe(true); // Should not match domain not in list const message4 = getMessage({ headers: getHeaders({ from: "test@other-company.com" }), }); - expect(matchesStaticRule(rule, message4)).toBe(false); + expect(matchesStaticRule(rule, message4, logger)).toBe(false); }); it("should treat pipes as OR operator in 'to' field", () => { @@ -306,25 +309,25 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ to: "support@company.com" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); // Should match second email const message2 = getMessage({ headers: getHeaders({ to: "help@company.com" }), }); - expect(matchesStaticRule(rule, message2)).toBe(true); + expect(matchesStaticRule(rule, message2, logger)).toBe(true); // Should match third email const message3 = getMessage({ headers: getHeaders({ to: "contact@company.com" }), }); - expect(matchesStaticRule(rule, message3)).toBe(true); + expect(matchesStaticRule(rule, message3, logger)).toBe(true); // Should not match other email const message4 = getMessage({ headers: getHeaders({ to: "sales@company.com" }), }); - expect(matchesStaticRule(rule, message4)).toBe(false); + expect(matchesStaticRule(rule, message4, logger)).toBe(false); }); it("should combine wildcards with pipe OR logic in from field", () => { @@ -336,25 +339,25 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ from: "weekly@newsletter.com" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); // Should match wildcard + second domain const message2 = getMessage({ headers: getHeaders({ from: "campaign@marketing.org" }), }); - expect(matchesStaticRule(rule, message2)).toBe(true); + expect(matchesStaticRule(rule, message2, logger)).toBe(true); // Should match third pattern with wildcard const message3 = getMessage({ headers: getHeaders({ from: "notifications@example.com" }), }); - expect(matchesStaticRule(rule, message3)).toBe(true); + expect(matchesStaticRule(rule, message3, logger)).toBe(true); // Should not match pattern not in list const message4 = getMessage({ headers: getHeaders({ from: "test@other.com" }), }); - expect(matchesStaticRule(rule, message4)).toBe(false); + expect(matchesStaticRule(rule, message4, logger)).toBe(false); }); it("should treat pipes as literal characters in subject field", () => { @@ -365,13 +368,13 @@ describe("matchesStaticRule", () => { headers: getHeaders({ subject: "Status: Active | Pending | Completed" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); // Should not match partial pipe patterns const message2 = getMessage({ headers: getHeaders({ subject: "Status: Active" }), }); - expect(matchesStaticRule(rule, message2)).toBe(false); + expect(matchesStaticRule(rule, message2, logger)).toBe(false); }); it("should treat pipes as literal characters in body field", () => { @@ -383,14 +386,14 @@ describe("matchesStaticRule", () => { textPlain: "Please choose option A | B | C from the menu to continue", }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); // Should not match partial pipe patterns const message2 = getMessage({ headers: getHeaders(), textPlain: "Please choose option A to continue", }); - expect(matchesStaticRule(rule, message2)).toBe(false); + expect(matchesStaticRule(rule, message2, logger)).toBe(false); }); it("should handle empty patterns between pipes gracefully", () => { @@ -400,12 +403,12 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ from: "test@domain1.com" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); const message2 = getMessage({ headers: getHeaders({ from: "test@domain2.com" }), }); - expect(matchesStaticRule(rule, message2)).toBe(true); + expect(matchesStaticRule(rule, message2, logger)).toBe(true); }); it("should handle single pattern without pipes in from field", () => { @@ -414,7 +417,7 @@ describe("matchesStaticRule", () => { headers: getHeaders({ from: "user@single-domain.com" }), }); - expect(matchesStaticRule(rule, message)).toBe(true); + expect(matchesStaticRule(rule, message, logger)).toBe(true); }); it("should handle pipes at beginning and end of from pattern", () => { @@ -424,12 +427,12 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ from: "test@domain1.com" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); const message2 = getMessage({ headers: getHeaders({ from: "test@domain2.com" }), }); - expect(matchesStaticRule(rule, message2)).toBe(true); + expect(matchesStaticRule(rule, message2, logger)).toBe(true); }); it("should handle mixed conditions with pipes in from and literal pipes in subject", () => { @@ -445,7 +448,7 @@ describe("matchesStaticRule", () => { subject: "Alert | System Status", }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); // Should match with second domain const message2 = getMessage({ @@ -454,7 +457,7 @@ describe("matchesStaticRule", () => { subject: "Alert | System Status", }), }); - expect(matchesStaticRule(rule, message2)).toBe(true); + expect(matchesStaticRule(rule, message2, logger)).toBe(true); // Should not match with wrong domain const message3 = getMessage({ @@ -463,7 +466,7 @@ describe("matchesStaticRule", () => { subject: "Alert | System Status", }), }); - expect(matchesStaticRule(rule, message3)).toBe(false); + expect(matchesStaticRule(rule, message3, logger)).toBe(false); // Should not match with partial subject const message4 = getMessage({ @@ -472,7 +475,7 @@ describe("matchesStaticRule", () => { subject: "Alert", }), }); - expect(matchesStaticRule(rule, message4)).toBe(false); + expect(matchesStaticRule(rule, message4, logger)).toBe(false); }); it("should handle complex email patterns with pipes", () => { @@ -484,25 +487,25 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ from: "noreply@newsletter.com" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); // Should match second pattern const message2 = getMessage({ headers: getHeaders({ from: "system-notifications@company.com" }), }); - expect(matchesStaticRule(rule, message2)).toBe(true); + expect(matchesStaticRule(rule, message2, logger)).toBe(true); // Should match third pattern with plus and wildcard const message3 = getMessage({ headers: getHeaders({ from: "alerts+billing@service.io" }), }); - expect(matchesStaticRule(rule, message3)).toBe(true); + expect(matchesStaticRule(rule, message3, logger)).toBe(true); // Should not match unrelated pattern const message4 = getMessage({ headers: getHeaders({ from: "user@other.com" }), }); - expect(matchesStaticRule(rule, message4)).toBe(false); + expect(matchesStaticRule(rule, message4, logger)).toBe(false); }); it("should support comma as separator in from field", () => { @@ -514,25 +517,25 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ from: "user@company-a.com" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); // Should match second domain const message2 = getMessage({ headers: getHeaders({ from: "contact@company-b.org" }), }); - expect(matchesStaticRule(rule, message2)).toBe(true); + expect(matchesStaticRule(rule, message2, logger)).toBe(true); // Should match third domain const message3 = getMessage({ headers: getHeaders({ from: "info@startup-x.io" }), }); - expect(matchesStaticRule(rule, message3)).toBe(true); + expect(matchesStaticRule(rule, message3, logger)).toBe(true); // Should not match unlisted domain const message4 = getMessage({ headers: getHeaders({ from: "test@other.com" }), }); - expect(matchesStaticRule(rule, message4)).toBe(false); + expect(matchesStaticRule(rule, message4, logger)).toBe(false); }); it("should support comma as separator in to field", () => { @@ -547,6 +550,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ to: "support@company.com" }), }), + logger, ), ).toBe(true); @@ -556,6 +560,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ to: "help@company.com" }), }), + logger, ), ).toBe(true); @@ -565,6 +570,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ to: "contact@company.com" }), }), + logger, ), ).toBe(true); }); @@ -578,25 +584,25 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ from: "admin@company1.com" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); // Should match second domain const message2 = getMessage({ headers: getHeaders({ from: "admin@company2.com" }), }); - expect(matchesStaticRule(rule, message2)).toBe(true); + expect(matchesStaticRule(rule, message2, logger)).toBe(true); // Should match third domain const message3 = getMessage({ headers: getHeaders({ from: "admin@company3.com" }), }); - expect(matchesStaticRule(rule, message3)).toBe(true); + expect(matchesStaticRule(rule, message3, logger)).toBe(true); // Should not match unlisted domain const message4 = getMessage({ headers: getHeaders({ from: "admin@company4.com" }), }); - expect(matchesStaticRule(rule, message4)).toBe(false); + expect(matchesStaticRule(rule, message4, logger)).toBe(false); }); it("should support mixed separators (pipe, comma, OR)", () => { @@ -611,6 +617,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "user@company1.com" }), }), + logger, ), ).toBe(true); @@ -620,6 +627,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "user@company2.com" }), }), + logger, ), ).toBe(true); @@ -629,6 +637,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "user@company3.com" }), }), + logger, ), ).toBe(true); @@ -638,6 +647,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "user@company4.com" }), }), + logger, ), ).toBe(true); }); @@ -654,6 +664,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "user@company1.com" }), }), + logger, ), ).toBe(true); @@ -663,6 +674,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "user@company2.com" }), }), + logger, ), ).toBe(true); }); @@ -679,6 +691,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "weekly@newsletter.com" }), }), + logger, ), ).toBe(true); @@ -688,6 +701,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "campaign@marketing.org" }), }), + logger, ), ).toBe(true); @@ -697,6 +711,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "notifications@example.com" }), }), + logger, ), ).toBe(true); }); @@ -713,6 +728,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "user@company1.com" }), }), + logger, ), ).toBe(true); @@ -722,6 +738,7 @@ describe("matchesStaticRule", () => { getMessage({ headers: getHeaders({ from: "user@company2.com" }), }), + logger, ), ).toBe(true); }); @@ -735,13 +752,13 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ subject: "Option A, Option B, Option C" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); // Should not match partial const message2 = getMessage({ headers: getHeaders({ subject: "Option A" }), }); - expect(matchesStaticRule(rule, message2)).toBe(false); + expect(matchesStaticRule(rule, message2, logger)).toBe(false); }); it("should not treat OR as separator in subject field", () => { @@ -753,13 +770,13 @@ describe("matchesStaticRule", () => { const message1 = getMessage({ headers: getHeaders({ subject: "Status: Active OR Pending" }), }); - expect(matchesStaticRule(rule, message1)).toBe(true); + expect(matchesStaticRule(rule, message1, logger)).toBe(true); // Should not match partial const message2 = getMessage({ headers: getHeaders({ subject: "Status: Active" }), }); - expect(matchesStaticRule(rule, message2)).toBe(false); + expect(matchesStaticRule(rule, message2, logger)).toBe(false); }); }); @@ -781,6 +798,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); expect(result.matches[0].rule.id).toBe(rule.id); @@ -803,6 +821,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); expect(result.matches[0].rule.id).toBe(rule.id); @@ -825,6 +844,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); expect(result.matches).toHaveLength(0); @@ -856,6 +876,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); expect(result.matches[0]?.rule.id).toBe(rule.id); @@ -906,6 +927,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); // Group didn't match and no other conditions, so rule should NOT match @@ -952,6 +974,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); expect(result.matches[0]?.rule.id).toBe(rule.id); @@ -999,6 +1022,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); // Should match the first rule only @@ -1093,6 +1117,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); expect(result.matches).toHaveLength(1); @@ -1140,6 +1165,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); // The rule should be excluded (not matched) @@ -1181,6 +1207,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); expect(result.matches[0]?.rule.id).toBe(rule.id); @@ -1223,6 +1250,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); // Groups are independent of AND/OR operator - static match should work @@ -1265,6 +1293,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); // Should match via learned pattern and short-circuit (not check static) @@ -1321,6 +1350,7 @@ describe("findMatchingRule", () => { emailAccount, provider, modelType: "default", + logger, }); // Should match despite the display name format, due to the group rule @@ -1357,6 +1387,7 @@ describe("filterToReplyPreset", () => { potentialMatches, message, provider, + logger, ); expect(result).toHaveLength(1); @@ -1387,6 +1418,7 @@ describe("filterToReplyPreset", () => { potentialMatches, message, provider, + logger, ); // Should return all rules when no TO_REPLY rule exists @@ -1431,6 +1463,7 @@ describe("filterToReplyPreset", () => { potentialMatches, message, provider, + logger, ); // Should filter out TO_REPLY rule @@ -1479,6 +1512,7 @@ describe("filterToReplyPreset", () => { potentialMatches, message, provider, + logger, ); // Should keep TO_REPLY rule because sender has replied before @@ -1516,6 +1550,7 @@ describe("filterToReplyPreset", () => { potentialMatches, message, provider, + logger, ); // Should keep TO_REPLY rule because received count is low @@ -1549,6 +1584,7 @@ describe("filterToReplyPreset", () => { [toReplyRule], message, provider, + logger, ); // All no-reply variations should return the rule (not filtered) @@ -1582,6 +1618,7 @@ describe("filterToReplyPreset", () => { potentialMatches, message, provider, + logger, ); // Should return all rules when error occurs @@ -1607,6 +1644,7 @@ describe("filterToReplyPreset", () => { potentialMatches, message, provider, + logger, ); // Should return all rules when no sender email @@ -1708,6 +1746,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); expect(getColdEmailRule).toHaveBeenCalledWith(emailAccount.id); @@ -1750,6 +1789,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); expect(getColdEmailRule).toHaveBeenCalledWith(emailAccount.id); @@ -1790,6 +1830,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); expect(isColdEmail).toHaveBeenCalled(); @@ -1830,6 +1871,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); expect(result.matches[0]?.rule.id).toBe("calendar-rule"); @@ -1863,6 +1905,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); expect(aiChooseRule).toHaveBeenCalledWith( @@ -1919,6 +1962,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); // Should match via learned pattern @@ -1958,6 +2002,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider: threadProvider, modelType: "default", + logger, }); // Rule should not match because it's a thread and runOnThreads=false @@ -2071,6 +2116,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider: providerNoThread, modelType: "default", + logger, }); // Should match via static evaluation since groups are empty @@ -2108,6 +2154,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider: threadProvider, modelType: "default", + logger, }); expect(prisma.executedRule.findMany).toHaveBeenCalledTimes(1); @@ -2148,6 +2195,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider: threadProvider, modelType: "default", + logger, }); expect(prisma.executedRule.findMany).toHaveBeenCalledTimes(1); @@ -2180,6 +2228,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider: providerNotThread, modelType: "default", + logger, }); expect(prisma.executedRule.findMany).not.toHaveBeenCalled(); @@ -2211,6 +2260,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider: threadProvider, modelType: "default", + logger, }); expect(prisma.executedRule.findMany).not.toHaveBeenCalled(); @@ -2229,8 +2279,8 @@ describe("findMatchingRules - Integration Tests", () => { }); // Should not throw, just return false - expect(() => matchesStaticRule(rule, message)).not.toThrow(); - const result = matchesStaticRule(rule, message); + expect(() => matchesStaticRule(rule, message, logger)).not.toThrow(); + const result = matchesStaticRule(rule, message, logger); expect(result).toBe(false); }); @@ -2259,6 +2309,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); // Static matched, so should be sent to AI for AND check @@ -2291,6 +2342,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); // Reasoning should combine existing matchReasons text + AI reason @@ -2320,7 +2372,7 @@ describe("findMatchingRules - Integration Tests", () => { }) as unknown as RegExpConstructor; try { - const matched = matchesStaticRule(rule as any, message as any); + const matched = matchesStaticRule(rule as any, message as any, logger); expect(matched).toBe(false); } finally { // restore @@ -2351,6 +2403,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); expect(result.matches.map((m) => m.rule.id)).toEqual([]); @@ -2384,6 +2437,7 @@ describe("findMatchingRules - Integration Tests", () => { emailAccount, provider, modelType: "default", + logger, }); // Only one occurrence of dup-rule should remain @@ -2403,7 +2457,7 @@ describe("evaluateRuleConditions", () => { headers: getHeaders({ from: "test@example.com" }), }); - const result = evaluateRuleConditions({ rule, message }); + const result = evaluateRuleConditions({ rule, message, logger }); expect(result.matched).toBe(true); expect(result.potentialAiMatch).toBe(false); @@ -2416,7 +2470,7 @@ describe("evaluateRuleConditions", () => { headers: getHeaders({ from: "other@example.com" }), }); - const result = evaluateRuleConditions({ rule, message }); + const result = evaluateRuleConditions({ rule, message, logger }); expect(result.matched).toBe(false); expect(result.potentialAiMatch).toBe(false); @@ -2433,7 +2487,7 @@ describe("evaluateRuleConditions", () => { }); const message = getMessage(); - const result = evaluateRuleConditions({ rule, message }); + const result = evaluateRuleConditions({ rule, message, logger }); expect(result.matched).toBe(false); expect(result.potentialAiMatch).toBe(true); @@ -2450,7 +2504,7 @@ describe("evaluateRuleConditions", () => { headers: getHeaders({ from: "test@example.com" }), }); - const result = evaluateRuleConditions({ rule, message }); + const result = evaluateRuleConditions({ rule, message, logger }); expect(result.matched).toBe(true); expect(result.potentialAiMatch).toBe(false); @@ -2467,7 +2521,7 @@ describe("evaluateRuleConditions", () => { headers: getHeaders({ from: "other@example.com" }), }); - const result = evaluateRuleConditions({ rule, message }); + const result = evaluateRuleConditions({ rule, message, logger }); expect(result.matched).toBe(false); expect(result.potentialAiMatch).toBe(true); @@ -2484,7 +2538,7 @@ describe("evaluateRuleConditions", () => { headers: getHeaders({ from: "test@example.com" }), }); - const result = evaluateRuleConditions({ rule, message }); + const result = evaluateRuleConditions({ rule, message, logger }); expect(result.matched).toBe(false); expect(result.potentialAiMatch).toBe(true); @@ -2501,7 +2555,7 @@ describe("evaluateRuleConditions", () => { headers: getHeaders({ from: "other@example.com" }), }); - const result = evaluateRuleConditions({ rule, message }); + const result = evaluateRuleConditions({ rule, message, logger }); expect(result.matched).toBe(false); expect(result.potentialAiMatch).toBe(false); @@ -2518,7 +2572,7 @@ describe("evaluateRuleConditions", () => { }); const message = getMessage(); - const result = evaluateRuleConditions({ rule, message }); + const result = evaluateRuleConditions({ rule, message, logger }); expect(result.matched).toBe(false); expect(result.potentialAiMatch).toBe(false); @@ -2535,7 +2589,7 @@ describe("evaluateRuleConditions", () => { headers: getHeaders({ from: "other@example.com" }), }); - const result = evaluateRuleConditions({ rule, message }); + const result = evaluateRuleConditions({ rule, message, logger }); expect(result.matched).toBe(false); expect(result.potentialAiMatch).toBe(false); diff --git a/apps/web/utils/ai/choose-rule/match-rules.ts b/apps/web/utils/ai/choose-rule/match-rules.ts index d52693793f..21508e556d 100644 --- a/apps/web/utils/ai/choose-rule/match-rules.ts +++ b/apps/web/utils/ai/choose-rule/match-rules.ts @@ -15,7 +15,7 @@ import prisma from "@/utils/prisma"; import { aiChooseRule } from "@/utils/ai/choose-rule/ai-choose-rule"; import { getEmailForLLM } from "@/utils/get-email-from-message"; import type { EmailAccountWithAI } from "@/utils/llms/types"; -import { createScopedLogger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import type { MatchReason, MatchingRuleResult, @@ -32,7 +32,7 @@ import { import { isColdEmail } from "@/utils/cold-email/is-cold-email"; import { isConversationStatusType } from "@/utils/reply-tracker/conversation-status-config"; -const logger = createScopedLogger("match-rules"); +const MODULE = "match-rules"; const TO_REPLY_RECEIVED_THRESHOLD = 10; @@ -50,13 +50,16 @@ export async function findMatchingRules({ emailAccount, provider, modelType, + logger: log, }: { rules: RuleWithActions[]; message: ParsedMessage; emailAccount: EmailAccountWithAI; provider: EmailProvider; modelType: ModelType; + logger: Logger; }): Promise { + const logger = log.with({ module: MODULE }); const coldEmailRule = await getColdEmailRule(emailAccount.id); if (coldEmailRule && isColdEmailRuleEnabled(coldEmailRule)) { @@ -97,6 +100,7 @@ export async function findMatchingRules({ emailAccount, provider, modelType, + logger, ); return results; @@ -128,12 +132,14 @@ async function findPotentialMatchingRules({ isThread, provider, emailAccountId, + logger, }: { rules: RuleWithActions[]; message: ParsedMessage; isThread: boolean; provider: EmailProvider; emailAccountId: string; + logger: Logger; }): Promise { const matches: { rule: RuleWithActions; @@ -209,6 +215,7 @@ async function findPotentialMatchingRules({ const { matched, potentialAiMatch, matchReasons } = evaluateRuleConditions({ rule, message, + logger, }); if (matched) { @@ -228,6 +235,7 @@ async function findPotentialMatchingRules({ potentialAiMatches, message, provider, + logger, ); const hasLearnedPatternMatch = matches.some((m) => @@ -247,9 +255,11 @@ async function findPotentialMatchingRules({ export function evaluateRuleConditions({ rule, message, + logger, }: { rule: RuleWithActions; message: ParsedMessage; + logger: Logger; }): { matched: boolean; potentialAiMatch: boolean; @@ -264,7 +274,7 @@ export function evaluateRuleConditions({ // Check STATIC condition const staticMatch = hasStaticCondition - ? matchesStaticRule(rule, message) + ? matchesStaticRule(rule, message, logger) : false; if (staticMatch) { matchReasons.push({ type: ConditionType.STATIC }); @@ -363,6 +373,7 @@ async function findMatchingRulesWithReasons( emailAccount: EmailAccountWithAI, provider: EmailProvider, modelType: ModelType, + logger: Logger, ): Promise { const isThread = provider.isReplyInThread(message); @@ -372,6 +383,7 @@ async function findMatchingRulesWithReasons( isThread, provider, emailAccountId: emailAccount.id, + logger, }); if (potentialAiMatches.length) { @@ -442,7 +454,9 @@ async function findMatchingRulesWithReasons( export function matchesStaticRule( rule: Pick, message: ParsedMessage, + logger: Logger, ) { + const log = logger.with({ module: MODULE }); const { from, to, subject, body } = rule; if (!from && !to && !subject && !body) return false; @@ -475,7 +489,7 @@ export function matchesStaticRule( return false; } catch (error) { - logger.error("Invalid regex pattern", { pattern, error }); + log.error("Invalid regex pattern", { pattern, error }); return false; } }; @@ -535,7 +549,9 @@ export async function filterConversationStatusRules< potentialMatches: T[], message: ParsedMessage, provider: EmailProvider, + logger: Logger, ): Promise { + const log = logger.with({ module: MODULE }); const toReplyRule = potentialMatches.find( (r) => r.systemType === SystemType.TO_REPLY, ); @@ -578,7 +594,7 @@ export async function filterConversationStatusRules< ); if (!hasReplied && receivedCount >= TO_REPLY_RECEIVED_THRESHOLD) { - logger.info( + log.info( "Filtering out TO_REPLY rule due to no prior reply and high received count", { ruleId: toReplyRule.id, @@ -589,7 +605,7 @@ export async function filterConversationStatusRules< return filteredOutConversationStatusRules(); } } catch (error) { - logger.error("Error checking reply history for TO_REPLY filter", { + log.error("Error checking reply history for TO_REPLY filter", { senderEmail, error, }); diff --git a/apps/web/utils/ai/choose-rule/run-rules.test.ts b/apps/web/utils/ai/choose-rule/run-rules.test.ts index 6b3a03f557..0238943952 100644 --- a/apps/web/utils/ai/choose-rule/run-rules.test.ts +++ b/apps/web/utils/ai/choose-rule/run-rules.test.ts @@ -14,6 +14,9 @@ import { ConditionType } from "@/utils/config"; import prisma from "@/utils/__mocks__/prisma"; import type { RuleWithActions } from "@/utils/types"; import { getAction } from "@/__tests__/helpers"; +import { createScopedLogger } from "@/utils/logger"; + +const logger = createScopedLogger("test"); vi.mock("@/utils/prisma"); vi.mock("server-only", () => ({})); @@ -65,6 +68,7 @@ describe("ensureConversationRuleContinuity", () => { conversationRules: [], regularRules: [regularRule], matches, + logger, }); expect(result).toEqual(matches); @@ -82,6 +86,7 @@ describe("ensureConversationRuleContinuity", () => { conversationRules: [toReplyRule], regularRules: [regularRule, conversationMetaRule], matches, + logger, }); expect(result).toEqual(matches); @@ -118,6 +123,7 @@ describe("ensureConversationRuleContinuity", () => { conversationRules: [toReplyRule], regularRules: [regularRule, conversationMetaRule], matches, + logger, }); expect(result).toEqual(matches); @@ -136,6 +142,7 @@ describe("ensureConversationRuleContinuity", () => { conversationRules: [toReplyRule], regularRules: [regularRule, conversationMetaRule], matches, + logger, }); expect(result).toHaveLength(2); @@ -159,6 +166,7 @@ describe("ensureConversationRuleContinuity", () => { conversationRules: [toReplyRule], regularRules: [regularRule], // No meta rule matches, + logger, }); expect(result).toEqual(matches); @@ -178,6 +186,7 @@ describe("ensureConversationRuleContinuity", () => { conversationRules: [toReplyRule], regularRules: [regularRule, conversationMetaRule], matches, + logger, }); expect(matches).toEqual(originalMatches); @@ -195,6 +204,7 @@ describe("ensureConversationRuleContinuity", () => { conversationRules: [toReplyRule], regularRules: [regularRule, conversationMetaRule], matches, + logger, }); expect(prisma.executedRule.findFirst).toHaveBeenCalledWith({ @@ -238,7 +248,7 @@ describe("limitDraftEmailActions", () => { }, ]; - const result = limitDraftEmailActions(matches); + const result = limitDraftEmailActions(matches, logger); expect(result).toBe(matches); }); @@ -252,7 +262,7 @@ describe("limitDraftEmailActions", () => { }, ]; - const result = limitDraftEmailActions(matches); + const result = limitDraftEmailActions(matches, createScopedLogger("test")); expect(result).toBe(matches); }); @@ -281,7 +291,7 @@ describe("limitDraftEmailActions", () => { }, ]; - const result = limitDraftEmailActions(matches); + const result = limitDraftEmailActions(matches, logger); expect(result[0].rule.actions).toEqual([]); expect(result[1].rule.actions).toHaveLength(1); @@ -318,7 +328,7 @@ describe("limitDraftEmailActions", () => { }, ]; - const result = limitDraftEmailActions(matches); + const result = limitDraftEmailActions(matches, logger); expect(result[0].rule.actions).toHaveLength(1); expect(result[0].rule.actions[0].type).toBe(ActionType.LABEL); @@ -349,7 +359,7 @@ describe("limitDraftEmailActions", () => { }, ]; - const result = limitDraftEmailActions(matches); + const result = limitDraftEmailActions(matches, logger); expect(result[0].rule.actions).toHaveLength(1); expect(result[0].rule.actions[0].id).toBe("draft-1"); @@ -380,7 +390,7 @@ describe("limitDraftEmailActions", () => { }, ]; - const result = limitDraftEmailActions(matches); + const result = limitDraftEmailActions(matches, logger); expect(result[0].rule.actions).toHaveLength(1); expect(result[0].rule.actions[0].id).toBe("draft-1"); @@ -412,7 +422,7 @@ describe("limitDraftEmailActions", () => { }, ]; - const result = limitDraftEmailActions(matches); + const result = limitDraftEmailActions(matches, logger); // Should select draft-2 because it has fixed content (static), even though draft-1 came first expect(result[0].rule.actions).toEqual([]); diff --git a/apps/web/utils/ai/choose-rule/run-rules.ts b/apps/web/utils/ai/choose-rule/run-rules.ts index 028c8b9630..ec0de1f069 100644 --- a/apps/web/utils/ai/choose-rule/run-rules.ts +++ b/apps/web/utils/ai/choose-rule/run-rules.ts @@ -12,7 +12,6 @@ import { findMatchingRules } from "@/utils/ai/choose-rule/match-rules"; import { getActionItemsWithAiArgs } from "@/utils/ai/choose-rule/choose-args"; import { executeAct } from "@/utils/ai/choose-rule/execute"; import prisma from "@/utils/prisma"; -import { createScopedLogger } from "@/utils/logger"; import type { MatchReason } from "@/utils/ai/choose-rule/types"; import { serializeMatchReasons } from "@/utils/ai/choose-rule/types"; import { sanitizeActionFields } from "@/utils/action-item"; @@ -38,8 +37,9 @@ import { removeConflictingThreadStatusLabels } from "@/utils/reply-tracker/label import { saveColdEmail } from "@/utils/cold-email/is-cold-email"; import { internalDateToDate } from "@/utils/date"; import { ConditionType } from "@/utils/config"; +import type { Logger } from "@/utils/logger"; -const logger = createScopedLogger("ai-run-rules"); +const MODULE = "ai/choose-rule"; export type RunRulesResult = { rule?: Pick< @@ -71,6 +71,7 @@ export async function runRules({ emailAccount, isTest, modelType, + logger, }: { provider: EmailProvider; message: ParsedMessage; @@ -78,6 +79,7 @@ export async function runRules({ emailAccount: EmailAccountWithAI; isTest: boolean; modelType: ModelType; + logger: Logger; }): Promise { const batchTimestamp = new Date(); // Single timestamp for this batch execution const { regularRules, conversationRules } = prepareRulesWithMetaRule(rules); @@ -88,6 +90,7 @@ export async function runRules({ emailAccount, provider, modelType, + logger, }); // Auto-reapply conversation tracking for thread continuity @@ -97,11 +100,13 @@ export async function runRules({ conversationRules, regularRules, matches: results.matches, + logger, }); - const finalMatches = limitDraftEmailActions(conversationAwareMatches); + const finalMatches = limitDraftEmailActions(conversationAwareMatches, logger); logger.trace("Matching rule", () => ({ + module: MODULE, results: finalMatches.map(filterNullProperties), })); @@ -180,6 +185,7 @@ export async function runRules({ isTest, modelType, batchTimestamp, + logger, ); executedRules.push({ @@ -245,6 +251,7 @@ async function executeMatchedRule( isTest: boolean, modelType: ModelType, batchTimestamp: Date, + logger: Logger, ) { const actionItems = await getActionItemsWithAiArgs({ message, @@ -252,6 +259,7 @@ async function executeMatchedRule( selectedRule: rule, client, modelType, + logger, }); const { immediateActions, delayedActions } = groupBy(actionItems, (item) => @@ -353,6 +361,7 @@ async function executeMatchedRule( await executeAct({ client, userEmail: emailAccount.email, + logger, userId: emailAccount.userId, emailAccountId: emailAccount.id, executedRule, @@ -475,12 +484,14 @@ export async function ensureConversationRuleContinuity({ conversationRules, regularRules, matches, + logger, }: { emailAccountId: string; threadId: string; conversationRules: RuleWithActions[]; regularRules: RuleWithActions[]; matches: { rule: RuleWithActions; matchReasons?: MatchReason[] }[]; + logger: Logger; }): Promise<{ rule: RuleWithActions; matchReasons?: MatchReason[] }[]> { if (conversationRules.length === 0) { return matches; @@ -506,6 +517,7 @@ export async function ensureConversationRuleContinuity({ logger.info( "Automatically adding conversation meta rule due to previous application in thread", + { module: MODULE }, ); // Find the meta rule in regularRules @@ -545,6 +557,7 @@ export function limitDraftEmailActions( rule: RuleWithActions; matchReasons?: MatchReason[]; }[], + logger: Logger, ): { rule: RuleWithActions; matchReasons?: MatchReason[]; @@ -571,6 +584,7 @@ export function limitDraftEmailActions( const selectedDraftId = preferredCandidate.action.id; logger.info("Limiting draft actions to a single selection", { + module: MODULE, selectedDraftId, }); diff --git a/apps/web/utils/email/google.ts b/apps/web/utils/email/google.ts index 2cae25f05d..77ef9aecf2 100644 --- a/apps/web/utils/email/google.ts +++ b/apps/web/utils/email/google.ts @@ -73,13 +73,16 @@ import { createScopedLogger, type Logger } from "@/utils/logger"; import { extractEmailAddress } from "@/utils/email"; import { getGmailSignatures } from "@/utils/gmail/signature-settings"; -const logger = createScopedLogger("gmail-provider"); - export class GmailProvider implements EmailProvider { readonly name = "google"; private readonly client: gmail_v1.Gmail; - constructor(client: gmail_v1.Gmail) { + private readonly logger: Logger; + + constructor(client: gmail_v1.Gmail, logger?: Logger) { this.client = client; + this.logger = (logger || createScopedLogger("gmail-provider")).with({ + provider: "google", + }); } toJSON() { @@ -241,7 +244,7 @@ export class GmailProvider implements EmailProvider { } async archiveMessage(messageId: string): Promise { - const log = logger.with({ + const log = this.logger.with({ action: "archiveMessage", messageId, }); @@ -265,7 +268,7 @@ export class GmailProvider implements EmailProvider { } private async archiveMessagesBulk(messageIds: string[]): Promise { - const log = logger.with({ + const log = this.logger.with({ action: "archiveMessagesBulk", messageIds: messageIds, }); @@ -292,7 +295,7 @@ export class GmailProvider implements EmailProvider { ownerEmail: string, emailAccountId: string, ): Promise { - const log = logger.with({ + const log = this.logger.with({ action: "archiveMessagesFromSenders", emailAccountId, email: ownerEmail, @@ -374,7 +377,7 @@ export class GmailProvider implements EmailProvider { ownerEmail: string, emailAccountId: string, ): Promise { - const log = logger.with({ + const log = this.logger.with({ action: "bulkTrashFromSenders", emailAccountId, email: ownerEmail, @@ -524,7 +527,7 @@ export class GmailProvider implements EmailProvider { labelId: string; labelName: string | null; }): Promise<{ usedFallback?: boolean; actualLabelId?: string }> { - const log = logger.with({ + const log = this.logger.with({ action: "labelMessage", messageId, labelId, @@ -586,7 +589,7 @@ export class GmailProvider implements EmailProvider { userEmail: string, executedRule?: { id: string; threadId: string; emailAccountId: string }, ): Promise<{ draftId: string }> { - const log = logger.with({ + const log = this.logger.with({ action: "draftEmail", email: userEmail, executedRuleId: executedRule?.id, @@ -672,7 +675,7 @@ export class GmailProvider implements EmailProvider { } async blockUnsubscribedEmail(messageId: string): Promise { - const log = logger.with({ + const log = this.logger.with({ action: "blockUnsubscribedEmail", messageId, }); @@ -964,7 +967,7 @@ export class GmailProvider implements EmailProvider { } async checkIfReplySent(senderEmail: string): Promise { - const log = logger.with({ + const log = this.logger.with({ action: "checkIfReplySent", sender: senderEmail, }); @@ -990,7 +993,7 @@ export class GmailProvider implements EmailProvider { senderEmail: string, threshold: number, ): Promise { - const log = logger.with({ + const log = this.logger.with({ action: "countReceivedMessages", sender: senderEmail, threshold, @@ -1195,7 +1198,7 @@ export class GmailProvider implements EmailProvider { { startHistoryId: options.startHistoryId?.toString(), }, - options.logger || logger, + options.logger || this.logger, ); } @@ -1226,7 +1229,7 @@ export class GmailProvider implements EmailProvider { } async getFolders() { - logger.warn("Getting folders is not supported for Gmail"); + this.logger.warn("Getting folders is not supported for Gmail"); return []; } @@ -1235,11 +1238,11 @@ export class GmailProvider implements EmailProvider { _ownerEmail: string, _folderName: string, ): Promise { - logger.warn("Moving thread to folder is not supported for Gmail"); + this.logger.warn("Moving thread to folder is not supported for Gmail"); } async getOrCreateOutlookFolderIdByName(_folderName: string): Promise { - logger.warn("Moving thread to folder is not supported for Gmail"); + this.logger.warn("Moving thread to folder is not supported for Gmail"); return ""; } diff --git a/apps/web/utils/email/microsoft.ts b/apps/web/utils/email/microsoft.ts index 77c06897e8..8b60b297b8 100644 --- a/apps/web/utils/email/microsoft.ts +++ b/apps/web/utils/email/microsoft.ts @@ -67,14 +67,16 @@ import { hasUnquotedParentFolderId } from "@/utils/outlook/message"; import { extractSignatureFromHtml } from "@/utils/email/signature-extraction"; import { moveMessagesForSenders } from "@/utils/outlook/batch"; -const logger = createScopedLogger("outlook-provider"); - export class OutlookProvider implements EmailProvider { readonly name = "microsoft"; private readonly client: OutlookClient; + private readonly logger: Logger; - constructor(client: OutlookClient) { + constructor(client: OutlookClient, logger?: Logger) { this.client = client; + this.logger = (logger || createScopedLogger("outlook-provider")).with({ + provider: "microsoft", + }); } toJSON() { @@ -110,7 +112,7 @@ export class OutlookProvider implements EmailProvider { snippet: messages[0]?.snippet || "", }; } catch (error) { - logger.error("getThread failed", { + this.logger.error("getThread failed", { threadId, error: error instanceof Error ? error.message : error, errorCode: (error as any)?.code, @@ -149,7 +151,7 @@ export class OutlookProvider implements EmailProvider { return message; } catch (error) { const err = error as any; - logger.error("getMessage failed", { + this.logger.error("getMessage failed", { messageId, error: error instanceof Error ? error.message : error, errorCode: err?.code, @@ -222,7 +224,7 @@ export class OutlookProvider implements EmailProvider { const sentItemsFolderId = folderIds.sentitems; if (!sentItemsFolderId) { - logger.warn("Could not find sent items folder"); + this.logger.warn("Could not find sent items folder"); return []; } @@ -348,7 +350,7 @@ export class OutlookProvider implements EmailProvider { let category = await this.getLabelById(labelId); if (!category && labelName) { - logger.warn("Category not found by ID, trying to get by name", { + this.logger.warn("Category not found by ID, trying to get by name", { labelId, labelName, }); @@ -408,7 +410,7 @@ export class OutlookProvider implements EmailProvider { handlePreviousDraftDeletion({ client: this, executedRule, - logger, + logger: this.logger, }), ]); return { draftId: result.id || "" }; @@ -493,7 +495,7 @@ export class OutlookProvider implements EmailProvider { return messages; } catch (error) { const err = error as any; - logger.error("getThreadMessages failed", { + this.logger.error("getThreadMessages failed", { threadId, error: error instanceof Error ? error.message : error, errorCode: err?.code, @@ -525,7 +527,7 @@ export class OutlookProvider implements EmailProvider { const parsedMessage = await getMessage(message.id, this.client); messages.push(parsedMessage); } catch (error) { - logger.warn("Failed to parse message in inbox thread", { + this.logger.warn("Failed to parse message in inbox thread", { error, messageId: message.id, threadId, @@ -540,7 +542,10 @@ export class OutlookProvider implements EmailProvider { return dateA - dateB; // asc order (oldest first) }); } catch (error) { - logger.error("Error fetching inbox thread messages", { error, threadId }); + this.logger.error("Error fetching inbox thread messages", { + error, + threadId, + }); throw error; } } @@ -570,7 +575,7 @@ export class OutlookProvider implements EmailProvider { (error as { statusCode?: number; code?: string }).code === "CategoryNotFound" ) { - logger.info("Label not found, skipping removal", { + this.logger.info("Label not found, skipping removal", { threadId, labelId, }); @@ -686,7 +691,7 @@ export class OutlookProvider implements EmailProvider { return mappedFilters; } catch (error) { - logger.error("Error in Outlook getFiltersList", { error }); + this.logger.error("Error in Outlook getFiltersList", { error }); throw error; } } @@ -721,7 +726,7 @@ export class OutlookProvider implements EmailProvider { messages: ParsedMessage[]; nextPageToken?: string; }> { - logger.info("getMessagesWithPagination called", { + this.logger.info("getMessagesWithPagination called", { query: options.query, maxResults: options.maxResults, pageToken: options.pageToken, @@ -742,7 +747,7 @@ export class OutlookProvider implements EmailProvider { dateFilters.push(`receivedDateTime gt ${options.after.toISOString()}`); } - logger.info("Query parameters separated", { + this.logger.info("Query parameters separated", { originalQuery, dateFilters, hasSearchQuery: !!originalQuery.trim(), @@ -765,7 +770,7 @@ export class OutlookProvider implements EmailProvider { // Only apply folder filtering if the query doesn't already specify parentFolderId const folderId = queryHasParentFolderId ? undefined : inboxFolderId; - logger.info("Calling queryBatchMessages with separated parameters", { + this.logger.info("Calling queryBatchMessages with separated parameters", { searchQuery: originalQuery.trim() || undefined, dateFilters, maxResults: options.maxResults || 20, @@ -934,10 +939,10 @@ export class OutlookProvider implements EmailProvider { maxResults: 1, }); const sent = (response.messages?.length ?? 0) > 0; - logger.info("Checked for sent reply", { senderEmail, sent }); + this.logger.info("Checked for sent reply", { senderEmail, sent }); return sent; } catch (error) { - logger.error("Error checking if reply was sent", { + this.logger.error("Error checking if reply was sent", { error, senderEmail, }); @@ -951,7 +956,7 @@ export class OutlookProvider implements EmailProvider { ): Promise { try { const query = `from:${senderEmail}`; - logger.info("Checking received message count", { + this.logger.info("Checking received message count", { senderEmail, threshold, }); @@ -963,13 +968,13 @@ export class OutlookProvider implements EmailProvider { }); const count = response.messages?.length ?? 0; - logger.info("Received message count check result", { + this.logger.info("Received message count check result", { senderEmail, count, }); return count; } catch (error) { - logger.error("Error counting received messages", { + this.logger.error("Error counting received messages", { error, senderEmail, }); @@ -1125,7 +1130,7 @@ export class OutlookProvider implements EmailProvider { }) => { // Skip messages without conversationId if (!message.conversationId) { - logger.warn("Message missing conversationId", { + this.logger.warn("Message missing conversationId", { messageId: message.id, }); return; @@ -1213,7 +1218,7 @@ export class OutlookProvider implements EmailProvider { return hasPreviousEmail; } catch (error) { - logger.error("Error checking previous communications", { + this.logger.error("Error checking previous communications", { error, options, }); @@ -1251,7 +1256,7 @@ export class OutlookProvider implements EmailProvider { id: options.historyId?.toString() || "0", conversationId: options.startHistoryId?.toString() || null, }, - logger: options.logger || logger, + logger: options.logger || this.logger, }); } @@ -1273,7 +1278,7 @@ export class OutlookProvider implements EmailProvider { async unwatchEmails(subscriptionId?: string): Promise { if (!subscriptionId) { - logger.warn("No subscription ID provided for Outlook unwatch"); + this.logger.warn("No subscription ID provided for Outlook unwatch"); return; } await unwatchOutlook(this.client.getClient(), subscriptionId); @@ -1283,7 +1288,7 @@ export class OutlookProvider implements EmailProvider { try { return atob(message.conversationIndex || "").length > 22; } catch (error) { - logger.warn("Invalid conversationIndex base64", { + this.logger.warn("Invalid conversationIndex base64", { conversationIndex: message.conversationIndex, error, }); @@ -1316,11 +1321,11 @@ export class OutlookProvider implements EmailProvider { destinationId: "archive", }); - logger.info("Message archived successfully", { + this.logger.info("Message archived successfully", { messageId, }); } catch (error) { - logger.error("Failed to archive message", { + this.logger.error("Failed to archive message", { messageId, error: error instanceof Error ? error.message : error, }); @@ -1391,10 +1396,10 @@ export class OutlookProvider implements EmailProvider { } } - logger.info("No signature found in recent sent emails"); + this.logger.info("No signature found in recent sent emails"); return []; } catch (error) { - logger.error("Failed to extract signature from sent emails", { + this.logger.error("Failed to extract signature from sent emails", { error: error instanceof Error ? error.message : String(error), }); return []; diff --git a/apps/web/utils/email/provider.ts b/apps/web/utils/email/provider.ts index f72f9782bb..7e2118949e 100644 --- a/apps/web/utils/email/provider.ts +++ b/apps/web/utils/email/provider.ts @@ -9,20 +9,23 @@ import { isMicrosoftProvider, } from "@/utils/email/provider-types"; import type { EmailProvider } from "@/utils/email/types"; +import type { Logger } from "@/utils/logger"; export async function createEmailProvider({ emailAccountId, provider, + logger, }: { emailAccountId: string; provider: string; + logger?: Logger; }): Promise { if (isGoogleProvider(provider)) { const client = await getGmailClientForEmail({ emailAccountId }); - return new GmailProvider(client); + return new GmailProvider(client, logger); } else if (isMicrosoftProvider(provider)) { const client = await getOutlookClientForEmail({ emailAccountId }); - return new OutlookProvider(client); + return new OutlookProvider(client, logger); } throw new Error(`Unsupported provider: ${provider}`); diff --git a/apps/web/utils/email/watch-manager.ts b/apps/web/utils/email/watch-manager.ts index e1481b03da..d591639f62 100644 --- a/apps/web/utils/email/watch-manager.ts +++ b/apps/web/utils/email/watch-manager.ts @@ -174,6 +174,7 @@ async function watchEmailAccount( const provider = await createEmailProvider({ emailAccountId: emailAccount.id, provider: account.provider, + logger, }); const result = await watchEmails({ diff --git a/apps/web/utils/middleware.ts b/apps/web/utils/middleware.ts index cf082cd79e..3f9b4b2fad 100644 --- a/apps/web/utils/middleware.ts +++ b/apps/web/utils/middleware.ts @@ -290,14 +290,13 @@ async function emailProviderMiddleware( const provider = await createEmailProvider({ emailAccountId: emailAccount.id, provider: emailAccount.account.provider, + logger: emailAccountReq.logger, }); const providerReq = emailAccountReq.clone() as RequestWithEmailProvider; providerReq.auth = emailAccountReq.auth; providerReq.emailProvider = provider; - providerReq.logger = emailAccountReq.logger.with({ - provider: emailAccount.account.provider, - }); + providerReq.logger = emailAccountReq.logger; return providerReq; } catch (error) { diff --git a/apps/web/utils/scheduled-actions/executor.test.ts b/apps/web/utils/scheduled-actions/executor.test.ts index 92543ca761..dcc02b7318 100644 --- a/apps/web/utils/scheduled-actions/executor.test.ts +++ b/apps/web/utils/scheduled-actions/executor.test.ts @@ -2,9 +2,12 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { ActionType, ScheduledActionStatus } from "@prisma/client"; import { executeScheduledAction } from "./executor"; import prisma from "@/utils/__mocks__/prisma"; +import { createScopedLogger } from "@/utils/logger"; // Run with: pnpm test utils/scheduled-actions/executor.test.ts +const logger = createScopedLogger("test"); + vi.mock("server-only", () => ({})); vi.mock("@/utils/prisma"); vi.mock("@/utils/user/get", () => ({ @@ -109,6 +112,7 @@ describe("executor", () => { automated: true, reason: null, ruleId: null, + matchMetadata: null, }); const { runActionFunction } = await import("@/utils/ai/actions"); @@ -137,6 +141,7 @@ describe("executor", () => { const result = await executeScheduledAction( mockScheduledAction, mockEmailProvider, + logger, ); expect(result.success).toBe(true); @@ -215,6 +220,7 @@ describe("executor", () => { const result = await executeScheduledAction( mockScheduledAction, mockEmailProvider, + logger, ); expect(result.success).toBe(false); @@ -243,7 +249,11 @@ describe("executor", () => { provider: "google", }); - await executeScheduledAction(mockScheduledAction, mockEmailProvider); + await executeScheduledAction( + mockScheduledAction, + mockEmailProvider, + logger, + ); expect(prisma.scheduledAction.update).toHaveBeenCalledWith({ where: { id: "scheduled-action-123" }, diff --git a/apps/web/utils/scheduled-actions/executor.ts b/apps/web/utils/scheduled-actions/executor.ts index e083427a7a..62b9b720ab 100644 --- a/apps/web/utils/scheduled-actions/executor.ts +++ b/apps/web/utils/scheduled-actions/executor.ts @@ -4,13 +4,13 @@ import { type ScheduledAction, } from "@prisma/client"; import prisma from "@/utils/prisma"; -import { createScopedLogger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import { getEmailAccountWithAiAndTokens } from "@/utils/user/get"; import { runActionFunction } from "@/utils/ai/actions"; import type { ActionItem, EmailForAction } from "@/utils/ai/types"; import type { EmailProvider } from "@/utils/email/types"; -const logger = createScopedLogger("scheduled-actions-executor"); +const MODULE = "scheduled-actions-executor"; /** * Execute a scheduled action @@ -18,14 +18,17 @@ const logger = createScopedLogger("scheduled-actions-executor"); export async function executeScheduledAction( scheduledAction: ScheduledAction, client: EmailProvider, + logger: Logger, ) { - logger.info("Executing scheduled action", { + const log = logger.with({ + module: MODULE, scheduledActionId: scheduledAction.id, actionType: scheduledAction.actionType, messageId: scheduledAction.messageId, - emailAccountId: scheduledAction.emailAccountId, }); + log.info("Executing scheduled action"); + try { const emailAccount = await getEmailAccountWithAiAndTokens({ emailAccountId: scheduledAction.emailAccountId, @@ -34,11 +37,12 @@ export async function executeScheduledAction( throw new Error("Email account not found"); } - const emailMessage = await validateEmailState(client, scheduledAction); + const emailMessage = await validateEmailState(client, scheduledAction, log); if (!emailMessage) { await markActionCompleted( scheduledAction.id, null, + log, "Email no longer exists", ); return { success: true, reason: "Email no longer exists" }; @@ -66,24 +70,25 @@ export async function executeScheduledAction( id: emailAccount.id, }, scheduledAction, + log, }); - await markActionCompleted(scheduledAction.id, executedAction?.id); - await checkAndCompleteExecutedRule(scheduledAction.executedRuleId); + await markActionCompleted(scheduledAction.id, executedAction?.id, log); + await checkAndCompleteExecutedRule(scheduledAction.executedRuleId, log); - logger.info("Successfully executed scheduled action", { + log.info("Successfully executed scheduled action", { scheduledActionId: scheduledAction.id, executedActionId: executedAction?.id, }); return { success: true, executedActionId: executedAction?.id }; } catch (error: unknown) { - logger.error("Failed to execute scheduled action", { + log.error("Failed to execute scheduled action", { scheduledActionId: scheduledAction.id, error, }); - await markActionFailed(scheduledAction.id, error); + await markActionFailed(scheduledAction.id, error, log); return { success: false, error }; } } @@ -94,12 +99,13 @@ export async function executeScheduledAction( async function validateEmailState( client: EmailProvider, scheduledAction: ScheduledAction, + log: Logger, ): Promise { try { const message = await client.getMessage(scheduledAction.messageId); if (!message) { - logger.info("Email no longer exists", { + log.info("Email no longer exists", { messageId: scheduledAction.messageId, scheduledActionId: scheduledAction.id, }); @@ -123,7 +129,7 @@ async function validateEmailState( error instanceof Error && error.message === "Requested entity was not found." ) { - logger.info("Email not found during validation", { + log.info("Email not found during validation", { messageId: scheduledAction.messageId, scheduledActionId: scheduledAction.id, }); @@ -143,12 +149,14 @@ async function executeDelayedAction({ emailMessage, emailAccount, scheduledAction, + log, }: { client: EmailProvider; actionItem: ActionItem; emailMessage: EmailForAction; emailAccount: { email: string; userId: string; id: string }; scheduledAction: ScheduledAction; + log: Logger; }) { const executedAction = await prisma.executedAction.create({ data: { @@ -186,7 +194,7 @@ async function executeDelayedAction({ internalDate: emailMessage.internalDate, }; - logger.info("Executing delayed action", { + log.info("Executing delayed action", { actionType: executedAction.type, executedActionId: executedAction.id, messageId: email.id, @@ -200,9 +208,10 @@ async function executeDelayedAction({ userId: emailAccount.userId, emailAccountId: emailAccount.id, executedRule, + logger: log, }); - logger.info("Successfully executed delayed action", { + log.info("Successfully executed delayed action", { actionType: executedAction.type, executedActionId: executedAction.id, }); @@ -216,6 +225,7 @@ async function executeDelayedAction({ async function markActionCompleted( scheduledActionId: string, executedActionId: string | null | undefined, + log: Logger, reason?: string, ) { await prisma.scheduledAction.update({ @@ -227,7 +237,7 @@ async function markActionCompleted( }, }); - logger.info("Marked scheduled action as completed", { + log.info("Marked scheduled action as completed", { scheduledActionId, executedActionId, reason, @@ -237,7 +247,11 @@ async function markActionCompleted( /** * Mark scheduled action as failed */ -async function markActionFailed(scheduledActionId: string, error: unknown) { +async function markActionFailed( + scheduledActionId: string, + error: unknown, + log: Logger, +) { await prisma.scheduledAction.update({ where: { id: scheduledActionId }, data: { @@ -245,7 +259,7 @@ async function markActionFailed(scheduledActionId: string, error: unknown) { }, }); - logger.warn("Marked scheduled action as failed", { + log.warn("Marked scheduled action as failed", { scheduledActionId, error, }); @@ -255,7 +269,10 @@ async function markActionFailed(scheduledActionId: string, error: unknown) { * Check if all scheduled actions for an ExecutedRule are complete * and update the ExecutedRule status accordingly */ -async function checkAndCompleteExecutedRule(executedRuleId: string) { +async function checkAndCompleteExecutedRule( + executedRuleId: string, + log: Logger, +) { const pendingActions = await prisma.scheduledAction.count({ where: { executedRuleId, @@ -271,7 +288,7 @@ async function checkAndCompleteExecutedRule(executedRuleId: string) { data: { status: ExecutedRuleStatus.APPLIED }, }); - logger.info("Completed ExecutedRule - all scheduled actions finished", { + log.info("Completed ExecutedRule - all scheduled actions finished", { executedRuleId, }); } diff --git a/apps/web/utils/webhook/process-history-item.ts b/apps/web/utils/webhook/process-history-item.ts index 98b5266944..b8011f4c8c 100644 --- a/apps/web/utils/webhook/process-history-item.ts +++ b/apps/web/utils/webhook/process-history-item.ts @@ -192,6 +192,7 @@ export async function processHistoryItem( emailAccount, isTest: false, modelType: "default", + logger, }); } } catch (error: unknown) { diff --git a/version.txt b/version.txt index 5d9b5ee282..70603c2c17 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.18.24 +v2.18.25