Troubleshooting
As a project grows, a bug can hide deep inside a tree of components. Citry gives you four things to work with when that happens: render errors that name the failing component, verbose logs of the render walk, visual component and slot boundaries, and a way to capture the exact HTML a component produced.
Read browser diagnostics first
Client failures start with a [Citry] message in the browser console. Use the first error in the chain:
| Symptom | Likely cause | Fix |
|---|---|---|
| A second or foreign Alpine warning | The page or a dependency loads Alpine separately | Remove that script and use Citry's plugin hook |
| Props initialization was skipped | $c-props is missing, invalid, asynchronous, or has the wrong declared type | Return a synchronous plain object and follow the named prop diagnostic |
| Missing or changed physical cap | A minifier, CDN, sanitizer, or DOM update changed ownership comments | Preserve the complete client graph marker contract |
| Fragment graph or asset adoption failed | Citry routes are not mounted, or one manifest or asset is incomplete | Mount the integration and inspect the first network or graph error |
Native x-for, x-if, or x-teleport clone was rejected | The template tries to clone a server-rendered active Citry component | Render component lists on the server or keep the structural directive inside the component |
Citry fails a damaged graph before callbacks can observe partial ownership. Do not suppress the diagnostic and continue with only the visible HTML.
Read the component path in errors
When a component fails to render, the bare exception (say KeyError: 'name') does not say which component was rendering. Citry rewrites the message to name the path from the root component down to the one that failed.
Take a Page that renders a Card, which renders an Avatar that expects a name it never receives:
from citry import Component
class Avatar(Component):
template = """
<img c-alt="name" />
"""
def template_data(self, kwargs, slots):
return {"name": kwargs["name"]}
class Card(Component):
template = """
<div class="card">
<c-Avatar />
</div>
"""
class Page(Component):
template = """
<main>
<c-Card />
</main>
"""
str(Page())
Rendering Page raises, and the message names the chain that led to the failure:
An error occurred while rendering components Page > Card > Avatar:
name
The path reads root first and failing component last, so Avatar is where to look. It travels with the exception itself, so it survives being caught and re-raised: a try/except in your own code still sees the annotated message.
When the failure happens inside slot content, the path also carries a frame for the slot that was being filled, for example Page > Layout > Layout(slot:body).
Find the failing line in a template
The path above points at Python code (Avatar.template_data). When the error is in a template expression instead, citry adds an underlined snippet of the template, so you see the exact expression and the lines around it.
Here Profile reads user_name in its template but never defines it:
from citry import Component
class Profile(Component):
template = """
<p>Welcome, {{ user_name }}</p>
"""
Rendering it points straight at the offending expression:
An error occurred while rendering components Profile:
Error in variable: KeyError: 'user_name'
1 | user_name
^^^^^^^^^
In template of 'Profile' (/path/to/profile.py::Profile):
2 | <p>Welcome, {{ user_name }}</p>
^^^^^^^^^^^^^^^
The header names the component whose template failed and the file it lives in, and the ^^^ underline marks the expression that raised.
Turn on debug and trace logging
Everything citry logs goes through the standard library logger named citry, so you configure it the usual way with Python's logging module. Citry uses two levels:
DEBUG: loading a component's associated HTML, JS, and CSS files, and autodiscovery of component modules.TRACE: a detailed view of the render walk. Logs when components, slots, and nodes start and finish rendering, and which fills a slot received.
TRACE is a level citry adds below DEBUG, with the numeric value 5. To see trace logs, set the level to 5 (there is no named constant for it):
import logging
logging.basicConfig(level=5, format="%(levelname)s %(name)s %(message)s")
Rendering a small page (a HomePage that renders a Hello greeting) then logs each step of the walk:
TRACE citry RENDER COMPONENT: 'HomePage' ID cz9kert00 PATH: HomePage
TRACE citry RENDER NODE ComponentNode @22:44
TRACE citry RENDER COMPONENT: 'Hello' ID cz9keru00 PATH: HomePage > Hello
Each line shows the component (or node) and its PATH in the tree. The ID and the @start:end span identify one instance and its place in the template source.
basicConfig turns on logging for the whole program. To keep the rest of your app quiet and raise only citry's level, target the named logger:
import logging
logging.getLogger("citry").setLevel(5)
Trace logging is nearly free when it is off (the render path only checks whether the level is enabled), but with it on a render is roughly twice as slow, plus the cost of whatever handler writes each line. Use it while debugging, not in production.
Visualize component and slot boundaries
Install the opt-in Debug extension when you need to see which component or slot produced each region of a page. Component boundaries are blue and carry the component class and render ID. Slot boundaries are red and carry the receiving component class and slot name.
from citry import Citry, Component
from citry.ext.debug import Debug
app = Citry(
extensions=[Debug],
extensions_defaults={
"debug": {
"highlight_components": True,
"highlight_slots": True,
},
},
)
class Card(Component):
citry = app
template = """
<article><c-slot name="body" /></article>
"""
The engine defaults apply to every component. Override either switch on one component with its nested config:
class Layout(Component):
citry = app
template = """
<main><c-slot /></main>
"""
class Debug:
highlight_components = False
highlight_slots = True
Debug keeps Citry's data-cid-*, key, event, and CSS-variable markers on the elements authored by the component. Full-document component or slot boundaries and transparent structural components are skipped automatically.
The boundaries themselves are real <div> elements. They can change flex or grid children, direct-child selectors, exact element identity, and restricted table or select content. Use Debug for development inspection, not in production or for layout-sensitive behavioral tests. Place it after an output-rewriting extension if you want to inspect that extension's final result.
Inspect the rendered output
Rendering a component gives you back a plain HTML string, so you can write it to a file and read exactly what came out. Calling a component builds a CitryElement; str() on it runs the full render and returns the HTML:
html = str(HomePage())
with open("result.html", "w", encoding="utf-8") as f:
f.write(html)
If you need to serialize with specific options, render first and call serialize() on the result: HomePage().render().serialize() returns the same string and lets you choose how JS and CSS dependencies are placed (see JS and CSS dependencies).
Debug with an AI coding agent
The features above make citry work well with AI coding agents. To debug a render with one, give it three things: the component source (already in your repo), the HTML that was produced, and a log of how it was produced.
Capture the rendered output to a file as shown above, and send trace logs to their own file with a file handler:
import logging
handler = logging.FileHandler("citry.log", mode="w")
citry_logger = logging.getLogger("citry")
citry_logger.setLevel(5)
citry_logger.addHandler(handler)
Then prompt the agent with both files attached:
I have a citry project. Citry is component-based web rendering for Python, in the style of Vue or React.
I am rendering the
HomePagecomponent, but the output is missing the greeting. The trace log is incitry.logand the rendered HTML is inresult.html.Tell me what you would look for in the log and why, whether it is there, and how you would fix the issue.