
When I started building my first custom ExB widget, one of the earliest things I invested in was a structured debug logging system. I use AI heavily in my development workflow (what I call "Vector coding," where the human architects and AI executes), and that approach demands real data to inform decisions. I needed to see exactly what was happening inside the widget at every layer, and I needed that visibility to be something I could switch on and off without touching code.
So I built DebugLogger: a small utility that activates structured debug logging from the URL. Add ?debug=FETCH to your ExB URL and you get clean, tagged logs for your network calls. Add ?debug=FETCH,RENDER and you see rendering too. Remove the parameter and logging goes silent. Zero overhead when it's off, no rebuilds, works the same in dev, test, and production.
It's become one of the most valuable pieces of my toolkit. When someone on my team hits an issue, the conversation is simple: "Turn on ?debug=FETCH,SELECTION, reproduce the problem, and send me the console output." I get a complete picture of what happened. No guessing, no poking around in the dark, just the data to diagnose and fix.
I've been using this across every widget in my MapSimple open-source suite, and I pulled it out as a standalone drop-in file so anyone building ExB widgets can use it. Hopefully it saves you some time.
One File, Five Minutes
No dependencies. No build config changes. Copy it into your widget and go.
Step 1: Grab debug-logger-standalone.ts from the DebugLogger release and drop it into your widget:
your-widget/
src/
utils/
debug-logger.ts <-- right here
runtime/
widget.tsxStep 2: Open the file and edit the bottom section. Replace the widget name and tags with your own:
export const debugLogger = createDebugLogger('MYWIDGET', [
'FETCH', // API/network calls
'RENDER', // UI rendering
'CONFIG', // Settings load
'MAP', // Map interactions
'SELECTION', // Record selection
'LIFECYCLE' // Widget open/close
])Tags are just labels for the subsystems in your widget. You decide what makes sense.
Step 3: Use it:
import { debugLogger } from '../utils/debug-logger'
debugLogger.log('FETCH', { action: 'start', url })That's it. You're done.
Activate It From the URL
This is the whole point. No code changes to toggle logging:
URL What Happens
| ?debug=FETCH | Logs only FETCH-tagged calls |
| ?debug=FETCH,RENDER | Logs FETCH and RENDER |
| ?debug=all | Logs everything |
| (nothing) | Silent. Zero overhead. |
Tags are case-insensitive, so ?debug=fetch and ?debug=FETCH both work.
ExB Iframe Note
This tripped me up early on. ExB renders widgets inside iframes, so when you add ?debug=FETCH to your browser URL, that param lives on the parent page, not the iframe. The logger handles this automatically. It checks window.location first, then falls back to window.parent.location. You just add the param to whatever URL is in your address bar and it works.
What the Output Looks Like
Clean, structured JSON with a tagged prefix:
[MYWIDGET-FETCH] {
"feature": "FETCH",
"timestamp": "2026-03-25T10:30:00.000Z",
"action": "start",
"url": "https://services.arcgis.com/..."
}The tag tells you the subsystem, the data is structured, and it's easy to filter in DevTools.
The BUG Level
One pattern I've found really useful: a BUG log level that always fires, even when ?debug isn't in the URL.
debugLogger.log('BUG', {
bugId: 'BUG-007',
category: 'SELECTION',
description: 'Selected record ID not found in output DS',
recordId: dataId
})This uses console.warn so it stands out. I use it for race conditions I've identified but can't fully prevent, fallback paths that shouldn't normally execute, and data integrity issues. It's like a tripwire. If it fires in production, you know exactly what happened without needing to reproduce it with debug mode on.
Tags That Work Well for ExB
After using this across several widgets, here are the tags I keep coming back to:
Tag What I Log
| CONFIG | Settings load, validation, migration |
| RENDER | Mount/unmount, card display, template rendering |
| FETCH | Network requests, responses, errors |
| SELECTION | Record selection, highlight, deselection |
| MAP | View interactions, layer ops, popups |
| DARK-MODE | Theme detection and switching |
| LIFECYCLE | Widget open/close, visibility changes |
Keep them short and uppercase. They show up in both the URL and the console, so brevity helps.
Performance
When debug is off (no ?debug in the URL), the cost is essentially zero. The log() call checks a boolean and returns immediately without serializing anything.
When it's on, each call runs JSON.stringify() on whatever you pass in. For normal widget work this is negligible. Just don't put it inside a scroll handler or animation loop unless you throttle it.
See It in Action
Want to see what this looks like in a real widget? Open your browser's DevTools console and hit this link:
https://exb-sample.mapsimple.org/?debug=QUERY,HASH-EXEC#pin=2223059013
That URL activates two debug tags (QUERY and HASH-EXEC) and passes in a hash-based query. Watch the console as the widget parses the hash, executes the query, and returns results.
Here's another one using FeedSimple, a live data feed widget pulling USGS earthquake data:
https://feed-quakes.mapsimple.org/?debug=FETCH,FEED-LAYER
This one shows the data fetch cycle and the feed layer creation. Same logger, different widget, same clean output. That's the kind of visibility you get.
Here are the tags you can try on the FeedSimple sample:
Tag What You'll See
| FETCH | Feed URL requests, response status, timing |
| PARSE | XML parsing, field discovery, item counts |
| RENDER | Card rendering, template processing |
| POLL | Auto-refresh polling cycle |
| JOIN | Spatial join with map layers |
| FEED-LAYER | Map layer creation, points, symbology, popups |
| TEMPLATE | Markdown template processing, token substitution |
| SETTINGS | Settings panel activity, config changes |
| EXPORT | CSV/data export |
| SEARCH | Runtime search filtering |
| SORT | Runtime sort operations |
| FEATURE-EFFECT | Feature effects (highlight, filter on map) |
And here are the tags for the QuerySimple / HelperSimple sample:
Tag What You'll See
| all | Every log across both widgets (heads up, high volume) |
| HASH | Deep link consumption and URL parameter parsing |
| TASK | Query execution, performance metrics, and data source status |
| RESULTS-MODE | Transitions between New, Add, and Remove selection modes |
| EXPAND-COLLAPSE | State management for result item details |
| SELECTION | Identify popup tracking and map selection sync |
| RESTORE | Logic used to rebuild the map selection after an identify event |
| WIDGET-STATE | The handshake between HelperSimple and QuerySimple |
| GRAPHICS-LAYER | Highlighting logic for graphics-enabled widgets |
Go Get It
The standalone file and full guide are in the DebugLogger release. One TypeScript file, no external dependencies, works with any ExB custom widget. Copy it, edit the config at the bottom, and you should be up and running in about five minutes.
If you're curious about the broader MapSimple project, it's a growing suite of open-source ExB widgets. The DebugLogger is the shared foundation that all of them use.
Questions, ideas, or bugs? Feel free to drop those here and I will work to address them. I really hope this can help ExB devs in the community. Happy coding!
Download: debug-logger-standalone.ts | Project: MapSimple