Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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 * * *"
}
]
```
Expand Down
163 changes: 163 additions & 0 deletions apps/web/app/api/reply-tracker/disable-unused-auto-draft/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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 { 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;

/**
* 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: {
Comment on lines +65 to +85

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.

🛠️ Refactor suggestion

N + 1 queries – fetch drafts in batch instead of per‑user loop

The loop issues one executedAction.findMany per user. If thousands of users have auto‑draft enabled the route will:

  • hammer the DB with many small queries,
  • hold the Lambda/function warm longer and increase cold‑start cost,
  • risk timing out for the 10 s default Vercel edge timeout (if not using background).

You can reduce round‑trips with a single query that groups by userId and aggregates the last 10 drafts per user, e.g.:

const draftsByUser = await prisma.executedAction.groupBy({
  by: ["executedRule.userId"],
  where: { /* same filters */ },
  _count: { id: true },
  _max: { createdAt: true },
  take: MAX_DRAFTS_TO_CHECK,
});

Then process the in‑memory groups.
This will scale linearly with returned rows instead of users.

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,
},
});
Comment thread
elie222 marked this conversation as resolved.

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;
}

// 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))) {
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);
});
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.