feat: log subreddit screening outcomes

This commit is contained in:
jjbrunton committed 2026-08-08 22:39:15 +01:00
1 parent 53e9a44a5c
commit f88d4af146
2 files changed
+90 -5

No files matched your search

+66 -5
View File
@@ -4,6 +4,18 @@ import type { Config } from '../config/index.js';
import type { Logger } from '../utils/logger.js';
import { parseGenderTag } from '../utils/gender-tag.js';
interface ListingScreeningSummary {
scanned: number;
alreadyProcessed: number;
alreadyMessaged: number;
sellerSignals: number;
missingGenderTag: number;
genderNotTargeted: number;
eligible: number;
outsideRecentWindow: number;
deferred: number;
}
export class MonitorService {
// Flips on each call so that when both pools have candidates the bot
// alternates between image and chat instead of always favouring whichever
@@ -35,20 +47,35 @@ export class MonitorService {
try {
this.logger.debug(`Checking r/${subreddit.name} (${subreddit.type}) for new posts...`);
const posts = await this.redditApi.getNewPosts(subreddit.name, 25, subreddit.type);
const summary: ListingScreeningSummary = {
scanned: 0,
alreadyProcessed: 0,
alreadyMessaged: 0,
sellerSignals: 0,
missingGenderTag: 0,
genderNotTargeted: 0,
eligible: 0,
outsideRecentWindow: 0,
deferred: 0,
};
let selected: RedditPost | null = null;
// Reddit normally returns /new in descending order, but sort explicitly
// so we can stop as soon as the first fully eligible candidate is found.
// Previously we screened the entire batch before choosing one, causing up
// to two profile requests for every otherwise-valid post in the listing.
const newestFirst = [...posts].sort((a, b) => b.created_utc - a.created_utc);
for (const post of newestFirst) {
for (const [index, post] of newestFirst.entries()) {
summary.scanned += 1;
// Older adapters used in integrations may only expose account-local
// history; production repositories provide the shared lookup.
if (this.postsRepo.isProcessedAnywhere?.(post.id) ?? this.postsRepo.isProcessed(post.id)) {
summary.alreadyProcessed += 1;
continue;
}
if (this.recipientHistory.hasSentTo(post.author)) {
summary.alreadyMessaged += 1;
this.logger.debug(`Skipping post ${post.id} from u/${post.author} - already messaged this user`);
continue;
}
@@ -64,6 +91,7 @@ export class MonitorService {
post,
);
if (isSeller) {
summary.sellerSignals += 1;
this.logger.info(`Skipping post ${post.id} from u/${post.author} - seller signals detected`);
this.recordRejection(post, subreddit.type, 'Seller signals detected');
continue;
@@ -76,11 +104,13 @@ export class MonitorService {
if (subreddit.type === 'chat' && targetGenders.length > 0) {
const tag = parseGenderTag(`${post.title}\n${post.selftext ?? ''}`);
if (!tag) {
summary.missingGenderTag += 1;
this.logger.debug(`Skipping chat post ${post.id} - no gender tag in title/body`);
this.recordRejection(post, subreddit.type, 'No gender tag in title/body');
continue;
}
if (!targetGenders.includes(tag)) {
summary.genderNotTargeted += 1;
this.logger.debug(
`Skipping chat post ${post.id} - gender tag "${tag}" not in target list [${targetGenders.join(', ')}]`,
);
@@ -92,19 +122,29 @@ export class MonitorService {
// At this point the candidate has passed every check. Select it now
// rather than spending more Reddit requests screening users that
// cannot be acted on during this poll cycle.
summary.eligible += 1;
if (!prioritizeRecent || post.created_utc >= recentCutoffSec) {
this.logger.info(
`Found eligible post in r/${subreddit.name}${prioritizeRecent ? ' (recent match)' : ''}`,
);
return [post];
selected = post;
summary.deferred = newestFirst.length - index - 1;
break;
}
// This is the newest eligible post in this subreddit. Keep it as a
// possible fallback, then move to the next subreddit to look for a
// recent candidate. Older posts in this batch cannot improve it.
summary.outsideRecentWindow += 1;
if (!fallback || post.created_utc > fallback.created_utc) fallback = post;
summary.deferred = newestFirst.length - index - 1;
break;
}
this.logListingSummary(subreddit.name, posts.length, summary, selected !== null);
if (selected) {
this.logger.info(
`Found eligible post in r/${subreddit.name}${prioritizeRecent ? ' (recent match)' : ''}`,
);
return [selected];
}
} catch (error) {
if (error instanceof RedditRateLimitError) throw error;
@@ -175,6 +215,27 @@ export class MonitorService {
return new Promise((resolve) => setTimeout(resolve, ms));
}
private logListingSummary(
subreddit: string,
fetched: number,
summary: ListingScreeningSummary,
selected: boolean,
): void {
const counts = [
`${fetched} fetched`,
`${summary.scanned} screened`,
`${summary.alreadyProcessed} already processed`,
`${summary.alreadyMessaged} already messaged`,
`${summary.sellerSignals} seller signals`,
`${summary.missingGenderTag} missing gender tag`,
`${summary.genderNotTargeted} gender not targeted`,
`${summary.eligible} eligible${selected ? ' (selected)' : ''}`,
`${summary.outsideRecentWindow} outside recent window`,
];
if (summary.deferred > 0) counts.push(`${summary.deferred} not evaluated after outcome`);
this.logger.info(`r/${subreddit} listing: ${counts.join('; ')}`);
}
/** Persist a completed screening decision so later polls do not repeat it. */
private recordRejection(
post: RedditPost,