Telemetry · Guides

Node Telemetry

Node telemetry lives under @threadplane/telemetry/node. It's built for package lifecycle hooks and server-side adapters.

import {
  captureRuntimeInstanceCreated,
  captureRuntimeRequestCreated,
  captureStreamStarted,
  captureStreamEnded,
  captureStreamErrored,
  disableTelemetry,
} from '@threadplane/telemetry/node';

#When to call these

These helpers are for server-side runtime and adapter code, not application request handlers. captureRuntimeInstanceCreated() fires when a runtime is constructed; captureStreamStarted()/captureStreamEnded() wrap a model stream. In a typical deployment that's adapter or framework-integration code — your route handlers and business logic don't call them directly.

If you want to opt the whole process out, call disableTelemetry() once at startup, before any capture helper runs. The flag is checked at capture time, but it has to be set first to take effect for the calls you care about.

#Opt out programmatically

Call disableTelemetry() before capture helpers run.

import { disableTelemetry } from '@threadplane/telemetry/node';
 
disableTelemetry();

This sets an in-process flag. It doesn't mutate environment variables.

#Capture runtime lifecycle

await captureRuntimeInstanceCreated({
  transport: 'langgraph',
  provider: 'openai',
  model: 'gpt-4.1',
  angularVersion: '21.1.0',
});

The RuntimeInstanceTelemetry type includes apiKey, but the adapter strips it before sending. Do not pass secrets as telemetry properties anyway.

Use captureRuntimeRequestCreated() when a runtime issues a request. The RuntimeRequestTelemetry type requires transport and requestType; provider and model are optional.

await captureRuntimeRequestCreated({
  transport: 'langgraph',
  requestType: 'stream',
  provider: 'openai',
  model: 'gpt-4.1',
});

#Capture streams

await captureStreamStarted({
  provider: 'openai',
  model: 'gpt-4.1',
});
 
await captureStreamEnded({
  provider: 'openai',
  model: 'gpt-4.1',
  durationMs: 1200,
});
 
await captureStreamErrored({
  provider: 'openai',
  model: 'gpt-4.1',
  error,
});

captureStreamErrored() records an error class. It doesn't send the full error object.

#Ingest and sampling

captureEvent() sends to:

https://threadplane.ai/api/ingest

unless NGAF_TELEMETRY_INGEST_URL is set.

Sampling uses NGAF_TELEMETRY_SAMPLE_RATE. Invalid values fall back to 1. Values are clamped to the range 0 to 1.

Every sent event includes sample_weight.

#Postinstall script

The postinstall entry point reads package name and version from npm lifecycle environment variables or package.json, skips disabled environments, and sends ngaf:postinstall.

It skips local top-level installs by default. Dependency installs under node_modules and global installs can be eligible unless disabled.

When DEBUG includes ngaf:telemetry, ngaf:*, or *, the script prints the payload shape it attempted to send. It prints the normal install telemetry notice only when the ingest endpoint accepted the event.

#Failure behavior

The Node adapter helpers catch errors and return without throwing. captureEvent() returns:

type CaptureResult =
  | { sent: true }
  | { sent: false; reason: 'disabled' | 'sampled' | 'failed' };

Use the result in tests or diagnostics. Don't make application correctness depend on telemetry delivery.

#Asserting the disabled path

Because captureEvent() returns a CaptureResult, you can assert that opting out actually short-circuits delivery. Call disableTelemetry() first, then check the result:

import { describe, expect, it } from 'vitest';
import { captureEvent, disableTelemetry } from '@threadplane/telemetry/node';
 
describe('telemetry opt-out', () => {
  it('does not send when disabled', async () => {
    disableTelemetry();
 
    const result = await captureEvent('ngaf:runtime_instance_created', { transport: 'langgraph' });
 
    expect(result).toEqual({ sent: false, reason: 'disabled' });
  });
});

disableTelemetry() sets a process-wide flag, so a test that asserts the enabled path must run in a process where it was never called.