Domain event profiling
Profile the events dispatched through @nestjs/event-emitter and see what each @OnEvent handler actually did.
This tutorial shows how to profile the domain events an application dispatches with @nestjs/event-emitter. Events are the classic blind spot of a request trace: the controller answers, and whatever the listeners did happens somewhere off to the side. This collector closes that gap on three fronts — an Events panel on the emitting profile, a Discover / Events view listing your subscriptions, and one profile per @OnEvent execution.
Prerequisites
@eleven-labs/nest-profilerinstalled and configured@nestjs/event-emitterregistered withEventEmitterModule.forRoot()
Step 1 - Install the package
pnpm add -D @eleven-labs/nest-profiler-event-emitterStep 2 - Register the module
EventEmitterModule.forRoot() stays in your own application module. Add EventEmitterCollectorModule to the dev-only profiling bundle — it resolves the app's EventEmitter2 and discovers every @OnEvent handler across the application:
import { Module } from '@nestjs/common';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { EventEmitterCollectorModule } from '@eleven-labs/nest-profiler-event-emitter';
@Module({
imports: [ProfilerModule.forRoot({ isGlobal: true }), EventEmitterCollectorModule.forRoot()],
})
export class ProfilingModule {}
ProfilingModuleis the dev-only bundle loaded bymain-dev.ts— see Enabling and disabling the profiler. If the profiler is installed as a production dependency behind the runtime gate, wrap the same call inConditionalModule.registerWhen(..., isProfilerEnabled).
Step 3 - Keep your emitters and listeners unchanged
Nothing about your code changes. Emit as usual:
await this.emitter.emitAsync('product.created', { id: product.id, name: product.name });…and subscribe as usual:
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class NotificationListener {
@OnEvent('product.created')
async onProductCreated(payload: { id: number; name: string }): Promise<void> {
// … send the notification …
}
}Step 4 - Inspect the emission
Trigger the flow and open /_profiler. On the profile that published the event, the Events panel lists each emission with:
- the event name, and an
asyncchip when it went throughemitAsync - how many listeners were subscribed —
0 · no listenerin amber, the cheapest way to catch a typo in an event name - the duration (the awaited handlers for
emitAsync, the synchronous dispatch foremit) - the redacted payload, expandable per row
Step 5 - Inspect the handler
Pick Events in the sidebar (?view=event): each @OnEvent execution has its own profile, listed with its status and duration. Open one to find:
- Event — the event, the
Provider.method()that handled it, the received payload, and the error when it failed - Logs, Database, HTTP Client, … — everything the handler itself did, no longer buried in the publishing request
With @eleven-labs/nest-profiler-routes installed, the sidebar also gains a Discover / Events view listing every subscription as ON product.created → NotificationListener.onProductCreated, each expanding to the @OnEvent options it registered with.
Try it in the example app
The in-process emitter is the example API's default messaging adapter, so there is no broker to start — only the catalog's Postgres:
pnpm example:devCreate a product, then open /_profiler:
curl -X POST http://localhost:3000/api/v1/products \
-H 'content-type: application/json' \
-d '{ "name": "Profiled product", "price": 42 }'The POST profile shows product.created in its Events panel, and the Events view lists the NotificationListener.onProductCreated execution it triggered.
Configuration
EventEmitterCollectorModule.forRoot({
capturePayload: true, // redacted payload per emission - disable if payloads hold PII
maxPayloadLength: 2000, // bound the stringified payload
ignoreEvents: ['heartbeat', /^internal\./], // never recorded
profileListeners: true, // one profile per @OnEvent execution
slowThreshold: 100, // an emission at or above this (ms) is tagged `slow`
});Set profileListeners: false to keep only the per-request Events panel — lighter storage, at the cost of losing what the handlers did.
Driving options from ConfigService
Use forRootAsync() in the bundle to resolve the values from ConfigService (or any provider). It resolves option values only: behind the runtime gate, wrap it in ConditionalModule.registerWhen(..., isProfilerEnabled) as usual (see Enabling and disabling the profiler).
import { ConfigService } from '@nestjs/config';
EventEmitterCollectorModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
capturePayload: config.get<boolean>('profiler.capturePayload') ?? true,
slowThreshold: config.get<number>('profiler.performance.slowThreshold'),
}),
});Performance tags
Emissions feed the core rule engine under their own event domain, so the usual tags apply without inheriting the query thresholds: slow past slowThreshold, n-plus-one when the same event name repeats nPlusOneThreshold times in one profile, and chatty past chattyThreshold emissions. See Performance tags.
How it works
The collector wraps the app's EventEmitter2 (emit and emitAsync) and reads the active profile synchronously at emit entry. For listener profiling it scans the DI container at bootstrap — providers and controllers — and wraps each @OnEvent method so the execution runs in its own CLS branch with its own profile. See How it works on the package page, and its Limitations for request-scoped subscribers.