From 4fc74be51d4b9693467cd37b77d81e3b04dff380 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 24 Nov 2025 06:16:16 -0500 Subject: [PATCH 1/5] adjust logs --- apps/web/utils/ai/actions.ts | 1 + apps/web/utils/ai/choose-rule/run-rules.ts | 1 + .../assistant/process-assistant-email.ts | 1 + apps/web/utils/label.server.ts | 12 +++------ apps/web/utils/reply-tracker/label-helpers.ts | 25 ++++++------------- apps/web/utils/reply-tracker/outbound.ts | 1 + version.txt | 2 +- 7 files changed, 16 insertions(+), 27 deletions(-) diff --git a/apps/web/utils/ai/actions.ts b/apps/web/utils/ai/actions.ts index f34442bf5c..9ac3057229 100644 --- a/apps/web/utils/ai/actions.ts +++ b/apps/web/utils/ai/actions.ts @@ -123,6 +123,7 @@ const label: ActionFunction<{ labelId: labelIdToUse, labelName: args.label || null, emailAccountId, + logger, }); }; diff --git a/apps/web/utils/ai/choose-rule/run-rules.ts b/apps/web/utils/ai/choose-rule/run-rules.ts index c7a6ffabd9..883bc62a85 100644 --- a/apps/web/utils/ai/choose-rule/run-rules.ts +++ b/apps/web/utils/ai/choose-rule/run-rules.ts @@ -328,6 +328,7 @@ async function executeMatchedRule( threadId: message.threadId, systemType: rule.systemType, provider: client, + logger, }), updateThreadTrackers({ emailAccountId: emailAccount.id, diff --git a/apps/web/utils/assistant/process-assistant-email.ts b/apps/web/utils/assistant/process-assistant-email.ts index febb722337..0eb9e8703c 100644 --- a/apps/web/utils/assistant/process-assistant-email.ts +++ b/apps/web/utils/assistant/process-assistant-email.ts @@ -270,6 +270,7 @@ async function withProcessingLabels( labelId: labels[0].id, labelName: labels[0].name, emailAccountId, + logger, }).catch((error) => { logger.error("Error labeling message", { error }); }); diff --git a/apps/web/utils/label.server.ts b/apps/web/utils/label.server.ts index 908f1500ba..4d8f308be9 100644 --- a/apps/web/utils/label.server.ts +++ b/apps/web/utils/label.server.ts @@ -1,5 +1,5 @@ import type { EmailProvider } from "@/utils/email/types"; -import { createScopedLogger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import prisma from "@/utils/prisma"; /** @@ -16,20 +16,16 @@ export async function labelMessageAndSync({ labelId, labelName, emailAccountId, + logger: log, }: { provider: EmailProvider; messageId: string; labelId: string; labelName: string | null; emailAccountId: string; + logger: Logger; }): Promise { - const logger = createScopedLogger("label.server").with({ - provider: provider.name, - messageId, - labelId, - labelName, - emailAccountId, - }); + const logger = log.with({ labelId, labelName }); const result = await provider.labelMessage({ messageId, diff --git a/apps/web/utils/reply-tracker/label-helpers.ts b/apps/web/utils/reply-tracker/label-helpers.ts index 7c6c9813b9..7151e0b5fe 100644 --- a/apps/web/utils/reply-tracker/label-helpers.ts +++ b/apps/web/utils/reply-tracker/label-helpers.ts @@ -1,5 +1,5 @@ import type { EmailProvider, EmailLabel } from "@/utils/email/types"; -import { createScopedLogger } from "@/utils/logger"; +import { createScopedLogger, type Logger } from "@/utils/logger"; import prisma from "@/utils/prisma"; import { ActionType } from "@/generated/prisma/enums"; import { @@ -24,6 +24,7 @@ export async function removeConflictingThreadStatusLabels({ provider, dbLabels: providedDbLabels, providerLabels: providedProviderLabels, + logger, }: { emailAccountId: string; threadId: string; @@ -31,16 +32,8 @@ export async function removeConflictingThreadStatusLabels({ provider: EmailProvider; dbLabels?: LabelIds; providerLabels?: EmailLabel[]; + logger: Logger; }): Promise { - const logger = createScopedLogger("removeConflictingThreadStatusLabels").with( - { - emailAccountId, - threadId, - systemType, - provider: provider.name, - }, - ); - const [dbLabels, providerLabels] = await Promise.all([ providedDbLabels ?? getLabelsFromDb(emailAccountId), providedProviderLabels ?? provider.getLabels(), @@ -98,21 +91,15 @@ export async function applyThreadStatusLabel({ messageId, systemType, provider, + logger, }: { emailAccountId: string; threadId: string; messageId: string; systemType: ConversationStatus; provider: EmailProvider; + logger: Logger; }): Promise { - const logger = createScopedLogger("applyThreadStatusLabel").with({ - emailAccountId, - threadId, - messageId, - systemType, - provider: provider.name, - }); - const [dbLabels, providerLabels] = await Promise.all([ getLabelsFromDb(emailAccountId), provider.getLabels(), @@ -149,6 +136,7 @@ export async function applyThreadStatusLabel({ labelId: targetLabel.labelId, labelName: targetLabel.label, emailAccountId, + logger, }).catch((error) => logger.error("Failed to apply thread status label", { labelId: targetLabel.labelId, @@ -166,6 +154,7 @@ export async function applyThreadStatusLabel({ provider, dbLabels, providerLabels, + logger, }), addLabel(), ]); diff --git a/apps/web/utils/reply-tracker/outbound.ts b/apps/web/utils/reply-tracker/outbound.ts index 2f3ad6c835..47ee298b5d 100644 --- a/apps/web/utils/reply-tracker/outbound.ts +++ b/apps/web/utils/reply-tracker/outbound.ts @@ -82,6 +82,7 @@ export async function handleOutboundReply({ messageId: message.id, systemType: aiResult.status, provider, + logger, }), updateThreadTrackers({ emailAccountId: emailAccount.id, diff --git a/version.txt b/version.txt index 4dd24265f1..5f4826611e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.20.19 +v2.20.20 From 06bc16481e2096d85149adac63cf156961800336 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:16:35 -0500 Subject: [PATCH 2/5] fix import --- apps/web/utils/reply-tracker/label-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/utils/reply-tracker/label-helpers.ts b/apps/web/utils/reply-tracker/label-helpers.ts index 7151e0b5fe..ff4b44169a 100644 --- a/apps/web/utils/reply-tracker/label-helpers.ts +++ b/apps/web/utils/reply-tracker/label-helpers.ts @@ -1,5 +1,5 @@ import type { EmailProvider, EmailLabel } from "@/utils/email/types"; -import { createScopedLogger, type Logger } from "@/utils/logger"; +import type { Logger } from "@/utils/logger"; import prisma from "@/utils/prisma"; import { ActionType } from "@/generated/prisma/enums"; import { From b475cf30fec688244a8712487fa2cf66ac0744c5 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:52:52 -0500 Subject: [PATCH 3/5] dont error on draft missing --- apps/web/utils/gmail/draft.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/web/utils/gmail/draft.ts b/apps/web/utils/gmail/draft.ts index f074fa0585..93c584903e 100644 --- a/apps/web/utils/gmail/draft.ts +++ b/apps/web/utils/gmail/draft.ts @@ -19,11 +19,33 @@ export async function getDraft(draftId: string, gmail: gmail_v1.Gmail) { const message = parseMessage(response.data.message as MessageWithPayload); return message; } catch (error) { - if (isGmailError(error) && error.code === 404) return null; + if (isNotFoundError(error)) { + logger.info("Draft not found, returning null.", { draftId }); + return null; + } throw error; } } +function isNotFoundError(error: unknown): boolean { + if (isGmailError(error) && error.code === 404) return true; + + // biome-ignore lint/suspicious/noExplicitAny: simple + const err = error as any; + + const statusCode = + err.response?.data?.error?.code ?? + err.response?.status ?? + err.status ?? + err.code ?? + err.error?.response?.data?.error?.code ?? + err.error?.response?.status ?? + err.error?.status ?? + err.error?.code; + + return statusCode === 404; +} + export async function deleteDraft(gmail: gmail_v1.Gmail, draftId: string) { try { logger.info("Deleting draft", { draftId }); From 89772f7f457d033453bf10fa3d95ba8c4f4fa2d4 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:56:18 -0500 Subject: [PATCH 4/5] slimmer error logs --- apps/web/utils/logger.ts | 56 ++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/apps/web/utils/logger.ts b/apps/web/utils/logger.ts index 25f10158aa..5aa812c29f 100644 --- a/apps/web/utils/logger.ts +++ b/apps/web/utils/logger.ts @@ -126,13 +126,7 @@ function formatError(args?: Record) { if (!args?.error) return args; const error = args.error; - const errorMessage = - error instanceof Error - ? error.message - : typeof error === "object" && error !== null && "message" in error - ? (error as { message: unknown }).message - : error; - + const errorMessage = getSimpleErrorMessage(error) ?? "Unknown error"; const errorFull = serializeError(error); return { @@ -151,10 +145,11 @@ function serializeError(error: unknown): unknown { stack: error.stack, }; - // Copy all enumerable properties - for (const key in error) { - if (Object.hasOwn(error, key)) { - serialized[key] = (error as any)[key]; + if (isRecord(error)) { + for (const key of Object.keys(error)) { + if (Object.hasOwn(error, key)) { + serialized[key] = error[key]; + } } } @@ -184,6 +179,45 @@ function processErrorsInObject(obj: unknown): unknown { return obj; } +function getSimpleErrorMessage(error: unknown): string | undefined { + if (typeof error === "string") { + return error; + } + + if (error instanceof Error) { + return error.message; + } + + if (!hasMessageField(error) && !hasNestedErrorField(error)) { + return undefined; + } + + if (hasMessageField(error) && typeof error.message === "string") { + return error.message; + } + + if (hasNestedErrorField(error)) { + const nested = error.error; + if (hasMessageField(nested) && typeof nested.message === "string") { + return nested.message; + } + } + + return undefined; +} + +function hasMessageField(value: unknown): value is { message?: unknown } { + return typeof value === "object" && value !== null && "message" in value; +} + +function hasNestedErrorField(value: unknown): value is { error: unknown } { + return typeof value === "object" && value !== null && "error" in value; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + // Field names that contain PII and should be hashed in production const SENSITIVE_FIELD_NAMES = new Set(["from", "sender", "to"]); From 8bbdd2c9e852a49efe2d1a121e8b3e3bcdb87825 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:26:18 -0500 Subject: [PATCH 5/5] fix tests --- apps/web/utils/reply-tracker/label-helpers.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/web/utils/reply-tracker/label-helpers.test.ts b/apps/web/utils/reply-tracker/label-helpers.test.ts index b8ca73246c..d276528a1c 100644 --- a/apps/web/utils/reply-tracker/label-helpers.test.ts +++ b/apps/web/utils/reply-tracker/label-helpers.test.ts @@ -2,6 +2,9 @@ import { describe, expect, test, vi, beforeEach } from "vitest"; import { applyThreadStatusLabel } from "./label-helpers"; import type { EmailProvider } from "@/utils/email/types"; import prisma from "@/utils/__mocks__/prisma"; +import { createScopedLogger } from "@/utils/logger"; + +const logger = createScopedLogger("test"); vi.mock("server-only", () => ({})); vi.mock("@/utils/prisma"); @@ -66,6 +69,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "TO_REPLY", provider: mockProvider, + logger, }); // Should remove other conversation status labels from thread @@ -98,6 +102,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "AWAITING_REPLY", provider: mockProvider, + logger, }); expect(mockProvider.removeThreadLabels).toHaveBeenCalledWith( @@ -118,6 +123,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "FYI", provider: mockProvider, + logger, }); expect(mockProvider.removeThreadLabels).toHaveBeenCalledWith( @@ -142,6 +148,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "ACTIONED", provider: mockProvider, + logger, }); expect(mockProvider.removeThreadLabels).toHaveBeenCalledWith( @@ -172,6 +179,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "TO_REPLY", provider: mockProvider, + logger, }), ).resolves.not.toThrow(); }); @@ -205,6 +213,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "TO_REPLY", provider: mockProvider, + logger, }); // Should still include FYI label from provider labels @@ -267,6 +276,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "TO_REPLY", provider: mockProvider, + logger, }); // Should have created the label @@ -296,6 +306,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "TO_REPLY", provider: mockProvider, + logger, }); // Should NOT call removeThreadLabels since there are no conflicting labels @@ -320,6 +331,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "FYI", provider: mockProvider, + logger, }); // Both operations should have been called @@ -334,6 +346,7 @@ describe("applyThreadStatusLabel", () => { messageId, systemType: "FYI", provider: mockProvider, + logger, }); const removeCall = vi.mocked(mockProvider.removeThreadLabels).mock.calls[0];