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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/utils/ai/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const label: ActionFunction<{
labelId: labelIdToUse,
labelName: args.label || null,
emailAccountId,
logger,
});
};

Expand Down
1 change: 1 addition & 0 deletions apps/web/utils/ai/choose-rule/run-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ async function executeMatchedRule(
threadId: message.threadId,
systemType: rule.systemType,
provider: client,
logger,
}),
updateThreadTrackers({
emailAccountId: emailAccount.id,
Expand Down
1 change: 1 addition & 0 deletions apps/web/utils/assistant/process-assistant-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ async function withProcessingLabels<T>(
labelId: labels[0].id,
labelName: labels[0].name,
emailAccountId,
logger,
}).catch((error) => {
logger.error("Error labeling message", { error });
});
Expand Down
24 changes: 23 additions & 1 deletion apps/web/utils/gmail/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

statusCode can be a string, so strict === 404 may miss not-found cases. Consider normalizing the value to a number before comparing so getDraft returns null consistently.

-  return statusCode === 404;
+  const code = typeof statusCode === "string" ? Number(statusCode) : statusCode;
+  return code === 404;

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

}

export async function deleteDraft(gmail: gmail_v1.Gmail, draftId: string) {
try {
logger.info("Deleting draft", { draftId });
Expand Down
12 changes: 4 additions & 8 deletions apps/web/utils/label.server.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -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<void> {
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,
Expand Down
56 changes: 45 additions & 11 deletions apps/web/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,7 @@ function formatError(args?: Record<string, unknown>) {
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 {
Expand All @@ -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];
}
}
}

Expand Down Expand Up @@ -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<string, unknown> {
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"]);

Expand Down
13 changes: 13 additions & 0 deletions apps/web/utils/reply-tracker/label-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -66,6 +69,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "TO_REPLY",
provider: mockProvider,
logger,
});

// Should remove other conversation status labels from thread
Expand Down Expand Up @@ -98,6 +102,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "AWAITING_REPLY",
provider: mockProvider,
logger,
});

expect(mockProvider.removeThreadLabels).toHaveBeenCalledWith(
Expand All @@ -118,6 +123,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "FYI",
provider: mockProvider,
logger,
});

expect(mockProvider.removeThreadLabels).toHaveBeenCalledWith(
Expand All @@ -142,6 +148,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "ACTIONED",
provider: mockProvider,
logger,
});

expect(mockProvider.removeThreadLabels).toHaveBeenCalledWith(
Expand Down Expand Up @@ -172,6 +179,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "TO_REPLY",
provider: mockProvider,
logger,
}),
).resolves.not.toThrow();
});
Expand Down Expand Up @@ -205,6 +213,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "TO_REPLY",
provider: mockProvider,
logger,
});

// Should still include FYI label from provider labels
Expand Down Expand Up @@ -267,6 +276,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "TO_REPLY",
provider: mockProvider,
logger,
});

// Should have created the label
Expand Down Expand Up @@ -296,6 +306,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "TO_REPLY",
provider: mockProvider,
logger,
});

// Should NOT call removeThreadLabels since there are no conflicting labels
Expand All @@ -320,6 +331,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "FYI",
provider: mockProvider,
logger,
});

// Both operations should have been called
Expand All @@ -334,6 +346,7 @@ describe("applyThreadStatusLabel", () => {
messageId,
systemType: "FYI",
provider: mockProvider,
logger,
});

const removeCall = vi.mocked(mockProvider.removeThreadLabels).mock.calls[0];
Expand Down
25 changes: 7 additions & 18 deletions apps/web/utils/reply-tracker/label-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { EmailProvider, EmailLabel } from "@/utils/email/types";
import { createScopedLogger } from "@/utils/logger";
import type { Logger } from "@/utils/logger";
import prisma from "@/utils/prisma";
import { ActionType } from "@/generated/prisma/enums";
import {
Expand All @@ -24,23 +24,16 @@ export async function removeConflictingThreadStatusLabels({
provider,
dbLabels: providedDbLabels,
providerLabels: providedProviderLabels,
logger,
}: {
emailAccountId: string;
threadId: string;
systemType: ConversationStatus;
provider: EmailProvider;
dbLabels?: LabelIds;
providerLabels?: EmailLabel[];
logger: Logger;
}): Promise<void> {
const logger = createScopedLogger("removeConflictingThreadStatusLabels").with(
{
emailAccountId,
threadId,
systemType,
provider: provider.name,
},
);

const [dbLabels, providerLabels] = await Promise.all([
providedDbLabels ?? getLabelsFromDb(emailAccountId),
providedProviderLabels ?? provider.getLabels(),
Expand Down Expand Up @@ -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<void> {
const logger = createScopedLogger("applyThreadStatusLabel").with({
emailAccountId,
threadId,
messageId,
systemType,
provider: provider.name,
});

const [dbLabels, providerLabels] = await Promise.all([
getLabelsFromDb(emailAccountId),
provider.getLabels(),
Expand Down Expand Up @@ -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,
Expand All @@ -166,6 +154,7 @@ export async function applyThreadStatusLabel({
provider,
dbLabels,
providerLabels,
logger,
}),
addLabel(),
]);
Expand Down
1 change: 1 addition & 0 deletions apps/web/utils/reply-tracker/outbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export async function handleOutboundReply({
messageId: message.id,
systemType: aiResult.status,
provider,
logger,
}),
updateThreadTrackers({
emailAccountId: emailAccount.id,
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.20.19
v2.20.20
Loading