NestJS Profiler
Tutorials

Configuration profiling

Inspect your application's resolved configuration values in the Config panel, with sensitive key masking.

This tutorial shows how to add the config collector to display resolved configuration values in the profiler's Config panel.

Prerequisites

  • @eleven-labs/nest-profiler installed and configured
  • @nestjs/config installed with load factories (required - see note below)

Load factories are required

The config collector reads ConfigService's internal config map. This map is only populated when you use the load option with factory functions (the registerAs pattern). If you only pass envFilePath or ignoreEnvFile, the internal config will be empty and the Config tab will show no entries.

Step 1 - Install the package

pnpm add @eleven-labs/nest-profiler-config@alpha

Step 2 - Define config factories with registerAs

Create one factory per configuration domain:

config/database.config.ts
import { registerAs } from '@nestjs/config';

export const databaseConfig = registerAs('database', () => ({
  host: process.env.DB_HOST ?? 'localhost',
  port: parseInt(process.env.DB_PORT ?? '5432', 10),
  name: process.env.DB_NAME ?? 'myapp',
  username: process.env.DB_USER ?? 'postgres',
  password: process.env.DB_PASSWORD ?? '',
}));
config/app.config.ts
import { registerAs } from '@nestjs/config';

export const appConfig = registerAs('app', () => ({
  port: parseInt(process.env.PORT ?? '3000', 10),
  env: process.env.NODE_ENV ?? 'development',
  debug: process.env.DEBUG === 'true',
}));

Step 3 - Register the modules

app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConditionalModule } from '@nestjs/config';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { ConfigCollectorModule } from '@eleven-labs/nest-profiler-config';
import { appConfig } from './config/app.config';
import { databaseConfig } from './config/database.config';

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

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [appConfig, databaseConfig],
    }),
    ConditionalModule.registerWhen(ProfilerModule.forRoot({ isGlobal: true }), isProfilerEnabled),
    ConditionalModule.registerWhen(
      ConfigCollectorModule.forRoot({ maskKeys: ['database.password'] }),
      isProfilerEnabled,
    ),
  ],
})
export class AppModule {}

The maskKeys option accepts dot-notation paths. Matched values are replaced with *** in the profiler UI.

Step 4 - Test it

Start your application and open any profile:

curl -i http://localhost:3000/health

Open /_profiler/{token} and click the Config tab.

You will see the configuration split into one collapsible section per namespace, with keys shown relative to their namespace:

app — 3 keys

KeyValue
port3000
envdevelopment
debugfalse

database — 5 keys

KeyValue
hostlocalhost
port5432
namemyapp
usernamepostgres
password***

Top-level scalar values that don't belong to a namespace appear under a General section. The toolbar badge shows the total number of resolved configuration keys (e.g., 8 keys).

forFeature is supported too

A namespace loaded lazily with ConfigModule.forFeature(databaseConfig) in a feature module is captured exactly like one passed to forRoot({load}) — both merge into the same config store. Grouping is by namespace, not by how it was loaded.

Masking additional keys

Any key matching the maskKeys array is masked. Keys are matched by their full dot-notation path:

ConfigCollectorModule.forRoot({
  maskKeys: ['database.password', 'redis.password', 'jwt.secret'],
});

Values matching /password|secret|key|token|credential/i are also masked automatically regardless of maskKeys.

Driving options from ConfigService

Use forRootAsync() to resolve maskKeys from ConfigService (or any provider), gated per environment with ConditionalModule — the recommended way (for the enabled flag, see Enabling and disabling the profiler):

import { ConditionalModule, ConfigService } from '@nestjs/config';

ConditionalModule.registerWhen(
  ConfigCollectorModule.forRootAsync({
    inject: [ConfigService],
    useFactory: (config: ConfigService) => ({
      maskKeys: config.get<string[]>('profiler.maskKeys') ?? ['database.password', 'jwt.secret'],
    }),
  }),
  isProfilerEnabled,
);

How it works

The collector reads ConfigService's internal config once at application bootstrap, groups it by namespace (with keys flattened relative to each namespace, e.g. pool.max) and applies the masking rules; every profile then shows that same snapshot. Masking still matches the fully-qualified path, so maskKeys: ['database.password'] works even though the key is displayed as password. See How it works on the package page.

Powered & maintained by

On this page