npm package: @jstverify/tracing
Description: JstVerify Node.js Tracing SDK — distributed tracing for Node.js backends
Latest version: 0.7.0

# @jstverify/tracing

Node.js distributed tracing SDK for [JstVerify](https://jstverify.com) application mapping. Auto-instruments your backend to produce trace spans that connect with the JstVerify JavaScript SDK, giving you a full frontend-to-backend service map.

Zero runtime dependencies. Supports Node.js 18+.

## Installation

```bash
npm install @jstverify/tracing
```

## Quick Start

### 1. Initialize (once at startup)

```typescript
import { init } from "@jstverify/tracing";

init({
  apiKey: "your-sdk-key",
  serviceName: "my-backend",
});
```

> The endpoint defaults to the production ingestion URL (`https://sdkapi.jstverify.com/v1/tracing/spans`). Override for dev environments:
>
> ```typescript
> init({
>   apiKey: "your-sdk-key",
>   endpoint: "https://sdkapi.dev.jstverify.com/v1/tracing/spans",
>   serviceName: "my-backend",
> });
> ```

### 2. Add Framework Middleware

**Express:**

```typescript
import { tracingMiddleware } from "@jstverify/tracing";

app.use(tracingMiddleware({ gatewayName: "my-api-gateway" }));
```

**Fastify:**

```typescript
import { fastifyTracingPlugin } from "@jstverify/tracing";

fastify.register(fastifyTracingPlugin, { gatewayName: "my-api-gateway" });
```

**Koa:**

```typescript
import { koaTracingMiddleware } from "@jstverify/tracing";

app.use(koaTracingMiddleware({ gatewayName: "my-api-gateway" }));
```

**Hapi:**

```typescript
import { hapiTracingPlugin } from "@jstverify/tracing";

await server.register({ plugin: hapiTracingPlugin, options: { gatewayName: "my-api-gateway" } });
```

**AWS Lambda (API Gateway):**

```typescript
import { JstVerifyTracingMiddleware } from "@jstverify/tracing";

export const handler = JstVerifyTracingMiddleware(async (event, context) => {
  return { statusCode: 200, body: "ok" };
});
```

For backend event sources (SQS, SNS, EventBridge):

```typescript
export const handler = JstVerifyTracingMiddleware({ backendTrace: true })(
  async (event, context) => {
    // process SQS messages, SNS notifications, etc.
  },
);
```

**Express / Fastify on AWS Lambda (via @vendia/serverless-express, etc.):**

When running a framework app on Lambda, use the framework middleware above for tracing — but you **must** also call `flush()` at the end of each request. The background flush timer uses `setInterval.unref()` which won't fire between Lambda invocations.

```typescript
import { flush } from "@jstverify/tracing";

app.get("/api/example", async (req, res) => {
  try {
    res.json({ ok: true });
  } finally {
    await flush();
  }
});
```

> **Note:** The `JstVerifyTracingMiddleware` Lambda wrapper handles flushing automatically — this manual flush is only needed when using framework middleware on Lambda.

**AWS AppSync Lambda Resolver:**

```typescript
import { JstVerifyAppSyncMiddleware } from "@jstverify/tracing";

export const handler = JstVerifyAppSyncMiddleware(async (event, context) => {
  return [{ id: "1", name: "Alice" }];
});
```

The AppSync middleware extracts trace context from `event.request.headers` or `event.stash` (pipeline resolvers) and derives the operation name from `event.info.parentTypeName` and `event.info.fieldName` (e.g. `Query.listUsers`). Only direct transport mode is supported — relay mode is not available for AppSync since GraphQL responses cannot carry custom HTTP headers.

### 3. Manual Instrumentation (optional)

```typescript
import { trace, traceSpan, SpanHandle } from "@jstverify/tracing";

// Decorator-style wrapping
const processPayment = trace("process-payment")(async (orderId: string) => {
  // ...
});

// Callback-based spans
async function handleOrder(orderId: string) {
  const result = await traceSpan("validate-order", async (span: SpanHandle) => {
    // ...
    span.setStatus(200);
    return validated;
  });

  await traceSpan("charge-card", async (span: SpanHandle) => {
    // ...
    span.setHttpMetadata({ method: "POST", url: "/payments/charge", statusCode: 201 });
  });
}
```

### 4. Custom Attributes

Attach custom key-value metadata to any span. Values are coerced to strings.

**Via `trace()` options:**

```typescript
const processPayment = trace("process-payment", {
  attributes: { orderId: "ORD-123", amount: 49.99, express: true },
})(async (orderId: string) => {
  // ...
});
```

**Via `SpanHandle.setAttribute()`:**

```typescript
await traceSpan("validate-order", async (span: SpanHandle) => {
  span.setAttribute("orderId", orderId);
  span.setAttribute("itemCount", items.length);
  // ...
});
```

When both options and handle set the same key, the handle value wins.

### 5. DynamoDB Tracing

Use the `traceDynamodb()` helper to wrap individual DynamoDB operations:

```typescript
import { traceDynamodb } from "@jstverify/tracing";

// Wraps any async operation with a DynamoDB-typed span
const result = await traceDynamodb("GetItem", "users-table", async () => {
  return client.send(new GetItemCommand({ TableName: "users-table", Key: { id: { S: "123" } } }));
});

const items = await traceDynamodb("Query", "orders-table", async () => {
  return client.send(new QueryCommand({ TableName: "orders-table", ... }));
});
```

Each call creates a child span with the operation name (e.g. `GetItem users-table`) and `serviceType: "dynamodb"`.

### 6. Auto-Patching

**Outgoing HTTP (fetch):**

```typescript
import { patchFetch } from "@jstverify/tracing";

patchFetch();
// All subsequent fetch() calls are automatically traced with headers injected
```

**AWS SDK v3:**

```typescript
import { createAwsSdkTracingMiddleware } from "@jstverify/tracing";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";

const client = new DynamoDBClient({});
client.middlewareStack.use(createAwsSdkTracingMiddleware());
```

### 7. Trace Context Propagation

For SQS and EventBridge, use the propagation helpers to forward trace context:

```typescript
import { propagation } from "@jstverify/tracing";

// SQS — add as MessageAttributes
const attrs = propagation.sqsMessageAttributes();
await sqs.send(new SendMessageCommand({
  QueueUrl: "...",
  MessageBody: "...",
  MessageAttributes: attrs,
}));

// EventBridge — merge into detail
const traceCtx = propagation.eventbridgeTraceContext();
await events.send(new PutEventsCommand({
  Entries: [{ Source: "my-app", DetailType: "OrderPlaced", Detail: JSON.stringify({ ...payload, ...traceCtx }) }],
}));
```

### 8. Shutdown

```typescript
import { shutdown } from "@jstverify/tracing";

await shutdown();
```

Shutdown is also registered via `process.on('beforeExit')` automatically.

## Configuration Options

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `apiKey` | string | required | Your JstVerify SDK API key |
| `endpoint` | string | production URL | Span ingestion endpoint URL (defaults to `https://sdkapi.jstverify.com/v1/tracing/spans`) |
| `serviceName` | string | required | Service name shown in the service map |
| `serviceType` | string | `"http"` | Service type identifier (auto-detects `"lambda"` in Lambda environments) |
| `gatewayName` | string | — | If set, framework middleware synthesizes an API Gateway parent span |
| `transport` | string | `"direct"` | `"direct"` sends spans via HTTP; `"relay"` encodes spans into response headers |
| `flushInterval` | number | `5` | Seconds between background flushes |
| `maxQueueSize` | number | `200` | Max buffered spans (circular buffer) |
| `maxBatchSize` | number | `50` | Max spans per API request |
| `debug` | boolean | `false` | Enable debug logging |
| `sanitizePii` | boolean | `true` | Scrub PII (SSN, email, phone, credit card) from span data |
| `piiPatterns` | string[] | `[]` | Additional regex patterns for PII scrubbing |
| `piiRedaction` | string | `"[REDACTED]"` | Replacement string for redacted PII |

## How It Works

### Direct Mode (default)

1. The middleware reads `X-JstVerify-Trace-Id` and `X-JstVerify-Parent-Span-Id` headers from incoming requests (injected by the JS SDK).
2. A root span is created for each request, with nested child spans for `trace()` wrapped functions and `traceSpan()` callbacks.
3. If `patchFetch()` is called, outgoing `fetch` calls are automatically instrumented — trace headers are injected so downstream services can continue the trace.
4. Spans are buffered in a queue and flushed to the JstVerify API in batches by a background `setInterval` timer (`.unref()`'d so it doesn't block process exit).

### Relay Mode

For backends without outbound internet access (private VPC, strict firewalls), relay mode encodes spans into the `X-JstVerify-Spans` response header. The JstVerify JS SDK reads this header and relays the spans to the ingestion API on behalf of the backend.

```typescript
init({
  apiKey: "your-sdk-key",
  serviceName: "my-backend",
  transport: "relay", // No endpoint needed
});
```

**How it works:**

1. Each request collects spans in a per-request buffer (async-safe via `AsyncLocalStorage`).
2. When the response is sent, all spans are base64url-encoded into the `X-JstVerify-Spans` header.
3. The JS SDK decodes the header and merges the spans into its own flush queue.

**Limitations:**

- ~20-30 spans per response max due to the 7500-byte header size limit.
- Only works for request-response flows — async background jobs have no response to carry spans.
- Cross-origin requests require the `Access-Control-Expose-Headers` header (set automatically by the middleware).

## License

MIT