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
4 changes: 2 additions & 2 deletions apps/web/app/(landing)/home/HeroAB.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function HeroAB() {
<Hero
title={
<span
className={`transition-opacity duration-300 ease-in-out ${
className={`transition-opacity duration-300 ease-out ${
isHydrated && isFlagEnabled ? "opacity-100" : "opacity-0"
}`}
>
Expand All @@ -60,7 +60,7 @@ export function HeroAB() {
}
subtitle={
<span
className={`transition-opacity duration-300 ease-in-out ${
className={`transition-opacity duration-300 ease-out ${
isHydrated && isFlagEnabled ? "opacity-100" : "opacity-0"
}`}
>
Expand Down
7 changes: 6 additions & 1 deletion apps/web/utils/ai/choose-rule/draft-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ export async function handlePreviousDraftDeletion({
logger.info("No previous draft found for this thread to delete");
}
} catch (error) {
logger.error("Error finding or deleting previous draft", { error });
logger.error("Error finding or deleting previous draft", {
error:
(error as any)?.error instanceof Error
? (error as any)?.error?.message
: error,
});
// Log error but continue, failing to delete shouldn't block execution
}
}
Expand Down
13 changes: 8 additions & 5 deletions apps/web/utils/gmail/attachment.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type { gmail_v1 } from "@googleapis/gmail";
import { withGmailRetry } from "@/utils/gmail/retry";

export async function getGmailAttachment(
gmail: gmail_v1.Gmail,
messageId: string,
attachmentId: string,
) {
const attachment = await gmail.users.messages.attachments.get({
userId: "me",
id: attachmentId,
messageId,
});
const attachment = await withGmailRetry(() =>
gmail.users.messages.attachments.get({
userId: "me",
id: attachmentId,
messageId,
}),
);
const attachmentData = attachment.data;
return attachmentData;
}
29 changes: 18 additions & 11 deletions apps/web/utils/gmail/filter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { gmail_v1 } from "@googleapis/gmail";
import { GaxiosError } from "gaxios";
import { GmailLabel } from "@/utils/gmail/label";
import { withGmailRetry } from "@/utils/gmail/retry";

export async function createFilter(options: {
gmail: gmail_v1.Gmail;
Expand All @@ -11,16 +12,18 @@ export async function createFilter(options: {
const { gmail, from, addLabelIds, removeLabelIds } = options;

try {
return await gmail.users.settings.filters.create({
userId: "me",
requestBody: {
criteria: { from },
action: {
addLabelIds,
removeLabelIds,
return await withGmailRetry(() =>
gmail.users.settings.filters.create({
userId: "me",
requestBody: {
criteria: { from },
action: {
addLabelIds,
removeLabelIds,
},
},
},
});
}),
);
} catch (error) {
if (isFilterExistsError(error)) return { status: 200 };
throw error;
Expand Down Expand Up @@ -55,11 +58,15 @@ export async function deleteFilter(options: {
}) {
const { gmail, id } = options;

return gmail.users.settings.filters.delete({ userId: "me", id });
return withGmailRetry(() =>
gmail.users.settings.filters.delete({ userId: "me", id }),
);
}

export async function getFiltersList(options: { gmail: gmail_v1.Gmail }) {
return options.gmail.users.settings.filters.list({ userId: "me" });
return withGmailRetry(() =>
options.gmail.users.settings.filters.list({ userId: "me" }),
);
}

function isFilterExistsError(error: unknown): error is GaxiosError {
Expand Down
15 changes: 9 additions & 6 deletions apps/web/utils/gmail/history.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { gmail_v1 } from "@googleapis/gmail";
import { withGmailRetry } from "@/utils/gmail/retry";

export async function getHistory(
gmail: gmail_v1.Gmail,
Expand All @@ -8,12 +9,14 @@ export async function getHistory(
maxResults?: number;
},
) {
const history = await gmail.users.history.list({
userId: "me",
startHistoryId: options.startHistoryId,
historyTypes: options.historyTypes,
maxResults: options.maxResults,
});
const history = await withGmailRetry(() =>
gmail.users.history.list({
userId: "me",
startHistoryId: options.startHistoryId,
historyTypes: options.historyTypes,
maxResults: options.maxResults,
}),
);

return history.data;
}
32 changes: 19 additions & 13 deletions apps/web/utils/gmail/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,18 +223,20 @@ export async function createLabel({
await ensureParentLabelsExist(gmail, name);

try {
const createdLabel = await gmail.users.labels.create({
userId: "me",
requestBody: {
name,
messageListVisibility,
labelListVisibility,
color: {
backgroundColor: color || getLabelColor(name),
textColor: "#000000",
const createdLabel = await withGmailRetry(() =>
gmail.users.labels.create({
userId: "me",
requestBody: {
name,
messageListVisibility,
labelListVisibility,
color: {
backgroundColor: color || getLabelColor(name),
textColor: "#000000",
},
},
},
});
}),
);
return createdLabel.data;
} catch (error) {
const errorMessage: string | undefined = (error as any).message;
Expand Down Expand Up @@ -273,7 +275,9 @@ async function ensureParentLabelsExist(gmail: gmail_v1.Gmail, name: string) {
}

export async function getLabels(gmail: gmail_v1.Gmail) {
const response = await gmail.users.labels.list({ userId: "me" });
const response = await withGmailRetry(() =>
gmail.users.labels.list({ userId: "me" }),
);
return response.data.labels;
}

Expand Down Expand Up @@ -305,7 +309,9 @@ export async function getLabelById(options: {
id: string;
}) {
const { gmail, id } = options;
return (await gmail.users.labels.get({ userId: "me", id })).data;
return (
await withGmailRetry(() => gmail.users.labels.get({ userId: "me", id }))
).data;
}

export async function getOrCreateLabel({
Expand Down
122 changes: 61 additions & 61 deletions apps/web/utils/gmail/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,67 @@ interface ErrorInfo {
errorMessage: string;
}

/**
* Retries a Gmail API operation when rate limits or temporary server errors are encountered
* - Rate limits: 429, 403 with specific reasons
* - Server errors: 502, 503, 504
*/
export async function withGmailRetry<T>(
operation: () => Promise<T>,
maxRetries = 5,
): Promise<T> {
return pRetry(operation, {
retries: maxRetries,
onFailedAttempt: async (error) => {
const errorInfo = extractErrorInfo(error);
const { retryable, isRateLimit, isServerError, isFailedPrecondition } =
isRetryableError(errorInfo);

if (!retryable) {
logger.warn("Non-retryable error encountered", {
error,
status: errorInfo.status,
reason: errorInfo.reason,
});
throw error;
}

const err = error as Record<string, unknown>;
const cause = (err?.cause ?? err) as Record<string, unknown>;
const retryAfterHeader = (
(cause?.response as Record<string, unknown>)?.headers as Record<
string,
string
>
)?.["retry-after"];

const delayMs = calculateRetryDelay(
isRateLimit,
isServerError,
isFailedPrecondition,
error.attemptNumber,
retryAfterHeader,
errorInfo.errorMessage,
);

logger.warn("Gmail error. Will retry", {
delaySeconds: Math.ceil(delayMs / 1000),
attemptNumber: error.attemptNumber,
maxRetries,
status: errorInfo.status,
isRateLimit,
isServerError,
isFailedPrecondition,
});

// Apply the custom delay
if (delayMs > 0) {
await sleep(delayMs);
}
},
});
}

/**
* Extracts error information from various error shapes
*/
Expand Down Expand Up @@ -162,67 +223,6 @@ export function calculateRetryDelay(
return 0;
}

/**
* Retries a Gmail API operation when rate limits or temporary server errors are encountered
* - Rate limits: 429, 403 with specific reasons
* - Server errors: 502, 503, 504
*/
export async function withGmailRetry<T>(
operation: () => Promise<T>,
maxRetries = 5,
): Promise<T> {
return pRetry(operation, {
retries: maxRetries,
onFailedAttempt: async (error) => {
const errorInfo = extractErrorInfo(error);
const { retryable, isRateLimit, isServerError, isFailedPrecondition } =
isRetryableError(errorInfo);

if (!retryable) {
logger.warn("Non-retryable error encountered", {
error,
status: errorInfo.status,
reason: errorInfo.reason,
});
throw error;
}

const err = error as Record<string, unknown>;
const cause = (err?.cause ?? err) as Record<string, unknown>;
const retryAfterHeader = (
(cause?.response as Record<string, unknown>)?.headers as Record<
string,
string
>
)?.["retry-after"];

const delayMs = calculateRetryDelay(
isRateLimit,
isServerError,
isFailedPrecondition,
error.attemptNumber,
retryAfterHeader,
errorInfo.errorMessage,
);

logger.warn("Gmail error. Will retry", {
delaySeconds: Math.ceil(delayMs / 1000),
attemptNumber: error.attemptNumber,
maxRetries,
status: errorInfo.status,
isRateLimit,
isServerError,
isFailedPrecondition,
});

// Apply the custom delay
if (delayMs > 0) {
await sleep(delayMs);
}
},
});
}

/**
* Parses the retry time from Gmail rate limit error messages
* Example: "User-rate limit exceeded. Retry after 2025-08-22T18:22:38.763Z"
Expand Down
13 changes: 9 additions & 4 deletions apps/web/utils/gmail/settings.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { gmail_v1 } from "@googleapis/gmail";
import { withGmailRetry } from "@/utils/gmail/retry";

export async function getFilters(gmail: gmail_v1.Gmail) {
const res = await gmail.users.settings.filters.list({ userId: "me" });
const res = await withGmailRetry(() =>
gmail.users.settings.filters.list({ userId: "me" }),
);
return res.data.filter || [];
}

export async function getForwardingAddresses(gmail: gmail_v1.Gmail) {
const res = await gmail.users.settings.forwardingAddresses.list({
userId: "me",
});
const res = await withGmailRetry(() =>
gmail.users.settings.forwardingAddresses.list({
userId: "me",
}),
);
return res.data.forwardingAddresses || [];
}
9 changes: 6 additions & 3 deletions apps/web/utils/gmail/signature-settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { gmail_v1 } from "@googleapis/gmail";
import { createScopedLogger } from "@/utils/logger";
import { withGmailRetry } from "@/utils/gmail/retry";

const logger = createScopedLogger("gmail-signature");

Expand All @@ -18,9 +19,11 @@ export async function getGmailSignatures(
gmail: gmail_v1.Gmail,
): Promise<GmailSignature[]> {
try {
const sendAsList = await gmail.users.settings.sendAs.list({
userId: "me",
});
const sendAsList = await withGmailRetry(() =>
gmail.users.settings.sendAs.list({
userId: "me",
}),
);

if (!sendAsList.data.sendAs || sendAsList.data.sendAs.length === 0) {
logger.warn("No sendAs settings found");
Expand Down
Loading
Loading