At commit 620c0e1, getsentry/sentry contains 44,115 analyzed functions, of which 4,027 fall in the critical band. Every one of the top 5 hotspots sits in the ‘fire’ quadrant — high structural complexity combined with recent commit activity — which means these are live regression risks, not backlog items. I would start with import_by_model in src/sentry/backup/services/import_export/impl.py, which carries an activity-weighted risk score of 20.72, a cyclomatic complexity of 48, and was touched today; that combination makes any change to it right now a bet against a function that already has 48 independent execution paths to get wrong.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
import_by_model | src/sentry/backup/services/import_export/impl.py | 20.7 | 48 | 9 | 45 |
useWidgetBuilderState | static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx | 20.5 | 78 | 7 | 71 |
reconstruct_messages | src/sentry/sentry_metrics/consumers/indexer/batch.py | 20.2 | 79 | 7 | 50 |
_bulk_snuba_query | src/sentry/utils/snuba.py | 20.1 | 48 | 7 | 38 |
dispatch | src/sentry/feedback/endpoints/error_page_embed.py | 19.9 | 33 | 3 | 36 |
Large Repo Analysis
sentry is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.
Sentry Hotspot Analysis — commit 620c0e1
Sentry is a large, actively developed error-monitoring platform. Across 44,115 analyzed functions, 4,027 are in the critical band — roughly 1 in 11 functions. The distribution below shows where the structural and activity pressure is concentrated:
44,115 functions analyzed
Every function in the repository falls into either ‘fire’ or ‘watch’ — there are zero dormant-complex or dormant-simple functions in the dataset. That means structural complexity in sentry is not sitting quietly; it is being actively changed. The five functions below are the ones where that combination is most acute.
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×5Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Complex Branching×4Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×4Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Hub Function×1Hub Function
Many other functions call this one — a change here ripples widely through callers.
All five top hotspots carry the exit_heavy, god_function, and long_function patterns simultaneously. That is not a coincidence — these are functions that grew to handle many responsibilities, accumulated branching for each edge case, and now exit early from many of those branches. Each early-return path is a test case the test suite may or may not cover.
import_by_model — impl.py
This function handles per-model import dispatch in sentry’s backup/restore RPC service. From the source excerpt, I can see it performs a cascading series of guard checks — validating the minimum ordinal, resolving the model by name, checking silo mode compatibility, asserting scope and import UUID presence — before constructing filters and selecting the correct import chunk type (ControlImportChunk vs RegionImportChunk) based on silo dependency resolution. All of that happens before it even reaches the transactional write.
The CC of 48 means there are 48 independent paths through this function. With a nesting depth of 9, some of those paths are deeply buried inside nested conditionals — the excerpt shows at least two levels of if metadata and metadata.fetch_type_ext and metadata.fetch_type_ext.is_global style chains, and that’s before the transaction block. A fan-out of 45 in Python is particularly concerning: because Python resolves types at runtime, each of those 45 callees could behave differently depending on silo mode or import flags, and none of those type contracts are enforced statically.
The exit_heavy and god_function patterns confirm what the code shows: this function is doing model resolution, silo validation, filter construction, idempotency checking, and transaction management all in one place. The excerpt even includes a comment acknowledging a data-race condition that isn’t fully solved — a signal that the function’s complexity has outpaced its own safety guarantees.
The function was touched today (1 commit in 30 days, days_since_changed: 0). Any change to the guard logic at the top alters the preconditions for everything below it.
Recommendation: Extract the guard/validation sequence (ordinal check, model lookup, silo check, scope check, UUID check) into a dedicated _validate_import_request method that returns either a validated context object or an RpcImportError. That alone removes at least 6–8 exit paths from the top of the function and brings the CC to a range where the transactional core can be reasoned about independently.
useWidgetBuilderState — useWidgetBuilderState.tsx
This is a React hook that manages the full state of sentry’s widget builder UI, serializing and deserializing every widget property — title, description, display type, dataset, fields, y-axis columns, query strings, sort configuration, limit, legend alias, legend type, selected aggregate, thresholds, linked dashboards, axis range, and text content — through query parameters or session storage. The source excerpt shows 15 separate useQueryParamState calls before even reaching the useMemo block that assembles the state object.
A CC of 78 is high by any measure — this function has 78 independent execution paths. Much of that complexity likely lives in the dispatch logic that handles WidgetAction variants, each of which must validate, transform, and route state changes to the appropriate setter. A fan-out of 71 means this hook directly calls 71 distinct functions; that includes deserializers, serializers, decoders, and state setters, and reflects the same blast-radius principle as a high fan-out in any language: a change to this hook’s internal logic can ripple into any of those 71 call targets.
The nesting depth of 7 suggests that some of the dispatch branches are themselves conditionally branched — likely handling dataset-specific field validation or display-type-specific sort behavior. The god_function and long_function patterns are accurate: this hook owns too much. It is both the state container and the state transition engine for the entire widget builder.
It was touched today (days_since_changed: 0). The dashboard widget builder is a user-facing feature under active iteration, making this a live regression surface.
Recommendation: Split the dispatch logic into a separate useWidgetBuilderDispatch hook or a reducer, and group the 15+ query-param state slices into logical sub-hooks by concern (e.g., useWidgetDisplayState for display type and dataset, useWidgetFieldState for fields and y-axis). That decomposition would cut both the CC and the fan-out roughly in half per unit.
reconstruct_messages — batch.py
This function sits in sentry’s metrics indexer consumer — the component that processes batches of raw metric messages from Kafka and maps tag keys and values to their integer IDs before forwarding them downstream. From the source excerpt, reconstruct_messages iterates over every message in an outer Kafka batch, checks whether each is filtered or invalid, pops the parsed payload, then for each tag key-value pair looks up the mapped integer ID, handles None returns by distinguishing between global and org-level quota exhaustion, conditionally indexes tag values if __should_index_tag_values is set, and accumulates COGS usage per use-case ID.
The CC of 79 reflects how many ways this loop can branch: filtered messages, invalid messages, missing keys, quota exceeded at org level, quota exceeded at global level, tag-value indexing enabled or not, None mapping results for both keys and values. Each combination is a distinct execution path. The nesting depth of 7 is visible in the excerpt — the if metadata and metadata.fetch_type_ext and metadata.fetch_type_ext.is_global check is already three levels deep inside a for k, v in tags.items() loop inside a for message in self.outer_message.payload loop.
In Python, the complex_branching and deeply_nested patterns here are especially risky because the quota-exceeded counter increments (exceeded_global_quotas, exceeded_org_quotas) are side effects that accumulate across loop iterations. Any new branch that skips an increment — say, a new metadata condition — produces silent undercounting with no type system to catch it.
This function was touched today. The metrics indexer is high-throughput infrastructure; a regression here affects metric ingestion for all customers.
Recommendation: Extract the per-message processing into a _reconstruct_single_message method that returns a structured result (including quota counters), keeping the outer loop responsible only for iteration and aggregation. The quota-exceeded logic that distinguishes global from org limits is a strong candidate for its own helper — it appears twice in the excerpt already.
_bulk_snuba_query — snuba.py
This function dispatches one or more queries to Snuba — sentry’s analytics query engine — either through a thread pool (for multiple queries) or directly (for a single query), then processes the response batch: parsing JSON, logging SQL if debug mode is active, detecting rejection due to quota policies, and mapping allocation policy metadata onto spans and SDK tags.
It carries a CC of 48, a nesting depth of 7, and a fan-out of 38. It is the only function in this top 5 with 2 touches in the last 30 days and 2 authors active in the last 90 days — both signals point to a function that multiple engineers are actively modifying in parallel. The _is_rejected_query branch in the excerpt triggers a nested loop over quota_allowance_summary, which itself iterates over possibly-dict values with another nested loop — that’s where the nesting depth accumulates. Every new allocation policy key added to the Snuba response format requires a change here.
The hub_function pattern is the distinguishing characteristic here compared to the other top hotspots: this function is a coordination point, not just a large function. It bridges thread pool management, HTTP response parsing, quota enforcement, and observability tagging. A change to the quota rejection format in Snuba’s response schema directly modifies this function, and that change then affects every caller that depends on proper span tagging or error propagation.
Recommendation: Extract the response-processing loop body — JSON parsing, SNUBA_INFO logging, quota rejection detection, and span tagging — into a _process_snuba_response function. That separates the dispatch strategy (single vs. thread-pool) from the response interpretation, and makes it possible to test quota-rejection handling without spinning up a thread pool.
dispatch — error_page_embed.py
This dispatch method handles the full HTTP lifecycle for sentry’s embedded error-page user feedback widget — the JavaScript snippet that surfaces a feedback form when a user encounters an error page. From the source excerpt, it validates the eventId query parameter, normalizes it, resolves the DSN project key, checks origin allowlisting, handles OPTIONS preflight, parses GET-parameter customization options, validates and saves a UserReport form, links the report to an event and environment, handles IntegrityError for duplicate submissions via an upsert, conditionally notifies on group association, and fires a user_feedback_received signal.
The CC of 33 reflects that branching chain. The nesting depth of only 3 is relatively contained — this function is wide rather than deep — but the fan-out of 36 is high for a single Django view method. Each of those 36 callees represents a dependency: eventstore.backend.get_event_by_id, atomic_transaction, router.db_for_write, user_feedback_received.send_robust, and so on. Because Python resolves these at runtime, a subtle change to any one of them — say, get_event_by_id returning a different shape — propagates here silently.
The exit_heavy pattern is prominent: the function has at least five early-return paths before it reaches the form-save logic. Each one requires a test case to confirm the correct response status and body. The god_function designation is earned — this single method spans authentication, input validation, database writes, event linkage, and signal dispatch.
This was touched today. The embedded feedback widget is a cross-cutting user-facing feature; a regression in dispatch means feedback stops being recorded or starts returning errors to end users.
Recommendation: Pull the UserReport persistence and event-linkage logic (the form.is_valid() branch) into a dedicated service method or command object. The validation and origin-checking guards at the top can stay in the view, but the database interaction — including the IntegrityError upsert pattern — belongs in a separate layer that can be tested without an HTTP request context.
Context functions
The context_only set includes useNavigate, useOrganization, setApiQueryData, onUpdate in issue actions, and list in the config runner. All are in the ‘watch’ quadrant — active but structurally simple (CC 3–7, nesting depth 1–2). I would not prioritize any of them for refactoring; they bear monitoring as activity signals, but they do not carry meaningful structural risk.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 5 |
god_function | 5 |
long_function | 5 |
complex_branching | 4 |
deeply_nested | 4 |
hub_function | 1 |
These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
Reproduce This Analysis
git clone https://github.com/getsentry/sentry
cd sentry
git checkout 620c0e1d9e76066b4b0e99705204508f4fc2e112
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →