ESC
Type to search...
S
Soli Docs

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?

Inspect State

Examine variable values and data structures at any point in your code

Trace Execution

Follow the program flow step by step to understand logic

Evaluate Code

Run arbitrary soli code to test hypotheses or calculate values

Debug Errors

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
queryStringThe AQL sent to SoliDB
bind_varsHash | nullBind variables, or null if none
duration_msFloatWall-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

Logged
  • All Model operations (all, where, find, create, update, destroy, count)
  • Eager-loaded includes and soft-delete scopes
  • Validation lookups (uniqueness)
  • HABTM join-table operations
Not Logged (v1)
  • 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
middlewareamberEach scoped or global middleware invocation
before_action / after_actiondim amberPer controller hook fire
actioncyanTop-level handler dispatch (e.g. posts#show)
view / partialgreenEach render(...) / render_partial(...)
dbpurpleEach AQL query (name = first 80 chars)
httppinkEach outbound HTTP.* call
fnslateEvery 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 → render dominates; 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 action with thin children → time is spent in user code outside the framework hooks; turn on the fn spans (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
BreakpointsEnabledDisabled
Debug PageFull interactiveSimple error
Hot ReloadEnabledDisabled
Stack TracesDetailedMinimal
AQL Query Log (dev_queries())Captured per requestAlways 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).

Next Steps

Combine debugging with testing to build robust applications.