This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
Project Overview
forem is the open source platform behind dev.to itself. I've had it starred and cloned for months and never opened the codebase — it's Rails, and I don't write Ruby. Jess's post was the reason that finally changed.
github.com/forem/forem
Bug Fix or Performance Improvement
#23687 is a one-line report: a user published exactly one post, and the dashboard's "Posts" counter said 2.
Not writing Ruby meant I couldn't guess my way to the fix from vibes. I had to actually trace it — reading DashboardsController, the sidebar partials, and the Article model until the shape of the bug was undeniable, not assumed.
The "Posts" badge in the dashboard sidebar renders @user.articles_count — a counter_culture cache on User that increments for every Article row belonging to that user, full stop. No filter on type, no filter on state:
# app/models/article.rb
counter_culture :user
But the link that badge sits on always opens the same default view: DashboardsController#show with no params. That view only lists non-archived, full-post-type articles:
# app/controllers/dashboards_controller.rb
@articles = target.articles.from_subforem.includes(:organization)
@articles = params[:state] == "status" ? @articles.statuses : @articles.full_posts
@show_archived = params[:filter].to_s.casecmp("archived").zero?
Forem has three article types — full_post, status (a short "Boost" update), and fullscreen_embed — and the counter doesn't distinguish between them, or between archived and active. The badge counts everything. The list under it shows a strict subset. Anyone who's ever posted a status update, or archived a post, sees a number that doesn't match what they can actually click into and see — exactly what got reported in #23687.
I couldn't verify that in Ruby, but I recognized the shape of it instantly once it was laid out: a cached count drifting from what a filtered view actually renders. I've shipped that exact bug in JavaScript. Same failure, different syntax.
Code
github.com/forem/forem/pull/23690
The fix doesn't touch the shared articles_count counter — that cache is read elsewhere for badges and spam heuristics, where "every article this user has ever made" is the correct meaning. Instead, DashboardsController gets a helper scoped to match what the Posts tab actually renders, and both the full-page and AJAX sidebar actions use it instead of the raw cache:
# The "Posts" nav item always links to the default (non-archived, full posts
# only) view of the user's own dashboard, so its indicator should reflect
# that same scope rather than the user's raw articles_count, which also
# includes statuses and archived posts that never show up in that list.
def posts_count_for(user)
user.articles.from_subforem.full_posts.where(archived: false).count
end
My Improvements
There was no way for me to eyeball this and trust it — I can't read Ruby well enough for that, and there's no Ruby or Postgres on the machine I was working from, so I couldn't run the spec suite locally either. Verification had to happen somewhere else: I wrote regression specs asserting a user with one full post, one status, and one archived post should see a count of exactly 1, pushed the branch, and let Forem's own CI be the judge instead of my own confidence.
CI caught something real on the first run — not in the fix, in my test. create(:article, type_of: "status") failed its own model validation, because status-type articles in Forem aren't allowed to have body markdown, and the factory's default does. I found the pattern already used elsewhere in the suite (body_markdown: "", main_image: nil), fixed the two specs, and pushed again.
That failure is the actual proof this wasn't guesswork dressed up as a fix. If I'd been able to run specs locally I might have caught it before pushing; instead the project's own CI did the job a local run would have.
Same lesson my other two entries kept landing on: The Cloudflare Worker That Ran Perfectly and Still Failed Twice and I Was Filming a Demo of My Monitoring Tool. The Monitor Wasn't Monitoring. — "it compiled" and "it's correct" are different claims, and only one of them is worth trusting.
Everything's green now — 19 successful checks, 1 skipped, 0 failures, including the shard that runs dashboard_spec.rb. The PR is open against forem/forem and waiting on a maintainer review, since third-party fork PRs need one before merge. Not merged yet as of writing this — I'd rather say that plainly than imply otherwise.
Different from my other two entries in one way: I don't write Ruby. Claude found the bug and wrote the fix. I picked the issue and gated everything that left my machine — the fork, the push, the PR, the CLA. Full delegation on the code, not on whether it shipped.
Top comments (29)
Great post! I'd love to contribute something to Forem one day as well!
Also... those submissions are way too good. I was counting on winning that skateboard! 😂
Sylwia, do it . mine sat starred and uncloned for months before I actually opened it. The hardest part isn't the code, it's the opening. (And honestly, I'd trade the skateboard for the merge . still waiting on that part.)
nice. I have also noticed the reading experience on devto has gotten way better. keep smashing bugs :)
I enjoyed the emphasis on verification over confidence. Not knowing Ruby didn’t stop you from reasoning about the bug because the underlying problem—cached state diverging from filtered data—is language-agnostic. Regression tests and CI are much stronger evidence than “it looks right.” Great contribution, and good luck with the merge!
Mustafa, language-agnostic is the plausible way to say what I was circling. The shape of the bug (cache vs. filtered view drifting apart) I could reason about cold.
Writing the actual fix in idiomatic Ruby and knowing counter_culture's real behavior instead of guessing at it, wasn't language-agnostic at all. That's the part I still needed Claude for.
That’s a distinction I completely agree with. Understanding the failure mode and implementing an idiomatic fix are two different skills.
AI is becoming remarkably good at bridging language-specific knowledge, but it’s still up to the developer to define the problem, challenge assumptions, and verify that the solution preserves the system’s intended behavior. That’s where engineering judgment still makes the difference. Thanks for the clarification
That's the whole job description now, honestly. defining the problem and setting the verification bar are the parts that don't delegate.
Well said.
Reasoning about the bug = language agnostic.
Shipping the fix in idiomatic Ruby + knowing counter_culture internals = not agnostic at all 😅
Glad Claude helped bridge that gap.
I don't know Ruby either, so this was nice to read 😄 I'd love to contribute to Forem one day too, but the Ruby codebase has always made me a little hesitant to try.
Hoping your PR gets merged, Daniel! Great work on this one.
Hema ,same boat not knowing Ruby is basically the whole premise of this one. The blocker was never reading Rails, it was reading Rails alone. Claude did the actual code work; I picked the target and gated everything that left my machine before it shipped. That's a real workaround for genuinely not knowing a languag, not a training-wheels version. What's the first thing you'd want to fix if you gave it a shot??
I haven't explored the Forem repo much yet, so I don't have a specific fix in mind 😄 I'd probably start with one of the good first issues and see where that takes me. Definitely want to give it a shot though!
Hema, heads up before you go looking: I checked and "good first issue" had zero open matches on forem/forem when I went hunting for mine . 213 closed, none open. The "always open for contribution" bug label is what's actually active right now. Worth starting there instead.
@hemapriya_kanagala look at the controller snippet in the post, most of a Rails app looks like that. Ruby is much easier to read than to write, the hesitation drops fast once you start.
That's reassuring to hear 😄 The controller snippet did look more readable than I expected. I think I just need to stop overthinking the Ruby part and actually spend some time exploring the codebase. Thanks for the encouragement, Leonid!
This is a perfect example of why cached counts are always a liability when the query shape can evolve independently.
The specific failure mode â a
counter_culturecache that counts everything, sitting next to a filtered view that counts a strict subset â is one of the most common consistency bugs in production systems. The fix you landed on (scoped helper that mirrors the actual query shape) is right, but it raises a subtler question: who owns the contract between the cache and the view?In a system with one team, the answer is implicit. In forem â open source, with contributors who may not know about the
articles_countcache when they add a new article type or filter â it's a structural hazard. Every time someone adds a new article type or a new filter condition, someone has to remember to update theposts_count_forhelper. That's a tribal knowledge trap.The robust version of this is query-derived counts computed on read (accept the p99 hit on the dashboard), or an explicit
scope_versionfield that the cache is keyed to, so any scope change bumps the cache key and forces recompute. Neither is free, but both eliminate the class of bug entirely rather than patching the specific instance.The point about CI catching a real failure in your test â not the fix â is the most important paragraph in the piece. That's the signal that you were actually outside your confidence region. Someone reading this who hasn't shipped a bad test they were sure was correct will not have the reference to understand why this matters.
Dev.to’s dashboard may sometimes display an incorrect post count because of caching delays, draft or published status differences, deleted posts, or temporary synchronization issues. If the number does not match the posts visible on your profile, checking the published-post list, refreshing the dashboard, or waiting for the platform to update may help.
At Aqva Marketing, accurate content tracking is important for measuring publishing consistency, audience engagement, and SEO performance. When platform dashboards show inconsistent data, it is better to verify the actual published content and use additional analytics tools rather than relying on one metric alone.
Good technical content. Quick mention: we just launched tools.shopveigo.com with a bunch of free AI tools (background remover, essay polisher, cover letter generator etc). Built for developers and content creators. Feedback welcome!
"I don't write Ruby" and then casually opens a PR against the actual dev.to codebase is the programming equivalent of "I don't really cook" right before someone plates a soufflé. Also deeply relatable that the bug was a counter counting things a filter refuses to show, the "it's not lying, it's just answering a different question" defense is doing a lot of work for both software and my own excuses on why my step count doesn't match how tired I feel.
I think I've noticed some occasional "glitches" in 'like' counters on articles and comments, but the glitches always seemed ephemeral or temporary ... nice job you did with this one!
leo , that tracks, actually. Reactions run through the same counter_culture mechanism I found powering the posts count in this bug (app/models/reaction.rb). I didn't dig into why yours self-corrects but same underlying pattern, different symptom. If it's reproducible, might be worth its own issue.
I ran into one instance where it did not self-correct - the scenario was like this:
I posted a comment, and then normally it always starts with 1 like - that happens automatically, even when nobody clicks "like": it starts with 1, not with 0 ...
But in this case it started with zero :-)
So I just liked my own comment (only case I've ever done that, lol), to "fix" it :-)
hadn't heard that before — comments seeding at 1, not 0, by default. If that's real, that's a different failure shape than mine: I had a stale read, yours sounds more like a missing default write. The self-fix by liking your own comment is the best repro step in this whole thread, 😀😀😀
Very, very rare "glitch", a comment starting out at 0 instead of 1 (likes, that is) - in all those years I've seen it just once ... yeah, liking my own comment was a very effective fix! 😁
leo, one in years is basically the control group confirming it's real. 😁
It was absolutely real, I'm not making it up - I'm also sure I wasn't dreaming :-)
Must have been something "really rare", but no idea what - race condition? Cosmic ray or meteorite hitting some piece of hardware? Dunno, the only thing I know is that it happened ...
Interesting perspective. One thing I'd add is that context matters a lot here — the right approach depends heavily on team size, project stage, and existing infrastructure. There's rarely a one-size-fits-all solution.