Skip to content

Let the polling sweep see the ads a post was boosted into - #41

Merged
diwenne merged 3 commits into
diwenne:mainfrom
emanueletech:fix/ad-comments-in-polling
Aug 30, 2026
Merged

Let the polling sweep see the ads a post was boosted into#41
diwenne merged 3 commits into
diwenne:mainfrom
emanueletech:fix/ad-comments-in-polling

Conversation

@emanueletech

Copy link
Copy Markdown
Contributor

The bug

Boost a post and some of its comments stop being answered — silently, and only some of them, which is what makes it hard to notice.

The reconciler exists because Instagram webhooks are best-effort. But it only ever looks at the campaign's own post, and a comment left on an ad carries the ad's media id, not the post's. So when Meta drops a webhook for a comment on an ad, nothing catches it: not the webhook path, not the safety net.

Measured on one boosted post, comparing the comments the Graph API reports against what actually arrived:

Comment Webhook delivered Answered
Link 🎉🎉🎉 no no
Link no no
Link yes yes
Link yes yes
Link? yes yes
Link yes yes

Three of six keyword comments were lost. Two were recent enough to re-enqueue by hand; the third was past the 7-day private-reply window and is gone. Boosting is also when comment volume is highest, so the safety net was missing exactly when it was needed most.

This is the same shape as #25 (which fixed the matching of ad comments) — the missing half is the polling path.

The fix

adMediaFor(postId) returns the ad copies of a post, and the sweep checks those media alongside the post itself.

The ad ids come from the webhooks already stored, not from the ads API — reading the ads API would require ads_management on top of the permissions the app asks for today, which seemed a heavy price for a safety net. The trade-off is explicit in the code: an ad becomes visible to the sweep only after one comment on it has arrived. That is enough for this failure mode, where some webhooks arrive and others do not.

Failure is contained: a lookup error returns an empty list and the sweep still checks the post exactly as before. A campaign on a post that was never boosted issues one extra query and gets nothing back.

Verification

npm run typecheck, npm run lint, npm test, npm run build all pass — 168 tests, 6 of them new.

The query was also run against a live database, where it correctly returned the ad media id for the boosted post. The five unit tests cover: the ad is returned, the post itself never is (it would be swept twice), rows without an id are dropped, an unboosted post yields nothing, and a failed query returns empty.

Meta does not deliver every comment webhook, which is the whole reason the
sweep exists — but the sweep only ever looked at the campaign's own post.
Comments left on an ad carry the ad's own media id, so they fell outside
it and were lost for good.

This is not theoretical. On one boosted post, three of six keyword
comments never produced a webhook; two of them were from this morning and
had to be re-enqueued by hand. Boosting is also when comment volume is
highest, so the safety net was missing precisely when it mattered.

The ad ids come from the webhooks already received rather than from the
ads API, which would need ads_management on top of the permissions the app
asks for today. The trade-off is that an ad becomes visible to the sweep
only after one comment on it has arrived — enough for this failure, where
some webhooks arrive and others do not. A failed lookup returns nothing
and leaves the sweep to check the post as before.
Five cases: the ad is returned, the post itself never is (it would be
swept twice), rows without an id are dropped, an unboosted post yields
nothing, and a failed query returns empty so the sweep still checks the
post.
@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

@emanueletech is attempting to deploy a commit to the diwenne's projects Team on Vercel.

A member of the Team first needs to authorize it.

The sweep learned to look at ad media this morning, but enqueued those
comments with only the ad's media id. The worker then looked for a
campaign bound to that id, found none, and dropped the comment — so the
sweep re-enqueued the same comment every five minutes and never delivered
anything. Four sweeps in a row reported "1 enqueued" while DmLog stayed
empty.

Found by testing on the live account rather than by reading the code: a
comment posted from another account was picked up, enqueued and silently
discarded, four times.
@emanueletech

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up: the first version enqueued ad comments with only the ad's media id, so the worker looked for a campaign bound to that id, found none, and dropped the comment. The sweep then re-enqueued the same comment every five minutes and delivered nothing — four sweeps in a row logging 1 enqueued while DmLog stayed empty.

originalMediaId now travels with the job when the sweep is looking at an ad, which is the field processComment already matches on (added in #25).

Worth saying how it surfaced, since it says something about the change: the unit tests passed, the SQL was verified against a live database, and the lookup returned the right ad id — the defect was one layer further on, in what the sweep did with that id. It only showed up by posting a real comment from another account and watching it go nowhere. If you would rather see a test that covers the enqueue payload rather than just the lookup, say so and I will add it.

@diwenne diwenne left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Verified locally: typecheck 0, lint 0, 168 tests (5 new adMediaFor cases) all pass. adMediaFor correctly sources ad IDs from WebhookEvent original_media_id (90d window), filters post itself + nulls, fail-open. sweepCampaign now sweeps ad media alongside post, and enqueues with originalMediaId so worker OR-match (mediaId/originalMediaId) from #25 routes ad comments to post-bound campaign. Fixes silent loss on boosted posts where webhooks are best-effort - second follow-up (fd1e17b) correctly addresses re-enqueue-every-5m worker drop.

@diwenne
diwenne merged commit d97e293 into diwenne:main Aug 30, 2026
1 of 2 checks passed
@emanueletech
emanueletech deleted the fix/ad-comments-in-polling branch August 31, 2026 07:09
kokerax pushed a commit to kokerax/openreply that referenced this pull request Sep 2, 2026
* Let the polling sweep see the ads a post was boosted into

Meta does not deliver every comment webhook, which is the whole reason the
sweep exists — but the sweep only ever looked at the campaign's own post.
Comments left on an ad carry the ad's own media id, so they fell outside
it and were lost for good.

This is not theoretical. On one boosted post, three of six keyword
comments never produced a webhook; two of them were from this morning and
had to be re-enqueued by hand. Boosting is also when comment volume is
highest, so the safety net was missing precisely when it mattered.

The ad ids come from the webhooks already received rather than from the
ads API, which would need ads_management on top of the permissions the app
asks for today. The trade-off is that an ad becomes visible to the sweep
only after one comment on it has arrived — enough for this failure, where
some webhooks arrive and others do not. A failed lookup returns nothing
and leaves the sweep to check the post as before.

* Cover the ad-media lookup with tests

Five cases: the ad is returned, the post itself never is (it would be
swept twice), rows without an id are dropped, an unboosted post yields
nothing, and a failed query returns empty so the sweep still checks the
post.

* Tell the worker which post an ad comment belongs to

The sweep learned to look at ad media this morning, but enqueued those
comments with only the ad's media id. The worker then looked for a
campaign bound to that id, found none, and dropped the comment — so the
sweep re-enqueued the same comment every five minutes and never delivered
anything. Four sweeps in a row reported "1 enqueued" while DmLog stayed
empty.

Found by testing on the live account rather than by reading the code: a
comment posted from another account was picked up, enqueued and silently
discarded, four times.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants