This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
Project Overview
snipforge.video is an AI video editing SaaS I run solo. Users upload a video and run tools on it in the browser: trimming, compression, GIF creation, AI dubbing with speaker diarization, auto captions, background removal, and about twenty other operations. Under the hood it is a single-file Flask app of roughly 16,000 lines, deployed on Railway, with FFmpeg doing the heavy lifting and Cloudflare R2 storing the media.
Being a solo founder means I am also the QA team, which is fitting because my day job is Lead QE. This bug still got past me for a while, and the reason it did is the interesting part.
Bug Fix or Performance Improvement
The GIF tool lets users pick a start and end time so they can turn a five second moment into a GIF instead of converting the whole video. Users were reporting that clip selection did nothing. Pick seconds 12 to 17 of a three minute video and you get a GIF of the entire three minutes: wrong content, a bloated file, and a job that takes far longer than it should.
The route handler looked correct. It read start and end from the request and passed them into the worker:
elif op=="gif":
threading.Thread(target=convert_gif, args=(jid, src, str(dst),
int(data.get("fps",10)), int(data.get("width",480)),
float(data.get("start",0)), float(data.get("end",0)))).start()
The function signature looked correct too:
def convert_gif(jid, src, dst, fps=10, width=480, start=0, end=0):
No errors, no exceptions, no log lines. Everything about the call site said this feature worked.
The problem: convert_gif was defined twice in the file. Once with full trim support, and again about 200 lines below it as an older, simpler version. Python does not warn you about duplicate function definitions in the same module. The second def silently rebinds the name, so the old version won. Its signature accepted start and end, but its body never referenced them. The parameters were accepted, then thrown away.
A 16,000-line single file made this easy to miss. Both definitions are 200 lines apart, both have identical signatures, and nothing in the runtime behavior distinguishes "trim ignored" from "trim never requested."
Code
snipforge is a closed-source production app, so instead of a repo link I am sharing the exact changes as snippets. Function and file names are lightly genericized; the diff is otherwise verbatim.
The fix itself is deliberately minimal. Delete the shadowing duplicate, keep the complete implementation (which trims via FFmpeg before the palette pass and cleans up its temp files), and leave a guard comment where the duplicate lived:
# NOTE: an older duplicate `convert_gif` used to be defined here. Python silently lets a
# second same-name def shadow the first, and the older copy ignored its start/end
# params, so clip-range GIFs converted the ENTIRE video. Keep exactly one convert_gif
# (defined above, with trim support). Do not re-add a second definition.
Before and after, same source video, same requested 5 second range:
| Before | After | |
|---|---|---|
| Requested range | 12s to 17s | 12s to 17s |
| Output duration | 180s | 5s |
| Output size | 48 MB | 1.9 MB |
| Job time | 74s | 6s |
My Improvements
A few decisions worth explaining.
Why delete instead of merge. The surviving implementation already did everything the old one did, plus trimming, plus a palette-generation fallback, plus temp file cleanup on both success and failure paths. There was nothing in the duplicate worth keeping. The tempting move was to rename one of them and keep both around "just in case," which is exactly how the file got to 16,000 lines in the first place.
Verifying the fix without a test harness for FFmpeg. Unit testing FFmpeg pipelines is painful, so I verified structurally: an AST walk confirming exactly one convert_gif definition remains and that its body actually references the trim parameters. That is the check that would have caught this bug on day one, since the broken version parses cleanly and runs happily.
import ast
tree = ast.parse(open("app.py").read())
gifs = [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef) and n.name == "convert_gif"]
assert len(gifs) == 1
I have since generalized this into a lint step that fails on any duplicate top-level def in the module. Pylint's function-redefined (E0102) catches this class of bug for free, and I was not running it.
A known limitation I chose to keep. The trim uses -ss before -i with -c copy, which is keyframe seeking. Depending on GOP size the clip can start up to a couple of seconds early. For GIF creation that trade-off is fine, since the alternative is a frame-accurate re-encode that roughly doubles job time for a format where nobody notices a keyframe of slack. Worth knowing it is there, though.
The real lesson. This bug threw no exception, logged nothing, and returned a successful result every single time. The signature was lying and nothing in the runtime could tell. The only ways to catch a bug like this are static analysis or telemetry that compares what was requested against what was produced, which is where Sentry came in.
Best Use of Sentry
I instrumented the job pipeline with Sentry's Flask SDK and added distributed tracing around each FFmpeg operation. The key addition for this bug class is a span attribute that records the requested clip range next to the actual output duration probed from the finished file:
with sentry_sdk.start_span(op="video.gif", description="GIF conversion") as span:
span.set_data("requested_start", start)
span.set_data("requested_end", end)
# ... run the ffmpeg pipeline ...
span.set_data("output_duration", get_duration(dst))
span.set_data("source_duration", get_duration(src))
On the broken code, every GIF trace shows the same signature: requested_end - requested_start of a few seconds, output_duration equal to the full source. No error monitoring in the world flags that, because nothing errored. But the trace data makes the lie visible at a glance, and a simple Sentry dashboard query for jobs where output duration exceeds the requested range by more than a threshold turns a silent bug into an alert.
The broader win is that snipforge, a production app with real users, had zero error monitoring before this challenge. It now has error tracking and per-operation tracing across the whole FFmpeg pipeline.
Top comments (0)