@pingops/sdk
v0.2.6
Published
PingOps SDK for Node.js
Maintainers
Readme
@pingops/sdk
PingOps SDK for Node.js — Bootstrap OpenTelemetry and capture outgoing HTTP and fetch API calls with minimal code. Built for observability of external API usage, AI/LLM calls, and third-party integrations.
Table of Contents
- Overview
- Installation
- Quick Start
- Configuration
- API Reference
- Tracing
- Filtering & Privacy
- Integration with Existing OpenTelemetry
- What Gets Captured
- Requirements
Overview
The PingOps SDK gives you:
- Automatic instrumentation — Outgoing HTTP (Node.js
httpmodule) andfetch(via Undici) are instrumented without wrapping your code. - Structured traces — Start traces with
userId,sessionId, tags, and metadata so every span is tied to your business context. - Control over what is captured — Domain allow/deny lists, header filtering, and optional request/response body capture with size limits.
- Flexible setup — Use environment variables, a config file (JSON or YAML), or pass config programmatically. Auto-initialize via
--requireor import@pingops/sdk/registerfirst.
You initialize once (at process startup or before any HTTP clients load); after that, outgoing requests are captured and sent to your PingOps backend in batches or immediately.
Installation
pnpm add @pingops/sdkOr with npm:
npm install @pingops/sdkRequirement: Node.js 20 or later (for native fetch and modern APIs).
Quick Start
Option 1: Auto-initialization (recommended)
Best for: Getting started quickly and for production when config comes from environment or a config file.
A. Using Node.js --require (runs before any application code):
node --require @pingops/sdk/register your-app.jsSet required environment variables:
export PINGOPS_API_KEY="your-api-key"
export PINGOPS_BASE_URL="https://api.pingops.com"
export PINGOPS_SERVICE_NAME="my-service"B. Importing the register entry first (must be before any HTTP client imports):
// Must be first — before axios, node-fetch, or any code that makes HTTP requests
import "@pingops/sdk/register";
import axios from "axios";
// ... rest of your applicationWith a config file, set PINGOPS_CONFIG_FILE to the path to your JSON or YAML file; environment variables override values from the file.
Option 2: Manual initialization
Best for: Config from code, feature flags, or when you need to ensure initialization order explicitly.
import { initializePingops } from "@pingops/sdk";
// Before importing or using any HTTP clients
initializePingops({
apiKey: process.env.PINGOPS_API_KEY,
baseUrl: "https://api.pingops.com",
serviceName: "my-service",
});
import axios from "axios";
// ... rest of your applicationYou can also initialize from a config file path (environment variables still override file values):
initializePingops("./pingops.config.yaml");
// or
initializePingops({ configFile: "./pingops.config.json" });Important: Call initializePingops before any HTTP client is loaded or used so instrumentation is applied correctly.
Configuration
Configuration can be provided via:
- Programmatic config — Object passed to
initializePingops(...) - Config file — Path as first argument or
{ configFile: "path" }; supports JSON and YAML - Environment variables — Always override file and can supply all required fields for auto-init
Required fields
| Field | Env var | Description |
| ------------- | ---------------------- | ------------------------- |
| baseUrl | PINGOPS_BASE_URL | PingOps backend base URL |
| serviceName | PINGOPS_SERVICE_NAME | Service name for resource |
apiKey is optional at config level; if your backend requires it, set apiKey or PINGOPS_API_KEY.
Full configuration reference
| Option | Type | Default | Description |
| --------------------- | ---------------------------- | ------------ | ---------------------------------------------- |
| apiKey | string | — | API key (or PINGOPS_API_KEY) |
| baseUrl | string | required | Backend base URL |
| serviceName | string | required | Service name |
| debug | boolean | false | Enable debug logs (PINGOPS_DEBUG=true) |
| headersAllowList | string[] | — | Headers to include (case-insensitive) |
| headersDenyList | string[] | — | Headers to exclude (overrides allow) |
| captureRequestBody | boolean | false | Capture request bodies (global) |
| captureResponseBody | boolean | false | Capture response bodies (global) |
| maxRequestBodySize | number | 4096 | Max request body size in bytes |
| maxResponseBodySize | number | 4096 | Max response body size in bytes |
| domainAllowList | DomainRule[] | — | Domains (and optional rules) to allow |
| domainDenyList | DomainRule[] | — | Domains to exclude |
| headerRedaction | HeaderRedactionConfig | — | Custom header redaction |
| batchSize | number | 50 | Spans per batch (PINGOPS_BATCH_SIZE) |
| batchTimeout | number | 5000 | Flush interval in ms (PINGOPS_BATCH_TIMEOUT) |
| exportMode | "batched" | "immediate" | "batched" | PINGOPS_EXPORT_MODE |
Config file path: Set PINGOPS_CONFIG_FILE to the path of your JSON or YAML file when using the register entry.
Export mode:
batched— Best for long-running processes; spans are sent in batches (default).immediate— Best for serverless/short-lived processes; each span is sent as it finishes to reduce loss on freeze/exit.
Config file examples
JSON (pingops.config.json):
{
"apiKey": "your-api-key",
"baseUrl": "https://api.pingops.com",
"serviceName": "my-service",
"debug": false,
"exportMode": "batched",
"batchSize": 50,
"batchTimeout": 5000,
"captureRequestBody": false,
"captureResponseBody": false
}YAML (pingops.config.yaml):
apiKey: your-api-key
baseUrl: https://api.pingops.com
serviceName: my-service
debug: false
exportMode: batched
batchSize: 50
batchTimeout: 5000API Reference
initializePingops(config)
Initializes the PingOps SDK: sets up OpenTelemetry NodeSDK, registers the PingOps span processor, and enables HTTP and Undici (fetch) instrumentation.
Overloads:
initializePingops(config: PingopsProcessorConfig): voidinitializePingops(configFilePath: string): voidinitializePingops({ configFile: string }): void
Example:
import { initializePingops } from "@pingops/sdk";
initializePingops({
baseUrl: "https://api.pingops.com",
serviceName: "my-service",
apiKey: process.env.PINGOPS_API_KEY,
exportMode: "immediate", // e.g. for serverless
});Calling initializePingops again after the first successful call is a no-op (idempotent).
shutdownPingops()
Gracefully shuts down the SDK and flushes remaining spans. Returns a Promise<void>.
Example:
import { shutdownPingops } from "@pingops/sdk";
process.on("SIGTERM", async () => {
await shutdownPingops();
process.exit(0);
});startTrace(options, fn)
Starts a new trace, sets PingOps attributes (e.g. userId, sessionId, tags, metadata) in context, runs the given function inside that context, and returns the function’s result. Any spans created inside the function (including automatic HTTP/fetch spans) are part of this trace and carry the same context.
If startTrace is called while the current OpenTelemetry context is tracing-suppressed, the SDK now starts the trace from a clean unsuppressed base context. This prevents leaked suppression flags from silently disabling outbound instrumentation for user traffic.
When using initializePingops without startTrace, SDK instrumentations also guard outbound user requests against leaked suppression context. Exporter traffic remains suppressed to avoid self-instrumentation recursion.
Parameters:
options.attributes— Optional PingopsTraceAttributes to attach to the trace and propagate to spans.options.seed— Optional string; when provided, a deterministic trace ID is derived from it (useful for idempotency or correlation with external systems).fn—() => T | Promise<T>. Your code; runs inside the new trace and attribute context.
Returns: Promise<T> — The result of fn.
Example:
import { startTrace, initializePingops } from "@pingops/sdk";
initializePingops({ baseUrl: "...", serviceName: "my-api" });
const data = await startTrace(
{
attributes: {
userId: "user-123",
sessionId: "sess-456",
tags: ["checkout", "v2"],
metadata: { plan: "pro", region: "us" },
captureRequestBody: true,
captureResponseBody: true,
},
seed: "order-789", // optional: stable trace ID for this order
},
async () => {
const res = await fetch("https://api.stripe.com/v1/charges", { ... });
return res.json();
}
);runUnsuppressed(fn)
Runs fn in a clean unsuppressed OpenTelemetry context. Use this at async task/job boundaries when you are not using startTrace, but still need outbound HTTP/fetch instrumentation to capture spans.
Example:
import { runUnsuppressed } from "@pingops/sdk";
await runUnsuppressed(async () => {
await fetch("https://api.example.com/work");
});This helper is scoped to the callback only and does not globally disable OpenTelemetry suppression.
getActiveTraceId()
Returns the trace ID of the currently active span, or undefined if there is none.
Example:
import { getActiveTraceId } from "@pingops/sdk";
const traceId = getActiveTraceId();
console.log("Current trace:", traceId);getActiveSpanId()
Returns the span ID of the currently active span, or undefined if there is none.
Example:
import { getActiveSpanId } from "@pingops/sdk";
const spanId = getActiveSpanId();PingopsTraceAttributes
Type for attributes you can pass into startTrace({ attributes }):
| Field | Type | Description |
| --------------------- | ------------------------ | --------------------------------------------------------------------- |
| traceId | string | Override trace ID (otherwise one is generated or derived from seed) |
| userId | string | User identifier |
| sessionId | string | Session identifier |
| tags | string[] | Tags for the trace |
| metadata | Record<string, string> | Key-value metadata |
| captureRequestBody | boolean | Override request body capture for spans in this trace |
| captureResponseBody | boolean | Override response body capture for spans in this trace |
Tracing
Why use startTrace?
- Correlation — Tie all outgoing calls in a request (or job) to one trace and to a user/session.
- Stable IDs — Use
seed(e.g. request ID or order ID) to get a deterministic trace ID for logging or external systems. - Scoped body capture — Enable
captureRequestBody/captureResponseBodyonly for specific traces (e.g. a single webhook or LLM call) instead of globally.
Auto-initialization when using startTrace
If you call startTrace before calling initializePingops, the SDK will try to auto-initialize from environment variables (PINGOPS_API_KEY, PINGOPS_BASE_URL, PINGOPS_SERVICE_NAME). If any of these are missing, startTrace throws. For predictable behavior, prefer initializing explicitly at startup.
Example: request-scoped trace
import { startTrace, getActiveTraceId, initializePingops } from "@pingops/sdk";
initializePingops({ baseUrl: "...", serviceName: "my-api" });
app.post("/webhook", async (req, res) => {
const result = await startTrace(
{
attributes: {
userId: req.user?.id,
sessionId: req.sessionId,
tags: ["webhook"],
metadata: { provider: req.body.provider },
},
seed: req.headers["x-request-id"] ?? undefined,
},
async () => {
await callExternalApi(req.body);
return { ok: true };
}
);
const traceId = getActiveTraceId();
res.setHeader("X-Trace-Id", traceId ?? "");
res.json(result);
});Filtering & Privacy
Domain allow/deny lists
Restrict which domains (and optionally paths) are captured:
initializePingops({
baseUrl: "https://api.pingops.com",
serviceName: "my-service",
domainAllowList: [
{ domain: "api.github.com", paths: ["/repos"] },
{ domain: ".openai.com" }, // suffix match
{
domain: "generativelanguage.googleapis.com",
captureRequestBody: true,
captureResponseBody: true,
},
],
domainDenyList: [{ domain: "internal.corp.local" }],
});Each rule in domainAllowList / domainDenyList can include:
domain— Exact or suffix (e.g..openai.com) match.paths— Optional path prefixes to allow/deny.headersAllowList/headersDenyList— Header rules for that domain.captureRequestBody/captureResponseBody— Override body capture for that domain.
Header allow/deny lists
Control which headers are included on captured spans (global default; domain rules can refine):
initializePingops({
baseUrl: "https://api.pingops.com",
serviceName: "my-service",
headersAllowList: ["user-agent", "x-request-id", "content-type"],
headersDenyList: ["authorization", "cookie", "x-api-key"],
});Deny list takes precedence over allow list. Sensitive headers are redacted by default; use headerRedaction in config for custom behavior.
Request/response body capture
- Global:
captureRequestBodyandcaptureResponseBodyin config. - Per-domain: Same flags on a DomainRule.
- Per-trace:
captureRequestBody/captureResponseBodyin PingopsTraceAttributes instartTrace.
Body size is capped by maxRequestBodySize and maxResponseBodySize (default 4096 bytes each). Larger bodies are truncated.
Integration with Existing OpenTelemetry
If you already use OpenTelemetry and only want the PingOps exporter and filtering, use PingopsSpanProcessor from @pingops/otel and add it to your existing TracerProvider:
import { NodeSDK } from "@opentelemetry/sdk-node";
import { PingopsSpanProcessor } from "@pingops/otel";
const sdk = new NodeSDK({
spanProcessors: [
new PingopsSpanProcessor({
apiKey: "your-api-key",
baseUrl: "https://api.pingops.com",
serviceName: "my-service",
exportMode: "batched",
domainAllowList: [{ domain: "api.example.com" }],
}),
],
// your existing instrumentations, resource, etc.
});
sdk.start();You can still use @pingops/sdk for startTrace, getActiveTraceId, and getActiveSpanId; ensure your tracer provider is the one that uses PingopsSpanProcessor (or is bridged to it) so those spans are exported to PingOps.
What Gets Captured
- Outgoing HTTP — Requests made with Node’s
http/https(e.g. many HTTP clients under the hood). - Outgoing fetch — Requests made with the global
fetch(in Node.js 18+ this is implemented by Undici; both are instrumented).
Only CLIENT spans with HTTP (or supported semantic) attributes are exported to PingOps; server-side and internal spans are filtered out.
Requirements
- Node.js ≥ 20
- ESM — The package is published as ES modules; use
importand, if needed,"type": "module"or.mjs.
Summary
| Goal | What to do |
| ------------------ | ----------------------------------------------------------------------------------------------- |
| Install | pnpm add @pingops/sdk |
| Auto-init from env | node --require @pingops/sdk/register your-app.js or import "@pingops/sdk/register" first |
| Manual init | initializePingops({ baseUrl, serviceName, ... }) before any HTTP usage |
| Config from file | PINGOPS_CONFIG_FILE=./pingops.config.yaml or initializePingops("./pingops.config.json") |
| Trace with context | startTrace({ attributes: { userId, sessionId, tags, metadata }, seed? }, async () => { ... }) |
| Unsuppress a scope | runUnsuppressed(async () => { ... }) |
| Get current IDs | getActiveTraceId(), getActiveSpanId() |
| Graceful shutdown | await shutdownPingops() |
For more detail on types and options, see the Configuration and API Reference sections above.
