Debugging & Breakpoints
Learn how to use soli's powerful debugging tools including breakpoints, interactive REPL, and the development error page.
Breakpoints
Use the break() function to pause execution and open the interactive debug page. This is useful for inspecting variable state and understanding program flow.
Setting Breakpoints
def process_user(user_id: Int) -> Hash
user = database.find(user_id)
# Set a breakpoint to inspect the user object
break()
if (user == null)
return {"error": "User not found"}
end
profile = enrich_profile(user)
# Another breakpoint to see enriched data
break()
profile
end
Why Use Breakpoints?
Examine variable values and data structures at any point in your code
Follow the program flow step by step to understand logic
Run arbitrary soli code to test hypotheses or calculate values
Automatically triggered on errors to help diagnose issues
Tip
Breakpoints only work in development mode. In production, break() calls are ignored.
Debug Page
When a breakpoint is hit or an error occurs, soli displays a comprehensive debug page with multiple panels for investigation.
Debug Page Features
# Interactive REPL - Execute soli code in breakpoint context
# Stack Trace - Navigate through call stack frames
# Source Viewer - View source code with line highlighting
# Request Inspector - Examine request params, query, headers, body
# Environment Info - View time, method, path of the request
Stack Trace Navigation
Click on any stack frame to view the source code at that location. The source viewer shows the relevant code with the error line highlighted.
Stack Trace:
0. process_user at ./src/users.sl:15
1. handle_request at ./src/routes.sl:42
2. main at ./src/main.sl:8
Click on any frame to see the source code.
Source Code Viewer
The source viewer displays code around the breakpoint or error location with:
- Line numbers for easy reference
- Highlighted error/breakpoint line
- Context lines above and below
- Syntax highlighting
Interactive REPL
The debug page includes an interactive REPL that runs in the context of your breakpoint. You can execute any soli code to inspect and manipulate data.
The REPL endpoint is token-protected and accepts loopback clients by default. If your *.test domains point to a trusted local server on another machine, start dev mode with SOLI_DEV_REPL_ALLOW_REMOTE=1 and pin the token to a secret you control via SOLI_DEV_REPL_SECRET=<long-random-string>. The server refuses to start in remote-allowed mode without the secret (SEC-051) so the credential is never embedded in HTML error pages.
Available Variables
# Request object - access all request data
req # Full request object
req["params"] # Route parameters (e.g., {id: "123"})
req["query"] # Query string parameters
req["body"] # Request body (POST/PUT data)
req["headers"] # HTTP headers
# Session
session # Session data
# Breakpoint context
breakpoint_env # Local variables at breakpoint
REPL Features
# Use @ to reference the last result
req["params"]["id"] # Returns: "123"
@ + 100 # Returns: 223 (uses last result)
# Navigation with arrow keys
# Up/Down: Browse command history
# Execute any valid soli code
print("Debug output")user["name"]; # Inspect variable
config["database"]; # View configuration
Quick Inspect Buttons
Click the quick inspect buttons to instantly view common data:
# Available quick inspect buttons:
req # View full request object
params # View route parameters
query # View query string
body # View request body
session # View session data
headers # View HTTP headers
AQL Query Log
When the server runs with --dev, every AQL query a request executes through the Model layer is captured into a per-request stack. The dev_queries() builtin returns that stack, so you can render a debug bar showing every query, its bind variables, and how long it took.
Return Shape
dev_queries() returns an Array<Hash> with these keys per entry:
| Key | Type | Description |
|---|---|---|
| query | String | The AQL sent to SoliDB |
| bind_vars | Hash | null | Bind variables, or null if none |
| duration_ms | Float | Wall-clock time in milliseconds |
Controller Example
fn index
users = User.where("doc.active == true").all
posts = Post.includes("author").all
# dev_queries() returns [] in production, populated under --dev
render("users/index", {
"users": users,
"posts": posts,
"queries": dev_queries()
})
end
Debug Bar Partial
<% if queries.length > 0 %>
<div class="dev-bar">
<h3><%= queries.length %> AQL queries</h3>
<ol>
<% for q in queries %>
<li>
<code><%= h(q["query"]) %></code>
<% if q["bind_vars"] != null %>
<small>binds: <%= h(json_stringify(q["bind_vars"])) %></small>
<% end %>
<span><%= q["duration_ms"] %> ms</span>
</li>
<% end %>
</ol>
</div>
<% end %>
Coverage
- All
Modeloperations (all,where,find,create,update,destroy,count) - Eager-loaded
includesand soft-delete scopes - Validation lookups (
uniqueness) - HABTM join-table operations
- Direct
Solidb(host, db).query(...)calls - Mocked queries from
register_query_mock(short-circuit before the executor) - Internal session-storage queries via
SoliDBClient
Zero Production Cost
In production, the executor never calls into the logger — the gate is a single relaxed atomic load. dev_queries() always returns [] there, so it's safe to leave the debug bar partial in your layout unconditionally.
Per-request Flamegraph
Click flame in the dev bar to expand a hierarchical flamegraph of every span captured during the request — middleware, before/after actions, controller dispatch, view, partials, every Soli function call, plus DB and HTTP. Hover any rectangle for an exact duration; click to zoom in; double-click to reset.
What gets captured
Spans are emitted at well-known framework wrap points and at every interpreter-level function call. Color encodes the kind:
| Kind | Color | Source |
|---|---|---|
| middleware | amber | Each scoped or global middleware invocation |
| before_action / after_action | dim amber | Per controller hook fire |
| action | cyan | Top-level handler dispatch (e.g. posts#show) |
| view / partial | green | Each render(...) / render_partial(...) |
| db | purple | Each AQL query (name = first 80 chars) |
| http | pink | Each outbound HTTP.* call |
| fn | slate | Every Soli function call (interpreter path) |
Export to Perfetto / chrome://tracing
The flame panel includes a ⬇ trace.json link that downloads the same data as Chrome Trace Event Format JSON. Drop the file into ui.perfetto.dev or open chrome://tracing for full timeline navigation, search, and aggregation across spans.
Production-safe. The flamegraph capture is gated on --dev: every span hook short-circuits on a thread-local enable flag, so production servers don't pay the cost. The panel is only injected into text/html responses, never into JSON or other content types.
HTMx-aware. Soli skips dev bar injection on responses to requests carrying the HX-Request: true header. HTMx swaps a fragment into a page that already shows the dev bar, so re-injecting the bar into the fragment would stack a duplicate at the bottom of the page on every swap.
Reading the chart
The X-axis is request time in microseconds (0 = request start). Y-axis is stack depth: parents are above their children. A wide rectangle near the top is a slow phase; a wide rectangle deep in the chart is a slow function. Look for:
- A view rectangle that spans most of the request →
renderdominates; check the partial breakdown row in the render panel. - Many narrow purple rectangles at the same depth → likely an N+1; the AQL panel will flag it.
- A very wide cyan
actionwith thin children → time is spent in user code outside the framework hooks; turn on thefnspans (always on under--dev) and zoom in to find the hot function.
Development Mode
Start the development server with hot reload to enable all debugging features:
# Start development server with hot reload
soli serve
# Allow the debug REPL from another trusted local machine
# (SEC-051: the secret is required — startup refuses without it)
SOLI_DEV_REPL_ALLOW_REMOTE=1 SOLI_DEV_REPL_SECRET=<long-random-string> soli serve . --dev
# The server will automatically reload when files change
# Breakpoints and debug page work in this mode
Development vs Production
| Feature | Development | Production |
|---|---|---|
| Breakpoints | Enabled | Disabled |
| Debug Page | Full interactive | Simple error |
| Hot Reload | Enabled | Disabled |
| Stack Traces | Detailed | Minimal |
AQL Query Log (dev_queries()) | Captured per request | Always returns [] |
Security Warning
Never run in development mode in production. The debug page exposes sensitive information including request data, environment variables, and source code. Only set SOLI_DEV_REPL_ALLOW_REMOTE=1 on trusted local networks, and always pair it with SOLI_DEV_REPL_SECRET so the REPL credential isn't embedded in HTML error pages (SEC-051).