Browsing profiles
The profiler UI endpoints, the debug headers linking a response to its profile, the built-in and custom list filters, and profile export.
Every profiled execution receives a unique token and lands in the profiler UI. This page is the reference for the endpoints that expose the collected data, the debug headers that link a response to its profile, the list filters, and how to export a profile.

Profiler UI endpoints
| Endpoint | Description |
|---|---|
GET /_profiler | Home page — profiles + views (HTML) |
GET /_profiler/:token | Profile detail page (HTML) |
GET /_profiler/:token/data | Raw profile data (JSON) |
Home page navigation
The home page uses the same two-column layout as the detail page: a sticky left sidebar lists the available views, and the active one is selected server-side from a ?view= query parameter (plain links, no client-side routing — consistent with the profiler's script-src 'self' CSP).
Each entrypoint kind is its own dissociated page under a Profiling group, and every global-scope collector is a view too:
| View | ?view= | Content |
|---|---|---|
| HTTP (default) | http | The HTTP list, its filters and its pager |
| GraphQL / Commands / … | the section key | One page per registered list section (each with its own filters and pager) |
| Discover — HTTP / … | discover-<transport> | The routing table of one transport, one view per registered route source |
| Schemas — TypeORM / … | the global panel's name | One view per installed ORM schema collector |
| Runtime | runtime | Process memory, CPU, event-loop lag and GC, sampled on an interval |
| Config | config | The remaining global-scope collectors, ungrouped |
Both sidebars — the home page's views and a profile's tabs — are the same component: the same group headings, the same item padding and the same count-badge scale, with the active item's badge picking up the accent. Each item carries its subject's glyph, and a protocol keeps one glyph wherever it is named: the same globe on Profiling / HTTP and Discover / HTTP, the same GraphQL mark on Profiling / GraphQL and Discover / GraphQL. A section that registers no icon keeps its label aligned with the others.
Views whose collector declares a group (Discover, Schemas) sit under that heading in the sidebar and carry a short label — the panel header restates the group, so Discover / HTTP stays unambiguous. A Discover key is prefixed on purpose: ?view=graphql is the GraphQL profile list, ?view=discover-graphql its routing table.
Most sidebar items carry a count badge: a list section shows its unfiltered profile total, and a global panel shows its own count (the first *Count field its data exposes, e.g. routeCount). A view that counts nothing — Runtime describes a process, it does not enumerate anything — carries none, since a badge reads as a quantity. The ?view= parameter coexists with the list filters, so a filtered link keeps its view: GET /_profiler?view=http&http_method=POST.
Process-wide memory and CPU are a view of their own — Runtime, in the sidebar — rather than a strip above every list. A trend needs an axis made of time, not of traffic: sampling once per profiled request makes idle periods vanish and a burst compress, which is the opposite of what you read a trend for. See CPU and memory.
Every list opens on the same two columns — Time then Duration — before the columns specific to its kind (the collector tables of a profile's detail page follow that same order), and the row itself is the link: clicking anywhere on it opens the profile (ctrl/cmd or middle click opens it in a new tab, Enter follows a focused row). The token is not a column: it identifies the profile in the URL, in the X-Debug-Token header and on the detail page, and repeating a truncated copy on every row only pushed the columns that discriminate one execution from another out of the way.
Debug headers
Every non-profiler request receives response headers:
| Header | Value |
|---|---|
X-Debug-Token | The request token (UUID v4) |
X-Debug-Token-Link | Link to /_profiler/{token} |
List filters
Each list (HTTP, GraphQL, Commands…) has its own filter bar and is filtered
independently, so query parameters are namespaced by the section key:
<section>_<filter>. The HTTP list, for example, uses http_method, http_status…
GET /_profiler?http_method=GET&http_minDuration=100&http_q=/api&http_statusClass=2The universal filters (available on every list) are:
| Parameter | Description |
|---|---|
q | Search across URL, GraphQL operation name and command name |
status | Exact response status code |
statusClass | Status class: 2, 3, 4 or 5 (matches 2xx…5xx) |
minDuration | Minimum duration in ms |
maxDuration | Maximum duration in ms |
tag | Keep only profiles carrying a performance tag (slow, n-plus-one, chatty, large-payload, zero-rows) — see Performance tags |
exception | Keep only profiles whose captured failure is of this type — an exception class (NotFoundException) or, for GraphQL, an error code (BAD_USER_INPUT) |
error | Checkbox — keep only profiles that failed, per each kind's error classification |
exception and error answer different questions and are meant to be used
together. error asks "what failed?" — a verdict you configure, which by
default does not count a 404 even though its NotFoundException was
captured. exception asks "show me the NotFoundExceptions", regardless of
whether they count as failures. Its options are not a fixed list: they are the
values actually present in your store, so each list offers only what it has
really seen.
Each entrypoint kind also contributes scoped filters, shown only above its own
list — e.g. method (HTTP), operationType (GraphQL, via
@eleven-labs/nest-profiler-graphql), commandStatus (Commands). A scoped filter
is namespaced like any other: graphql_operationType=mutation.
A kind may also hide a universal filter that is redundant on its own list: the
Commands list has no error checkbox, since its commandStatus filter
(Success/Failed) already asks exactly that.
Custom list filters
Filters are pluggable. A filter is a ProfilerListFilter — it describes its own
control, parses its raw query value and decides whether a profile matches:
import { ProfilerCoreService, ProfilerListFilter } from '@eleven-labs/nest-profiler';
const slowFilter: ProfilerListFilter<boolean> = {
key: 'slow',
label: 'Slow only',
control: 'checkbox',
// Checked boxes submit '1'; undefined keeps the filter inactive.
parse: (raw) => (raw ? true : undefined),
matches: (profile) => (profile.performance.duration ?? 0) >= 500,
};Register it from a module's onModuleInit (the cross-module path, robust to
import order):
core.registerListFilter(slowFilter); // core: ProfilerCoreServiceor declaratively via the PROFILER_LIST_FILTERS multi-token:
{ provide: PROFILER_LIST_FILTERS, useValue: slowFilter, multi: true }Pagination
Each list paginates independently to keep the page light when many profiles are
captured. A section shows listPageSize profiles per page (default 25,
configurable via ProfilerModule.forRoot({ listPageSize })) with a
Previous/Next pager; the pager is hidden when a section fits on one page.
The current page is carried as a section-namespaced query parameter,
<section>_page, so sections page independently — and pager links preserve the
active filters and the other sections' pages:
GET /_profiler?http_page=2&graphql_page=3&http_status=200Page numbers are 1-based and clamped to the available range. Submitting a filter bar resets every section back to page 1, since the result set changed.
Exceptions and their causes
The Exceptions tab shows every failure captured for a profile: the ones a route handler threw, the ones a guard or a pipe threw before the interceptor ran, and — for GraphQL — the errors returned inside a 200 envelope.
Each exception also carries its cause chain. throw new InternalServerErrorException('...', { cause: err }) is how a layered application reports a failure, and the cause is usually the half that says what went wrong: the outer exception is what the client is told, the ECONNREFUSED or QueryFailedError underneath is the diagnosis. The tab renders the chain under the exception, one Caused by block per level, each with its own message, code and stack.
The chain is followed to a bounded depth and is cycle-safe, so a wrapped error cannot bloat a stored profile. A machine-readable code is captured whenever the error carries one — Node's ENOENT/ECONNREFUSED, a driver's own code, a GraphQL extensions.code — and it is what the exception list filter groups by, since a class name like GraphQLError discriminates nothing.
Export a profile
Every profile detail page has an Export JSON button. You can also download the raw profile directly:
curl http://localhost:3000/_profiler/{token}/data > profile.jsonCopying requests & queries
Inspired by the Symfony Web Profiler, the detail page offers one-click copy buttons that turn a captured operation into something you can paste straight into a terminal or REPL:
| Panel | Button | What you get |
|---|---|---|
| Request | Copy as cURL | A runnable curl command for the incoming request (method, absolute URL, headers, body) |
| HTTP Client | Copy as cURL | The same, for each outgoing request the handler made |
| SQL (TypeORM / MikroORM) | Copy SQL | The query with its bound parameters inlined, ready to run in a SQL client |
| MongoDB | Copy query | A mongosh command — db.<collection>.<op>(<filter>) or db.<collection>.aggregate([…]) |
| RabbitMQ | Copy payload / Copy publish | The decoded message payload, and an amqplib channel.publish(…) snippet that re-emits it |
Header values that the profiler masks at capture time (e.g. authorization) stay masked in the copied command — the feature is for replaying requests during development, not for exfiltrating secrets.
The cURL and SQL builders are also exported for programmatic use:
import { buildCurlCommand, interpolateSql } from '@eleven-labs/nest-profiler';
buildCurlCommand({
method: 'POST',
url: '/users',
headers: { host: 'localhost:3000' },
body: { name: 'Ada' },
});
interpolateSql('SELECT * FROM "user" WHERE id = $1', [42]); // → SELECT * FROM "user" WHERE id = 42Visual tour — the Profiler UI page walks through the profiles list, every built-in tab and every collector panel with screenshots.