npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hell-factory/trace-reaper

v2.0.1

Published

OpenTelemetry span decorator for NestJS by Hell Factory

Downloads

569

Readme

🪓 trace-reaper

Distributed tracing decorator for NestJS (HTTP + Microservices) powered by OpenTelemetry — brought to you by Hell Factory 🔥

CI npm


✨ Features

  • @Span() decorator to auto-create spans for methods (sync and async preserved)
  • TraceInterceptor for HTTP and microservice handlers
  • ReaperClientProxy — drop-in ClientProxy that injects W3C trace context into RPC payloads
  • ✅ Automatic context extraction from incoming headers / payload _headers
  • ✅ Tracer scope @hell-factory/trace-reaper — filter spans cleanly in any OTel backend
  • ✅ Zero business-logic coupling — works alongside any OpenTelemetry SDK setup

📦 Installation

npm install @hell-factory/trace-reaper
# or
pnpm add @hell-factory/trace-reaper

Peer dependencies

{
  "@nestjs/common": "^11.1.19",
  "@nestjs/microservices": "^11.1.19",
  "@opentelemetry/api": "^1.9.0",
  "rxjs": "^7.8.2"
}

⚠️ @nestjs/microservices >= 11.1.19 required (security patch GHSA-hpwf-8g29-85qm).


🚀 Quick start

The library exposes three primitives. Mix and match.

| Primitive | Use it for | |---|---| | @Span() | Wrap any method to record an OTel span | | TraceInterceptor | NestJS interceptor — extracts context from HTTP headers / RPC _headers | | ReaperClientProxy | Replaces ClientProxy to inject context into outgoing RPC calls |


📡 Producer side — ReaperClientProxy

Replace ClientProxyFactory.create(...) with new ReaperClientProxy(...). Every emit / send call automatically injects W3C trace headers into the payload under the _headers field.

import { Module } from '@nestjs/common';
import { Transport } from '@nestjs/microservices';
import { ReaperClientProxy } from '@hell-factory/trace-reaper';

@Module({
  providers: [
    {
      provide: 'TODO_SERVICE',
      useFactory: () =>
        new ReaperClientProxy({
          transport: Transport.RMQ,
          options: {
            urls: ['amqp://localhost:5672'],
            queue: 'todo_queue',
          },
        }),
    },
  ],
})
export class AppModule {}

Outgoing payload shape:

// caller code
await client.emit('todo.created', { id: 'abc', title: 'buy milk' });

// what reaches the broker
{
  id: 'abc',
  title: 'buy milk',
  _headers: { traceparent: '00-...', tracestate: '...' }
}

Primitives, arrays, and null payloads pass through unchanged.


📥 Consumer side

Pick one of the two: TraceInterceptor (recommended) or @Span().

Option A — TraceInterceptor (recommended)

Single registration covers HTTP and RPC handlers.

// main.ts — register globally
import { NestFactory } from '@nestjs/core';
import { TraceInterceptor } from '@hell-factory/trace-reaper';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new TraceInterceptor());
  await app.listen(3000);
}
bootstrap();
// or per-controller
import { UseInterceptors, Controller, Get, Param } from '@nestjs/common';
import { TraceInterceptor } from '@hell-factory/trace-reaper';

@UseInterceptors(TraceInterceptor)
@Controller('user')
export class UserController {
  @Get(':id')
  getUser(@Param('id') id: string) {
    return { id };
  }
}

The interceptor:

  • HTTP — extracts context from req.headers
  • RPC — extracts context from payload._headers, then strips it before the handler sees the payload
  • Span name = <ControllerClass>.<handler>

Option B — @Span() per method

Useful for service-layer methods or when you want explicit span names.

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
import { Span } from '@hell-factory/trace-reaper';

@Controller()
export class HelloController {
  @Get('/hello')
  @Span() // span name defaults to 'getHello'
  getHello(@Req() _req: Request) {
    return 'Hello World!';
  }

  @Get('/hi')
  @Span('greet.hi') // explicit name
  getHi() {
    return 'Hi';
  }
}
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { Span } from '@hell-factory/trace-reaper';

@Controller()
export class UserEvents {
  @MessagePattern('user.created')
  @Span()
  handleEvent(@Payload() payload: any) {
    // payload._headers has been stripped — payload is your data only
    return 'OK';
  }
}

How @Span() extracts context

  1. HTTP-shape arg (firstArg.headers && firstArg.url) → uses firstArg.headers as carrier
  2. RPC-shape arg (firstArg._headers) → uses firstArg._headers, strips it, replaces args[0] with a clean copy (no caller mutation)
  3. Otherwise → falls back to the active context

Sync vs async

@Span() preserves the original return shape:

class S {
  @Span()
  add(a: number, b: number): number { return a + b; }       // returns number

  @Span()
  async fetch(): Promise<User> { return await db.find(); }   // returns Promise<User>
}

The span ends after the synchronous return, the resolved promise, or the thrown error.


🧰 API

Span(options?: string | { name?: string })

Method decorator. Creates a span around the decorated method.

| Argument | Description | |---|---| | string | Use this string as the span name | | { name } | Same, object form | | omitted | Span name = method name |

class TraceInterceptor implements NestInterceptor

Stateless interceptor. Register globally or per controller.

  • HTTP context → extracts from req.headers
  • RPC context → extracts from payload._headers, strips the field
  • Span name = <ControllerClass>.<handlerMethod>
  • Sets SpanStatusCode.OK on success, SpanStatusCode.ERROR + recordException on failure

class ReaperClientProxy extends ClientProxy

Drop-in replacement for the result of ClientProxyFactory.create(options).

| Method | Behavior | |---|---| | emit(pattern, data) | Injects _headers into object payloads, delegates to underlying proxy | | send(pattern, data) | Same as above for request/response | | connect() | Delegates | | close() | Returns Promise<void> \| voidawait it to wait for graceful disconnect |

publish, dispatchEvent, and unwrap are not implemented (NestJS does not invoke them on overridden emit / send paths).

TRACE_HEADERS_FIELD

Constant '_headers' — the payload field used to carry W3C trace context across RPC.

Tracer scope

All spans are emitted under instrumentation scope:

otel.library.name    = "@hell-factory/trace-reaper"
otel.library.version = "<package version>"

Use this to filter spans in your backend.


🛠️ OpenTelemetry SDK setup

trace-reaper only uses the OTel API — your app must register a real SDK.

// tracing.ts — load before AppModule
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'my-service',
  }),
  traceExporter: new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' }),
});

sdk.start();
// main.ts
import './tracing';        // ← must be imported first
import { NestFactory } from '@nestjs/core';
// ...

🔁 Migration from 1.x → 2.x

Version 2.0 is a breaking release. Highlights:

| Change | Impact | |---|---| | @Span() no longer wraps sync methods in a Promise | If you await-ed a sync method, remove the await. Async methods unchanged. | | RPC carrier key is _headers (was sometimes headers) | Producers using ReaperClientProxy already write _headers. If you hand-built RPC payloads with headers, rename to _headers. | | ReaperClientProxy.close() returns Promise<void> \| void | await client.close() for graceful shutdown. Old void callers keep working at runtime; TypeScript strict callers may need a type update. | | Tracer scope default@hell-factory/trace-reaper | Update backend filters / dashboards that filtered by scope.name = "default". | | Peer @nestjs/microservices ^11.1.17^11.1.19 | Security patch (GHSA-hpwf-8g29-85qm). Bump on the consumer side. |


🧪 Verifying trace propagation

End-to-end sanity check:

  1. Service A calls client.send('user.create', { name: 'X' }) via ReaperClientProxy.
  2. Service B receives payload, TraceInterceptor (or @Span()) extracts _headers.
  3. In your backend (Jaeger / Tempo / Honeycomb / Datadog), the spans appear in one trace under scope @hell-factory/trace-reaper.

If the spans show as two separate traces, check:

  • Producer used ReaperClientProxy (not raw ClientProxyFactory)?
  • Consumer registered TraceInterceptor or decorated handler with @Span()?
  • Both services started the OTel SDK before NestJS bootstrapped?

📜 License

MIT © Hell Factory