Quick answer: Normalize the stack trace, walk it against a path- and pattern-based ownership table, fall back to git blame on the top frame, and apply a per-engineer load cap. Treat auto-assignment as a first guess, not a verdict — humans should be able to reassign in one click and teach the system by updating the ownership file.
Crash triage is the part of shipping games that nobody budgets for. You enter beta expecting to write code, and instead you spend four hours a day reading stack traces and pasting Jira links into Slack. Auto-assignment turns that four hours into fifteen minutes of review. The system I describe here is deliberately simple — a few hundred lines of code — but it consistently routes 80–90% of crashes correctly on day one and improves from there.
Step 1: Normalize the Stack Trace
Raw stack traces are a mess. They contain mangled C++ names, inlined frames, address offsets that change every build, and engine internals that aren’t actionable. Before you can match crashes to engineers, you need a clean representation.
- Strip absolute offsets and build IDs so identical crashes group together.
- Demangle C++ symbols (
c++filton Linux,UnDecorateSymbolNameon Windows). - Skip engine frames that aren’t owned by your team (
UnityEngine.*,UE::Core,godot::*). - Collapse inlined frames if your symbol server can’t resolve them reliably.
What remains is a short, clean list of your team’s code frames. That’s the input to your assignment logic.
Step 2: Ownership Rules
Keep ownership in a single file checked into the game repo. Text-based ownership files are easier to review in pull requests than database rows, and they put responsibility in the same place as the code. Here’s the format I use:
# CODEOWNERS-style mapping — path : primary : backup
src/networking/** : alex : mara
src/physics/** : mara : alex
src/audio/** : james : sofia
src/ui/** : sofia : james
src/save_system/** : alex : mara
# Pattern-based fallbacks (top-of-stack symbol match)
symbol: *::MatchmakingClient::* : alex
symbol: *::PhysicsIntegrator::* : mara
symbol: *::AudioMixer::* : james
Walk the cleaned stack from the top frame down. The first frame whose file path or symbol matches a rule wins. This order matters: the top of the stack is usually where the crash actually happened, and lower frames are context. A crash in PhysicsIntegrator::Step called from GameLoop::Tick is a physics bug, not a game-loop bug.
Step 3: Git Blame as Fallback
When no ownership rule matches — common in new code or in edge cases — fall back to git blame on the top frame’s source file. Pull the last committer of the offending line and assign to them, with a note explaining the fallback was used. The note matters: engineers push back if they receive a crash from a file they’ve never touched, and the note lets them verify the logic.
def blame_owner(file, line):
result = subprocess.run(
["git", "blame", "-L", f"{line},{line}", "--porcelain", file],
capture_output=True, text=True
)
for ln in result.stdout.splitlines():
if ln.startswith("author-mail "):
return email_to_handle(ln.split(maxsplit=1)[1])
return None
Cache blame results by file and line hash — blame is not free on large repos, and crash groups are stable enough that the same group lookup happens hundreds of times.
Step 4: Load Balancing and Caps
Without safeguards, auto-assignment will dump 90% of crashes on whoever owns the InputSystem module, because input touches every frame. Apply per-engineer caps. If Alex already has 15 active crashes assigned, the 16th routes to his backup, Mara, with a note explaining the overflow.
I’d keep the cap at 10–20 for active (unresolved) crashes per engineer. Anything beyond that stops being useful feedback and starts being a backlog that gets ignored.
Step 5: Escalation
Some crashes can’t wait. If the crash rate for a specific signature crosses a threshold — say, more than 1% of sessions in the last hour — escalate beyond normal assignment. Page the on-call engineer, drop a message in the incident channel, and bump the priority automatically. The system I use marks these as P0-auto so they’re visually distinguishable from human-tagged P0s.
The best thing about auto-assignment isn’t that it picks the right engineer. It’s that it picks an engineer within a minute. Even a wrong first assignment gets reassigned faster than a ticket that sat in the triage queue for three days.
Step 6: Integrate With Your Tracker
The auto-assignment service should be a thin integration layer. When a new crash group fires, it calls your bug tracker’s API, creates the issue, sets the assignee, and attaches the reasoning as a comment. When an engineer reassigns, a webhook tells the service to update its internal stats so load balancing stays accurate.
Bugnet’s API exposes the hooks for all of this out of the box: create an issue with POST /api/v1/bugs, assign with PATCH /api/v1/bugs/{id}, and subscribe to bug.assigned webhooks. The integration is usually one afternoon of work for a shop that already has crash reporting flowing in.
Tuning From Real Data
Review reassignments weekly. Every time an engineer moves a crash from the auto-assigned engineer to someone else, that’s a signal the ownership table is wrong. Track the ten most-reassigned patterns and update the rules. Within a month of tuning, a well-run auto-assignment system will be routing 90%+ of crashes correctly on the first try.
Related Issues
For the crash-grouping foundation this builds on, see best practices for error logging in game code. For on-call design that pairs with escalation, read how to set up a bug report SLA for your studio.
The goal is never perfect assignment. It’s an assignment within ninety seconds that a human can correct in five.