Skip to content
Edgar Mesquita edited this page Oct 4, 2026 · 14 revisions

Debugging & Development Tools

🌐 This page in: English · Português

eQuantic.UI provides professional debugging tools similar to Next.js, with development-only features that help you identify and fix issues quickly.

🔍 Development Mode Detection

The framework automatically detects the environment using IWebHostEnvironment.IsDevelopment() and exposes it to the browser via:

window.__EQ_DEV__; // true in development, false in production

All development tools are conditionally loaded based on this flag.

📝 Logger System

The logger provides consistent, prefixed logging that only outputs in development mode.

Usage

import { logger } from "@equantic/ui-runtime";

// Development only (silenced in production)
logger.debug("Component state:", state);
logger.info("API call completed");

// Always logs (even in production)
logger.warn("Deprecated API used");
logger.error("Failed to load data:", error);

Log Levels

Method Output Production Prefix
debug() Console debug ❌ Silenced [eQuantic.UI]
info() Console info ❌ Silenced [eQuantic.UI]
warn() Console warn ✅ Always [eQuantic.UI]
error() Console error ✅ Always [eQuantic.UI]

Filtering Logs

In browser DevTools, you can filter by the prefix:

[eQuantic.UI]

Implementation

The logger is implemented in src/eQuantic.UI.Runtime/src/utils/logger.ts:

const isDev = typeof window !== "undefined" && window.__EQ_DEV__;

export const logger = {
  debug(...args: any[]) {
    if (isDev) console.debug("[eQuantic.UI]", ...args);
  },

  info(...args: any[]) {
    if (isDev) console.info("[eQuantic.UI]", ...args);
  },

  warn(...args: any[]) {
    console.warn("[eQuantic.UI]", ...args);
  },

  error(...args: any[]) {
    console.error("[eQuantic.UI]", ...args);
  },
};

🚨 Error Overlay

The error overlay provides a full-screen, Next.js-style error UI that appears automatically when runtime errors occur.

Features

  • Automatic Capture: Catches unhandled errors and promise rejections
  • C# stack traces ✨: the overlay is source-map aware: it fetches each bundle's .js.map, decodes it (src/dev/source-map.ts + src/dev/stack-remapper.ts), and rewrites the Call Stack as the original C# frames, plus a snippet of the failing C# source line. Falls back to the JS view if no map is available. (This is what makes the "0 JS knowledge" promise real at debug time: a C# developer sees C#, not transpiled JavaScript.)
  • Keyboard Support: Press Esc to close
  • Development Only: Never appears in production (loaded via a dynamic import() in dev)
  • Clean UX: Red header, monospace font, scrollable content

When It Appears

The error overlay automatically displays for:

  1. Unhandled Errors: Any uncaught exception in JavaScript
  2. Promise Rejections: Unhandled async errors
// These will trigger the error overlay in dev mode
throw new Error("Something went wrong");

Promise.reject("Async error");

await fetch("/api/data"); // If fetch fails and not caught

Error Overlay UI

┌─────────────────────────────────────────────┐
│ ⚠️ Build Error                    Close (Esc)│
├─────────────────────────────────────────────┤
│                                             │
│ Error message here                          │
│                                             │
│ ┌─────────────────────────────────────────┐ │
│ │ Stack trace:                            │ │
│ │   at MyComponent.render (page.js:42)    │ │
│ │   at Reconciler.patch (reconciler.js:12)│ │
│ │   ...                                   │ │
│ └─────────────────────────────────────────┘ │
│                                             │
├─────────────────────────────────────────────┤
│ This error overlay only appears in          │
│ development. Fix the error to continue.     │
└─────────────────────────────────────────────┘

Manual Error Display

You can manually show errors in the overlay:

import { errorOverlay } from "@equantic/ui-runtime/dev";

if (window.__EQ_DEV__) {
  errorOverlay.show({
    message: "Custom error message",
    stack: error.stack,
    componentStack: "Component hierarchy...",
  });
}

Clearing the Overlay

// Programmatically clear
errorOverlay.clear();

// User actions
// - Press Esc key
// - Click "Close" button

Implementation

The error overlay is implemented in src/eQuantic.UI.Runtime/src/dev/error-overlay.ts:

class ErrorOverlay {
  private overlay: HTMLDivElement | null = null;
  private errors: ErrorInfo[] = [];

  show(error: ErrorInfo) {
    if (!window.__EQ_DEV__) return; // Dev only

    this.errors.push(error);
    this.render();
  }

  clear() {
    this.errors = [];
    if (this.overlay) {
      this.overlay.remove();
      this.overlay = null;
    }
  }

  private render() {
    // Creates full-screen overlay with error details
  }
}

export const errorOverlay = new ErrorOverlay();

// Auto-capture errors
if (window.__EQ_DEV__) {
  window.addEventListener("error", (event) => {
    errorOverlay.show({
      message: event.message,
      stack: event.error?.stack,
    });
  });

  window.addEventListener("unhandledrejection", (event) => {
    errorOverlay.show({
      message: `Unhandled Promise Rejection: ${event.reason}`,
      stack: event.reason?.stack,
    });
  });
}

🛠️ Debugging Components

Browser DevTools

A Debug build writes a source map beside each module, linked from it and carrying the C# the module came from, so the browser's debugger shows the C# itself.

Chrome DevTools:

  1. Open DevTools (F12)
  2. Go to Sources tab
  3. Find the C# file in the tree, named by its path inside the project (Pages/Home.cs), never by the build machine's
  4. Set breakpoints directly in the C# source
  5. Inspect state, props, and local variables

Source Maps

What a module's map carries is the EQuanticSourceMaps property, and its default follows the configuration:

EQuanticSourceMaps Default for What the build writes
full Debug a map beside each module, linked from it, with the C# inside, for the browser's debugger
external (opt in) a map without the C# and without the link, for an error reporter that uploads maps itself; bun's debugId is kept
none every other configuration no map, and no link

A map with the C# inside is the component's source, a [ServerAction]'s body included, and the output folder is a web root: that is why only Debug writes one. Whatever the property says, a publish takes no map from the compiler's output. To keep maps for a deployed build, write them without the C#:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <EQuanticSourceMaps>external</EQuanticSourceMaps>
</PropertyGroup>

eqc composes each map itself, in C#, with nothing installed: bun maps the JavaScript to the TypeScript eqc wrote, eqc's own V3 map leads from that TypeScript to the C#, and the two become one map from the JavaScript the browser runs to the C#. Its sources are named relative to the map and inside the project, so a debugger shows the project's own path; a file from outside the project (a package's sources in the NuGet cache) is named by its file name alone. A module written from more than one file names each one, with its own C#: a class that takes the default an interface supplies leads the default's lines to the interface's file, where the debugger shows them (Since 0.2.0-preview.60). A map is JSON whatever the C# holds, a form feed or any other control in a comment included (Since 0.2.0-preview.60). A full map, shortened:

{
  "version": 3,
  "sources": ["../../Pages/Home.cs"],
  "sourcesContent": ["…the C# of Pages/Home.cs…"],
  "mappings": "AAAA;AACA;..."
}

Each statement has a segment of its own (Since 0.2.0-preview.58): a frame or a breakpoint lands on the C# statement that threw or that it was set on, not on its method's first line. A line the compiler writes by itself belongs to the statement that produced it: a pattern switch's else if maps to its case, a using's dispose to the using, a do's condition to itself, and a local function's statements and an expression-bodied member's expression to their own lines. A lambda's block maps statement by statement too (Since 0.2.0-preview.60), and what follows the block on its closing line maps to the statement again. That holds for a lambda passed to an instance or static method of the app's own, to a list's own method (ForEach, Find, Exists…) or to Where, Select, Any, All, Aggregate and the other LINQ operators written as one shape, for one in a collection expression (children: [ … ]), and for one held by a local, a local function or a delegate; and, since the same release, for one inside an expression-bodied member, an object creation, an object or collection initializer or an anonymous object. Every statement of a body with an out or ref parameter maps to its own line as well. That body runs inside an arrow the compiler adds, so a stack read in the browser has one frame more than .NET's between the throw and the caller. Elsewhere a lambda's block maps, for now, to the statement that holds it: passed to an extension method, to a delegate or through ?., given to First, Count, Sum, OrderBy, GroupBy and the other operators with a translation of their own.

This allows you to:

  • Set breakpoints in C# code
  • Step through C# logic
  • Inspect C# variable names
  • See original line numbers in stack traces

Component Inspection

To inspect component state and props:

// In browser console
window.__EQ_DEBUG = true; // Enable debug mode

// Components expose their state
const component = document.querySelector(
  '[data-component-id="abc"]',
).__component;
console.log(component.state);
console.log(component.props);

🧪 Testing & Debugging

Integration Tests with Playwright

For debugging SSR vs CSR rendering issues:

test("SSR matches CSR", async ({ page }) => {
  // Get SSR HTML
  const ssrResponse = await page.goto("http://localhost:5000");
  const ssrHtml = await ssrResponse.text();

  // Wait for CSR hydration
  await page.waitForLoadState("networkidle");
  const csrHtml = await page.content();

  // Compare
  expect(normalizeHtml(ssrHtml)).toBe(normalizeHtml(csrHtml));
});

Server-Side Debugging

Debug C# code normally with Visual Studio or VS Code:

  1. Set breakpoints in .cs files
  2. Run with debugger attached: dotnet run
  3. Breakpoints hit during:
    • Server-side rendering (SSR)
    • Server Action invocations
    • Component compilation

Network Debugging

Monitor Server Actions in browser DevTools:

  1. Open Network tab
  2. Filter by _equantic/actions
  3. Inspect:
    • Request payload (method name, arguments)
    • Response data
    • Timing information
    • Errors (with stack traces)

Asset Provider Debugging

When using IRequireAssets, verify that dependencies are correctly injected:

  1. Inspect Source: Open browser "View Page Source" and search for the script/style tags.
  2. Network Tab: Check if the external URLs (e.g., CDNs) are loading successfully (Status 200).
  3. Deduplication: Verify that multiple components didn't inject the same script twice.
  4. Order: Stylesheets should appear before scripts for proper rendering.

Server-Side Rendering (SSR) Assets

If assets are missing in the initial HTML:

  1. Verify the component implements IRequireAssets.
  2. Ensure AddUI() is called in Program.cs.
  3. Check if the AssetCollection is correctly gathering the assets during the render pass.

📊 Performance Debugging

Runtime Performance

The reconciler tracks performance metrics in development mode:

// Enable performance tracking
window.__EQ_PERF = true;

// View metrics
console.table(window.__EQ_PERF_DATA);

Metrics include:

  • Render Time: How long each component took to render
  • Diff Time: Time spent in reconciler
  • DOM Operations: Number of actual DOM changes
  • Event Listeners: Active listener count

Build Performance

Monitor compilation times:

dotnet build -v:detailed

Look for:

  • CompileEQuanticUI target duration
  • Number of components compiled
  • TypeScript generation time
  • Bun bundling time

🔧 Common Issues

Runtime.js Not Loading

Symptom: boot() never executes, GET /_equantic/runtime.js returns 404

Solution: the browser is answered by the Server, not by the file in wwwroot/_equantic/: app.MapUI() maps /_equantic/runtime.js to the bundle the eQuantic.UI.Server assembly embeds. A 404 comes either from that endpoint, whose response then names the resources the assembly does carry, or from no endpoint at all, so check that app.MapUI() runs. The build's own copy, wwwroot/_equantic/runtime.js, is the same bytes for the tools that load the modules outside a server; the CopyEQuanticRuntime target takes it from the Server package, and the build fails with "the served runtime was not found" when it cannot (see Package Architecture)

# The copy comes from the Server package (not the SDK's)
ls ~/.nuget/packages/equantic.ui.server/<version>/tools/runtime/

# Force rebuild
dotnet clean
dotnet build -v:n

Theme Tokens Missing in CSR

Symptom: Server-rendered HTML is themed, but client-side rendering isn't

Root Cause: the theme bridge blob was not adopted at boot

Solution:

  1. Verify runtime.js loads before component scripts
  2. Check browser console for [eQuantic.UI] Boot process started
  3. Inspect window.__EQ_THEME__ in console - should contain the serialized theme

Source Maps Not Working

Symptom: Can't debug original C# code in browser DevTools

Solution:

  1. Build in Debug, or set EQuanticSourceMaps to full: a build in any other configuration writes no map, and an external map is not linked from its module, so the browser does not find it on its own
  2. Check that .map files exist in wwwroot/_equantic/
  3. Enable source maps in browser DevTools settings
  4. Clear browser cache and rebuild
  5. A frame that stops in the TypeScript intermediate (obj/eQuantic/ts/…) comes from a map eqc could not compose, and eqc reports each one in the build log ("… leads to the TypeScript only")

Error Overlay Not Appearing

Symptom: Errors logged to console but no overlay

Checks:

  1. Is window.__EQ_DEV__ true? (Check in console)
  2. Is error overlay imported? (Check runtime.js includes it)
  3. Is error overlay CSS loaded? (Check for #equantic-error-overlay styles)

Force Show:

// Manually trigger overlay
import { errorOverlay } from "@equantic/ui-runtime/dev";
errorOverlay.show({ message: "Test error" });

🎯 Best Practices

Development Workflow

  1. Use Logger Liberally: Add debug logs during development, they're free in production
  2. Test Both Modes: Always test with Development and Production environment
  3. Monitor Network: Keep DevTools Network tab open to catch failed Server Actions
  4. Source Maps Come with Debug: a Debug build writes them (EQuanticSourceMaps is full there), so there is nothing to enable
  5. Use Error Overlay: Don't suppress errors - let the overlay show them

Production Debugging

For production issues:

  1. Server Logs: Check ASP.NET Core logs for Server Action errors
  2. Browser Console: Only warn and error logs appear
  3. Sentry/AppInsights: Integrate error tracking services
  4. Source Maps: a publish ships none. Build with EQuanticSourceMaps=external and upload the maps it writes in wwwroot/_equantic/ to your error tracker; never serve a full map from a web root, since it is the component's source

Debugging Checklist

Before reporting issues:

  • Check browser console for errors
  • Verify window.__EQ_DEV__ is true (dev) or false (prod)
  • Confirm runtime.js loads (Network tab)
  • Check the theme bridge blob (window.__EQ_THEME__)
  • Test with browser cache disabled
  • Try in incognito/private mode
  • Compare SSR HTML with CSR HTML
  • Check MSBuild output for warnings
  • Verify NuGet packages are correct versions

📚 Related Documentation

Clone this wiki locally