From a35908c8d300b8db2650a9824265ff9eb65ef0f6 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 18 Apr 2025 01:08:26 +0300 Subject: [PATCH 1/3] disabled unused drafts --- README.md | 6 +- .../disable-unused-auto-draft/route.ts | 161 ++++++++++++++++++ apps/web/package.json | 1 + pnpm-lock.yaml | 13 ++ 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts diff --git a/README.md b/README.md index 6a93a4260a..472cc08d0f 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ To start watching emails visit: `/api/google/watch/all` ### Watching for email updates Set a cron job to run these: -The Google watch is necessary. The Resend one is optional. +The Google watch is necessary. Others are optional. ```json "crons": [ @@ -222,6 +222,10 @@ The Google watch is necessary. The Resend one is optional. { "path": "/api/resend/summary/all", "schedule": "0 16 * * 1" + }, + { + "path": "/api/reply-tracker/disable-unused-auto-draft", + "schedule": "0 3 * * *" } ] ``` diff --git a/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts b/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts new file mode 100644 index 0000000000..aeade638ab --- /dev/null +++ b/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts @@ -0,0 +1,161 @@ +import { NextResponse } from "next/server"; +import subDays from "date-fns/subDays"; +import { withError } from "@/utils/middleware"; +import prisma from "@/utils/prisma"; +import { ActionType, SystemType } from "@prisma/client"; +import { createScopedLogger } from "@/utils/logger"; +import { hasCronSecret, hasPostCronSecret } from "@/utils/cron"; +import { captureException } from "@/utils/error"; + +const logger = createScopedLogger("auto-draft/disable-unused"); + +// Force dynamic to ensure fresh data on each request +export const dynamic = "force-dynamic"; + +const MAX_DRAFTS_TO_CHECK = 10; + +/** + * Disables auto-draft feature for users who haven't used their last 10 drafts + * Only checks drafts that are more than a day old to give users time to use them + */ +async function disableUnusedAutoDrafts() { + logger.info("Starting to check for unused auto-drafts"); + + const oneDayAgo = subDays(new Date(), 1); + + // Find all users who have the auto-draft feature enabled (have an Action of type DRAFT_EMAIL) + const usersWithAutoDraft = await prisma.user.findMany({ + where: { + rules: { + some: { + systemType: SystemType.TO_REPLY, + actions: { + some: { + type: ActionType.DRAFT_EMAIL, + }, + }, + }, + }, + }, + select: { + id: true, + rules: { + where: { + systemType: SystemType.TO_REPLY, + }, + select: { + id: true, + }, + }, + }, + }); + + logger.info( + `Found ${usersWithAutoDraft.length} users with auto-draft enabled`, + ); + + const results = { + usersChecked: usersWithAutoDraft.length, + usersDisabled: 0, + errors: 0, + }; + + // Process each user + for (const user of usersWithAutoDraft) { + try { + // Find the last 10 drafts created for the user + const lastTenDrafts = await prisma.executedAction.findMany({ + where: { + executedRule: { + userId: user.id, + rule: { + systemType: SystemType.TO_REPLY, + }, + }, + type: ActionType.DRAFT_EMAIL, + draftId: { not: null }, + createdAt: { lt: oneDayAgo }, // Only check drafts older than a day + }, + orderBy: { + createdAt: "desc", + }, + take: MAX_DRAFTS_TO_CHECK, + select: { + id: true, + wasDraftSent: true, + draftSendLog: { + select: { + id: true, + }, + }, + }, + }); + + // Skip if user has fewer than 10 drafts (not enough data to make a decision) + if (lastTenDrafts.length < MAX_DRAFTS_TO_CHECK) { + logger.info("Skipping user - only has few drafts", { + userId: user.id, + numDrafts: lastTenDrafts.length, + }); + continue; + } + + // Check if any of the drafts were sent + const anyDraftsSent = lastTenDrafts.some( + (draft) => draft.wasDraftSent === true || draft.draftSendLog, + ); + + // If none of the drafts were sent, disable auto-draft + if (!anyDraftsSent) { + logger.info("Disabling auto-draft for user - last 10 drafts not used", { + userId: user.id, + }); + + // Delete the DRAFT_EMAIL actions from all TO_REPLY rules + await prisma.action.deleteMany({ + where: { + rule: { + userId: user.id, + systemType: SystemType.TO_REPLY, + }, + type: ActionType.DRAFT_EMAIL, + content: null, + }, + }); + + results.usersDisabled++; + } + } catch (error) { + logger.error("Error processing user", { userId: user.id, error }); + captureException(error); + results.errors++; + } + } + + logger.info("Completed auto-draft usage check", results); + return results; +} + +export const GET = withError(async (request) => { + if (!hasCronSecret(request)) { + captureException( + new Error("Unauthorized request: api/auto-draft/disable-unused"), + ); + return new Response("Unauthorized", { status: 401 }); + } + + const results = await disableUnusedAutoDrafts(); + return NextResponse.json(results); +}); + +export const POST = withError(async (request: Request) => { + if (!(await hasPostCronSecret(request))) { + captureException( + new Error("Unauthorized cron request: api/auto-draft/disable-unused"), + ); + return new Response("Unauthorized", { status: 401 }); + } + + const results = await disableUnusedAutoDrafts(); + return NextResponse.json(results); +}); diff --git a/apps/web/package.json b/apps/web/package.json index 569cbbc043..930e5b836f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -76,6 +76,7 @@ "@upstash/redis": "1.34.7", "@vercel/analytics": "1.5.0", "ai": "4.3.6", + "ai-fallback": "0.1.2", "braintrust": "0.0.197", "capital-case": "2.0.0", "cheerio": "1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ddd297179..cd8bc7a5a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -293,6 +293,9 @@ importers: ai: specifier: 4.3.6 version: 4.3.6(react@19.1.0)(zod@3.24.2) + ai-fallback: + specifier: 0.1.2 + version: 0.1.2(zod@3.24.2) braintrust: specifier: 0.0.197 version: 0.0.197(@aws-sdk/credential-provider-web-identity@3.750.0)(openai@4.93.0(encoding@0.1.13)(ws@8.18.0)(zod@3.24.2))(react@19.1.0)(sswr@2.2.0(svelte@4.2.12))(svelte@4.2.12)(vue@3.4.19(typescript@5.8.3))(zod@3.24.2) @@ -5980,6 +5983,9 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ai-fallback@0.1.2: + resolution: {integrity: sha512-O84i0fC4iMe9FTXu7anHq6K/FQA75JAWthf6iBgQmHug5wTuJilGApqqsWC4685Ul5hsWC2xEnLD82dGuxUvjw==} + ai@3.4.33: resolution: {integrity: sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==} engines: {node: '>=18'} @@ -18254,6 +18260,13 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ai-fallback@0.1.2(zod@3.24.2): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.7(zod@3.24.2) + transitivePeerDependencies: + - zod + ai@3.4.33(openai@4.93.0(encoding@0.1.13)(ws@8.18.0)(zod@3.24.2))(react@19.1.0)(sswr@2.2.0(svelte@4.2.12))(svelte@4.2.12)(vue@3.4.19(typescript@5.8.3))(zod@3.24.2): dependencies: '@ai-sdk/provider': 0.0.26 From 3ec6cb5eba7117f0e26bb2e9ea355fff52bfdc3b Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 18 Apr 2025 01:25:23 +0300 Subject: [PATCH 2/3] maxDuration. disable GET --- .../disable-unused-auto-draft/route.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts b/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts index aeade638ab..03f244b627 100644 --- a/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts +++ b/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts @@ -4,13 +4,14 @@ import { withError } from "@/utils/middleware"; import prisma from "@/utils/prisma"; import { ActionType, SystemType } from "@prisma/client"; import { createScopedLogger } from "@/utils/logger"; -import { hasCronSecret, hasPostCronSecret } from "@/utils/cron"; +import { hasPostCronSecret } from "@/utils/cron"; import { captureException } from "@/utils/error"; const logger = createScopedLogger("auto-draft/disable-unused"); // Force dynamic to ensure fresh data on each request export const dynamic = "force-dynamic"; +export const maxDuration = 300; const MAX_DRAFTS_TO_CHECK = 10; @@ -136,17 +137,18 @@ async function disableUnusedAutoDrafts() { return results; } -export const GET = withError(async (request) => { - if (!hasCronSecret(request)) { - captureException( - new Error("Unauthorized request: api/auto-draft/disable-unused"), - ); - return new Response("Unauthorized", { status: 401 }); - } - - const results = await disableUnusedAutoDrafts(); - return NextResponse.json(results); -}); +// For easier local testing +// export const GET = withError(async (request) => { +// if (!hasCronSecret(request)) { +// captureException( +// new Error("Unauthorized request: api/auto-draft/disable-unused"), +// ); +// return new Response("Unauthorized", { status: 401 }); +// } + +// const results = await disableUnusedAutoDrafts(); +// return NextResponse.json(results); +// }); export const POST = withError(async (request: Request) => { if (!(await hasPostCronSecret(request))) { From 2ae6ed49c7203531bb5dc61e8739fa965091ae79 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 18 Apr 2025 01:27:15 +0300 Subject: [PATCH 3/3] comment --- .../web/app/api/reply-tracker/disable-unused-auto-draft/route.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts b/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts index 03f244b627..e14aadc1dc 100644 --- a/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts +++ b/apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts @@ -24,6 +24,7 @@ async function disableUnusedAutoDrafts() { const oneDayAgo = subDays(new Date(), 1); + // TODO: may need to make this more efficient // Find all users who have the auto-draft feature enabled (have an Action of type DRAFT_EMAIL) const usersWithAutoDraft = await prisma.user.findMany({ where: {