Conversation
Three defects in the animated banners, all visible in the first real renders. Size. Every unit came out two pixels short in each dimension — 298x248 for a 300x250 — because the capturer screenshots `.stage` and `.stage` was on the inner content div, inside the unit's 1px border. An off-size creative is rejected outright by ad networks, so this was the difference between an asset and a file. `.stage` is now the unit itself. Bytes. 327KB for the rectangle against a 150KB ceiling. The cause was not the frame count, it was that everything moved on every frame: a full-width sweep running the whole loop, plus a drifting background wash, changed every pixel of every frame, so GIF's per-frame rectangle diff had nothing to elide. Motion is now confined to three short windows — entrance, one sweep, the CTA beat — with the unit at rest between them, which reads as a deliberate beat rather than a limitation. The wash is static; animating it cost most of the file for motion nobody could see. 10fps and 40 frames rather than 12.5 and 50, because 10fps is exactly 10 centiseconds and GIF stores delays in hundredths. A test now asserts that most of the loop is perfectly still. That is a size requirement wearing a timeline's clothes: lose it and the banners quietly stop fitting. Scope. Product ads only was being enforced by the backfill alone. The dashboard save and the shared creator behind the public API queued a render for any campaign, so campaigns pointing at blog posts and social profiles were getting video and animated banners that were explicitly out of scope — the first banner rendered in production advertised a dev.to article. The rule now lives in queueCampaignVideo, where all three paths pass through, for the same reason the API gap taught: a rule enforced at one of three call sites holds only until someone adds a fourth. A caller that passes no destination still renders, so a forgotten argument produces an extra video rather than a campaign that mysteriously never gets one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan48 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 31 | LOW: 15
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three defects in the animated banners, all visible in the first real renders.
1. Every unit was two pixels short
298×248for a 300×250,726×88for a 728×90. The capturer screenshots.stage, and.stagesat on the inner content div — inside the unit's 1px border.An off-size creative is rejected outright by ad networks, so this was the difference between an asset and a file.
.stageis now the unit itself.2. Over the byte budget, and not for the reason I assumed
327 KB for the rectangle against a 150 KB ceiling. The cause wasn't the frame count — it was that everything moved on every frame. A full-width sweep running the whole loop, plus a drifting background wash, changed every pixel of every frame, so GIF's per-frame rectangle diff had nothing to elide. I was paying full price for 50 frames.
Motion is now confined to three short windows — entrance, one sweep, the CTA beat — with the unit at rest between them. That reads as a deliberate beat rather than a limitation. The wash is static: animating it cost most of the file for motion nobody could see.
10fps / 40 frames rather than 12.5 / 50, because 10fps is exactly 10 centiseconds and GIF stores delays in hundredths.
A test now asserts most of the loop is perfectly still. That's a size requirement wearing a timeline's clothes — lose it and the banners quietly stop fitting.
3. Scope: blog and social campaigns were getting media
You said product ads only. That was enforced by the backfill alone — the dashboard save and the shared creator behind the public API queued a render for any campaign. The first banner to render successfully in production advertised a
dev.toarticle.The rule now lives in
queueCampaignVideo, where all three paths pass through — the same lesson as the API gap: a rule enforced at one of three call sites holds until someone adds a fourth. A caller that passes no destination still renders, so a forgotten argument produces an extra video rather than a campaign that mysteriously never gets one.Verification
2570 passed / 1 failed repo-wide — the pre-existing
tracker-geofailure. Both typechecks clean. Four new scope tests (blog skipped, social skipped, product rendered, unknown destination rendered) and the stillness assertion.The byte reduction can only be measured against real rendered frames, which needs the container's Chromium — I'll queue a live render after this deploys and report the actual sizes and dimensions before calling it done.