Across portainer/portainer’s 10,656 functions, 597 are in the critical band — and the five highest-ranked are all in the ‘fire’ quadrant, meaning they combine heavy structural complexity with recent commit activity. CreateIngressView leads with a risk score of 20.14, a cyclomatic complexity of 58, and a max nesting depth of 7, and it was touched 6 days ago. That is not a cleanup backlog item — it is a live regression surface. Portainer is a container management platform with a full-stack TypeScript frontend and a Go backend; at the scale reflected here, I would start triaging with the Kubernetes ingress and API update layer before any other part of the codebase.
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 |
|---|---|---|---|---|---|
CreateIngressView | app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx | 20.1 | 58 | 7 | 72 |
createSseClient | app/react/portainer/generated-api/portainer/core/serverSentEvents.gen.ts | 17.4 | 7 | 11 | 27 |
endpointUpdate | api/http/handler/endpoints/endpoint_update.go | 16.3 | 15 | 5 | 39 |
updateEndpointGroup | api/http/handler/endpointgroups/endpointgroup_update.go | 15.3 | 9 | 5 | 29 |
registryUpdate | api/http/handler/registries/registry_update.go | 15.0 | 14 | 4 | 24 |
Large Repo Analysis
portainer 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.
Triage overview
10,656 functions analyzed
Every function in this repo falls into either the ‘fire’ or ‘watch’ quadrant — there are no dormant-complex outliers sitting untouched. The 2,137 fire-quadrant functions are both structurally non-trivial and recently changed; the 8,519 watch-quadrant functions are active but low-complexity and are not a refactoring priority. The five critical-band functions I’m covering here sit at the top of the fire quadrant and represent the highest live regression risk in the codebase.
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.Deeply Nested×4Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Complex Branching×3Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Cyclic Hub×1Cyclic Hub
Participates in a call cycle with other high-traffic functions, creating circular dependency risk.
The antipattern picture across the top five is consistent: every critical-band function carries the god-function, long-function, and exit-heavy patterns simultaneously. That combination is significant. God functions accumulate callees over time — each new feature adds another dependency rather than a new abstraction — and long functions resist the kind of targeted, scoped changes that keep regression risk low. Exit-heavy functions multiply that burden: each early-return path is a branch that needs its own test case, and in TypeScript the type-narrowing state at each exit point can diverge in ways that cyclomatic complexity alone understates.
CreateIngressView — app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx
CreateIngressView is the top-level component for creating Kubernetes ingress resources. A cyclomatic complexity of 58 means there are 58 independent execution paths through this component — each one a potential bug surface and a required test case. A max nesting depth of 7 tells me there are at least seven layers of nested control structures somewhere inside it, which makes local reasoning about state and props genuinely hard. The fan-out of 72 is the most striking number here: this component calls 72 distinct functions, making it a structural hub. A change to any one of those 72 dependencies can produce unexpected behavior in this component without any modification to CreateIngressView itself.
Every commit recorded against this file has been tagged as a bug fix — though with only one commit in the historical record, that is a thin signal rather than evidence of repeated defects. Still, the combination of extreme fan-out, deep nesting, and active change (touched 6 days ago) makes this the single highest-priority function in the repo for structural review.
The god-function and long-function patterns together suggest this component has grown by accretion. My concrete recommendation: identify the ingress-rule editing logic, the validation logic, and the form-state management as three separable concerns, then extract each into its own component or hook. That alone would reduce both the fan-out and the cyclomatic complexity substantially, and it would make the remaining paths in CreateIngressView testable in isolation.
createSseClient — app/react/portainer/generated-api/portainer/core/serverSentEvents.gen.ts
This function stands apart from the others in the top five because its cyclomatic complexity of 7 is moderate — not alarming on its own. The alarm comes entirely from the max nesting depth of 11. A nesting depth of 11 means the deepest logic in this function sits inside eleven layers of nested control structures. That is the kind of structure that emerges from deeply chained promise or async-event callbacks, which is exactly what you’d expect in a Server-Sent Events client: connect, handle open, handle message, handle error, handle reconnect, and so on, each layer adding another conditional wrapper.
The .gen.ts suffix is significant: this file is generated code, likely produced by openapi-ts (given the openapi-ts.config.ts file also present in the data). Generated code with ND 11 is still hard to debug and reason about at runtime — the generation process doesn’t flatten callback nesting. The commit history carries no defect signal, but it was changed 3 days ago, and the SSE pattern combined with 27 distinct callees means a subtle async sequencing bug here would be difficult to isolate.
My recommendation: review whether the SSE reconnection and event-dispatch logic can be expressed as a flat state machine or using an explicit event-emitter abstraction rather than nested callbacks. If this is generated, the fix may belong in the generator template rather than in the output file directly — worth checking the openapi-ts.config.ts configuration for hooks or transform options that flatten async nesting.
endpointUpdate — api/http/handler/endpoints/endpoint_update.go
endpointUpdate is the Go HTTP handler for updating environment (endpoint) configuration in Portainer. A CC of 15 sits above the moderate threshold of 10 — there are 15 independent paths through the update logic, each representing a different combination of fields being updated, authorization checks, or edge-case validations. The fan-out of 39 is the more telling number: this handler reaches into 39 distinct functions, spanning what are likely authorization checks, datastore reads, payload validation, notification dispatch, and downstream service updates.
The god-function, long-function, and exit-heavy patterns are all present. Exit-heavy handlers in Go are common — the idiomatic early-return-on-error style naturally produces many exit points — but at 15 CC and 39 callees, the combination makes it difficult to reason about which downstream operations have already been committed when any particular exit is reached. A partial update that errors halfway through is a consistency risk.
All commits recorded against this file are tagged as bug fixes — but there is only one, the same thin signal seen in CreateIngressView. The function was touched 6 days ago. My recommendation: identify the distinct responsibilities — request parsing, authorization, field-by-field update logic, and persistence — and extract each into a named function. That reduces both the CC and the fan-out of the handler itself, and it makes the partial-update consistency question explicit rather than implicit.
updateEndpointGroup — api/http/handler/endpointgroups/endpointgroup_update.go
updateEndpointGroup mirrors endpointUpdate in structure but handles endpoint group configuration. Its CC of 9 is just below the moderate threshold of 10, but the deeply-nested pattern flags a max nesting depth of 5, and the fan-out of 29 places it firmly in god-function territory for a handler that conceptually has one job: apply a diff to a group record.
All three of the overlapping patterns — god-function, long-function, exit-heavy — appear here as well. The single recorded commit against this file is tagged as a bug fix, following the same pattern as the other API handlers in this cohort. The function was last changed 6 days ago, the same commit window as endpointUpdate and CreateIngressView, suggesting a coordinated change touched all three.
Because endpoint groups aggregate multiple endpoints, changes to this handler can produce ripple effects across the environment list in the UI. My recommendation: extract the authorization and membership-reconciliation logic into separate, testable functions. The nesting depth of 5 suggests at least one deeply conditional block that could be inverted — rewriting as guard clauses would flatten the structure and make the exit paths explicit.
registryUpdate — api/http/handler/registries/registry_update.go
registryUpdate handles updates to container registry configuration. At CC 14 and a fan-out of 24, it is structurally the closest sibling to endpointUpdate in this list. The max nesting depth of 4 is lower than the others, but the complex-branching pattern is present alongside god-function, long-function, and exit-heavy — suggesting the complexity here comes more from branching logic (likely registry-type-specific update paths) than from deep nesting.
Registry credentials and access configuration are security-adjacent: a missed branch in update logic could leave stale credentials accessible or fail to propagate access-control changes. The single recorded commit against this file is also tagged as a bug fix, consistent with the other Go handlers in this group. Changed 6 days ago.
My recommendation: if the complex branching reflects per-registry-type behavior (e.g. DockerHub vs. ECR vs. GitLab registry), that is a strong signal to introduce a registry-type strategy or interface so each type’s update logic lives in isolation. That refactoring would reduce both the CC and the fan-out of the main handler to something closer to a dispatcher.
What the cohort pattern tells me
Four of the five critical functions — endpointUpdate, updateEndpointGroup, registryUpdate, and CreateIngressView — were all last changed 6 days ago. That is almost certainly a single coordinated feature or fix commit touching the environment management surface across both the Go API and the TypeScript frontend. When a single commit touches multiple god functions simultaneously, the blast radius is the union of all their fan-outs — in this case, that is a very large fraction of the application’s service layer.
Worth noting as context: every recorded commit against those four files has been a bug-fix commit. That does not prove the functions are defective, but it does suggest that when engineers touch these files, it is typically to fix something rather than to add a feature. That history, combined with the structural metrics, is a reasonable argument for prioritizing extract-method refactoring before the next development push.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 5 |
god_function | 5 |
long_function | 5 |
deeply_nested | 4 |
complex_branching | 3 |
cyclic_hub | 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/portainer/portainer
cd portainer
git checkout 580a9fdfcf3128519fdbf0c60bd4d351595463a7
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 →