NestJS Profiler
Packages

nest-profiler-event-emitter

Profile the domain events dispatched through @nestjs/event-emitter, and every @OnEvent handler execution.

@eleven-labs/nest-profiler-event-emitter

@eleven-labs/nest-profiler-event-emitter captures the domain events an application dispatches through @nestjs/event-emitter. Every emission shows up in an Events panel on the profile that published it, every @OnEvent subscription is listed in the Discover / Events view, and — by default — each handler execution becomes a profile of its own, so the work an event triggers stops being invisible.

Installation

pnpm add @eleven-labs/nest-profiler-event-emitter @nestjs/event-emitter

Peer dependencies: @nestjs/event-emitter ^3.0.0, nestjs-cls ^6.0.0

Setup

app.module.ts
import { Module } from '@nestjs/common';
import { ConditionalModule } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { EventEmitterCollectorModule } from '@eleven-labs/nest-profiler-event-emitter';

const isProfilerEnabled = (env: NodeJS.ProcessEnv) => env['PROFILER_ENABLED'] === 'true';

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    ConditionalModule.registerWhen(EventEmitterCollectorModule.forRoot(), isProfilerEnabled),
  ],
})
export class AppModule {}

Enabling / disabling — gate the collector with ConditionalModule.registerWhen(..., isProfilerEnabled) as shown, so it loads only when PROFILER_ENABLED is on. Wire the core ProfilerModule once at the root — the recommended setup bundles the root-level profiler modules into a single ProfilingModule behind a ConditionalModule gate (see Enabling and disabling the profiler and the example app). A top-level enabled option is also supported as an alternative.

Place the collector wherever EventEmitterModule is registered — it is infra-scoped, like the cache and messaging collectors, not a root-level panel.

What it collects

1. The Events panel — what a profile emitted

One row per emit / emitAsync call made during the profiled execution:

FieldDescription
eventEmitted event name (a namespaced array is joined with dots)
payloadRedacted and size-bounded emitted value; undefined when capture is off
listenerCountListeners subscribed at emit time — 0 is highlighted as a likely mistake
durationSynchronous dispatch for emit; every awaited handler for emitAsync
asynctrue when emitted through emitAsync
startedAtUnix timestamp
errorMessage of an error thrown by emit or rejected by an awaited handler

Entries feed the core performance-rule engine under the event tag domain, so a slow emission is tagged slow, a repeated one n-plus-one, and a request emitting a lot of them chatty — with the thresholds below.

2. The event entrypoint — what a listener did

Unless profileListeners: false, every @OnEvent execution gets its own profile, carrying the logs, SQL queries and outgoing HTTP calls that ran inside the handler. They render in a dedicated Events list table (?view=event), with Status and Event filters and an Event detail tab showing the handler, the received payload and any error.

3. The Discover / Events view — what subscribes to what

With @eleven-labs/nest-profiler-routes installed, the module also contributes a Discover / Events view listing every @OnEvent subscription discovered across providers and controllers, as ON <event> → <Provider>.<method>() — each expanding to the options it registered with (async, prependListener).

Options

OptionDefaultDescription
enabledtrueBuild-time switch; false registers no provider at all
capturePayloadtrueCapture the (redacted) emitted value — turn off when payloads may hold PII
maxPayloadLength2000Max length of the stringified payload kept per event
ignoreEvents[]Event names never recorded; strings match exactly, RegExps are tested against the name
emitterTokenDI token of the EventEmitter2 to patch, when it is not the default class
profileListenerstrueGive each @OnEvent execution its own profile
slowThreshold100An emission at or above this duration (ms) is tagged slow
nPlusOneThreshold2This many identical event names or more tags the profile n-plus-one
chattyThreshold20At or above this many emissions, the profile is tagged chatty
slowSeveritywarningSeverity of the slow tag
errorWhat counts as a failed handler execution (ProfilerErrorOptions)

newListener and removeListener — EventEmitter2's own subscription bookkeeping — are always ignored.

Toolbar badge

The number of events emitted during the execution (e.g. 3), hidden when none. The tab turns amber or red when the emissions carry slow / error tags.

How it works

At module initialization the collector resolves the app's EventEmitter2 through ModuleRef and wraps its emit and emitAsync. The active Profile is read synchronously at emit entry, the one point where the request's nestjs-cls context is guaranteed active; emitAsync is then timed out-of-band by observing the returned promise, so the caller still gets it untouched. Both methods are restored on shutdown.

For listener profiling, the collector scans the DI container at bootstrap for @OnEvent methods and replaces each one on its owning instance with a wrapper that opens a fresh CLS branch, runs the handler, then collects and persists the resulting profile. @nestjs/event-emitter resolves instance[method] at emit time, so the wrapping takes effect regardless of hook order; the handler's decorator metadata is copied onto the wrapper so the loader still recognises it as a listener.

Everything degrades to a no-op when the profiler core is absent or disabled, and the app boots unchanged when @nestjs/event-emitter is not registered.

Limitations

  • Request-scoped subscribers are not profiled. @nestjs/event-emitter resolves a fresh instance per event through Injector.loadPerContext, so there is no stable handler to wrap. They still appear in the Discover / Events view.
  • EventEntry.error is rarely populated. @OnEvent defaults to suppressErrors: true, so a throwing handler is logged by @nestjs/event-emitter and never surfaces to the emitter. Subscribe with { suppressErrors: false } to see handler failures on the emitting profile — the handler's own event profile records the failure either way.
  • An emitAsync the caller never awaits may miss the panel. The entry is recorded when the returned promise settles, which for a fire-and-forget emitAsync can be after the request finished and its profile was already collected and stored. The emission is then written to a profile nobody reads again. Await the promise — or use emit — if the call must appear in the Events panel. The handlers themselves are unaffected: each still produces its own event profile.
  • A method carrying several @OnEvent decorators is filed under all of them. @nestjs/event-emitter registers the handler as (...args) => instance[method](...args), so the handler is never told which subscription fired. Its profiles are named review.archived, review.deleted rather than picking one and being wrong half the time. Split the method in two if you need them apart.
Powered & maintained by

On this page