@pingops/sdk 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,36 @@
1
1
  # @pingops/sdk
2
2
 
3
- PingOps SDK for Node.js. Provides a simple API for bootstrapping OpenTelemetry and capturing outgoing API and LLM calls.
3
+ **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.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Overview](#overview)
10
+ - [Installation](#installation)
11
+ - [Quick Start](#quick-start)
12
+ - [Configuration](#configuration)
13
+ - [API Reference](#api-reference)
14
+ - [Tracing](#tracing)
15
+ - [Filtering & Privacy](#filtering--privacy)
16
+ - [Integration with Existing OpenTelemetry](#integration-with-existing-opentelemetry)
17
+ - [What Gets Captured](#what-gets-captured)
18
+ - [Requirements](#requirements)
19
+
20
+ ---
21
+
22
+ ## Overview
23
+
24
+ The PingOps SDK gives you:
25
+
26
+ - **Automatic instrumentation** — Outgoing HTTP (Node.js `http` module) and `fetch` (via Undici) are instrumented without wrapping your code.
27
+ - **Structured traces** — Start traces with `userId`, `sessionId`, tags, and metadata so every span is tied to your business context.
28
+ - **Control over what is captured** — Domain allow/deny lists, header filtering, and optional request/response body capture with size limits.
29
+ - **Flexible setup** — Use environment variables, a config file (JSON or YAML), or pass config programmatically. Auto-initialize via `--require` or import `@pingops/sdk/register` first.
30
+
31
+ 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.
32
+
33
+ ---
4
34
 
5
35
  ## Installation
6
36
 
@@ -8,150 +38,441 @@ PingOps SDK for Node.js. Provides a simple API for bootstrapping OpenTelemetry a
8
38
  pnpm add @pingops/sdk
9
39
  ```
10
40
 
41
+ Or with npm:
42
+
43
+ ```bash
44
+ npm install @pingops/sdk
45
+ ```
46
+
47
+ **Requirement:** Node.js **20 or later** (for native `fetch` and modern APIs).
48
+
49
+ ---
50
+
11
51
  ## Quick Start
12
52
 
53
+ ### Option 1: Auto-initialization (recommended)
54
+
55
+ **Best for:** Getting started quickly and for production when config comes from environment or a config file.
56
+
57
+ **A. Using Node.js `--require`** (runs before any application code):
58
+
59
+ ```bash
60
+ node --require @pingops/sdk/register your-app.js
61
+ ```
62
+
63
+ Set required environment variables:
64
+
65
+ ```bash
66
+ export PINGOPS_API_KEY="your-api-key"
67
+ export PINGOPS_BASE_URL="https://api.pingops.com"
68
+ export PINGOPS_SERVICE_NAME="my-service"
69
+ ```
70
+
71
+ **B. Importing the register entry first** (must be before any HTTP client imports):
72
+
73
+ ```typescript
74
+ // Must be first — before axios, node-fetch, or any code that makes HTTP requests
75
+ import "@pingops/sdk/register";
76
+
77
+ import axios from "axios";
78
+ // ... rest of your application
79
+ ```
80
+
81
+ With a config file, set `PINGOPS_CONFIG_FILE` to the path to your JSON or YAML file; environment variables override values from the file.
82
+
83
+ ### Option 2: Manual initialization
84
+
85
+ **Best for:** Config from code, feature flags, or when you need to ensure initialization order explicitly.
86
+
13
87
  ```typescript
14
88
  import { initializePingops } from "@pingops/sdk";
15
89
 
90
+ // Before importing or using any HTTP clients
16
91
  initializePingops({
17
- apiKey: "your-api-key", // or set PINGOPS_API_KEY env var
92
+ apiKey: process.env.PINGOPS_API_KEY,
18
93
  baseUrl: "https://api.pingops.com",
19
94
  serviceName: "my-service",
20
95
  });
96
+
97
+ import axios from "axios";
98
+ // ... rest of your application
21
99
  ```
22
100
 
23
- ## Features
101
+ You can also initialize from a config file path (environment variables still override file values):
24
102
 
25
- - **Automatic Instrumentation**: Captures HTTP and fetch API calls automatically
26
- - **Node.js Support**: Works in Node.js environments (including Node.js 20+ with native fetch)
27
- - **GenAI Support**: Captures LLM calls using OpenTelemetry GenAI semantic conventions
28
- - **Manual Instrumentation**: Create custom spans for specific operations
29
- - **Zero Configuration**: Works out of the box with sensible defaults
103
+ ```typescript
104
+ initializePingops("./pingops.config.yaml");
105
+ // or
106
+ initializePingops({ configFile: "./pingops.config.json" });
107
+ ```
30
108
 
31
- ## API
109
+ **Important:** Call `initializePingops` before any HTTP client is loaded or used so instrumentation is applied correctly.
110
+
111
+ ---
112
+
113
+ ## Configuration
114
+
115
+ Configuration can be provided via:
116
+
117
+ 1. **Programmatic config** — Object passed to `initializePingops(...)`
118
+ 2. **Config file** — Path as first argument or `{ configFile: "path" }`; supports JSON and YAML
119
+ 3. **Environment variables** — Always override file and can supply all required fields for auto-init
120
+
121
+ ### Required fields
122
+
123
+ | Field | Env var | Description |
124
+ |----------------|-----------------------|--------------------------------|
125
+ | `baseUrl` | `PINGOPS_BASE_URL` | PingOps backend base URL |
126
+ | `serviceName` | `PINGOPS_SERVICE_NAME`| Service name for resource |
127
+
128
+ `apiKey` is optional at config level; if your backend requires it, set `apiKey` or `PINGOPS_API_KEY`.
129
+
130
+ ### Full configuration reference
131
+
132
+ | Option | Type | Default | Description |
133
+ |-------------------------|-------------------------|------------|-------------|
134
+ | `apiKey` | `string` | — | API key (or `PINGOPS_API_KEY`) |
135
+ | `baseUrl` | `string` | **required** | Backend base URL |
136
+ | `serviceName` | `string` | **required** | Service name |
137
+ | `debug` | `boolean` | `false` | Enable debug logs (`PINGOPS_DEBUG=true`) |
138
+ | `headersAllowList` | `string[]` | — | Headers to include (case-insensitive) |
139
+ | `headersDenyList` | `string[]` | — | Headers to exclude (overrides allow) |
140
+ | `captureRequestBody` | `boolean` | `false` | Capture request bodies (global) |
141
+ | `captureResponseBody` | `boolean` | `false` | Capture response bodies (global) |
142
+ | `maxRequestBodySize` | `number` | `4096` | Max request body size in bytes |
143
+ | `maxResponseBodySize` | `number` | `4096` | Max response body size in bytes |
144
+ | `domainAllowList` | `DomainRule[]` | — | Domains (and optional rules) to allow |
145
+ | `domainDenyList` | `DomainRule[]` | — | Domains to exclude |
146
+ | `headerRedaction` | `HeaderRedactionConfig` | — | Custom header redaction |
147
+ | `batchSize` | `number` | `50` | Spans per batch (`PINGOPS_BATCH_SIZE`) |
148
+ | `batchTimeout` | `number` | `5000` | Flush interval in ms (`PINGOPS_BATCH_TIMEOUT`) |
149
+ | `exportMode` | `"batched"` \| `"immediate"` | `"batched"` | `PINGOPS_EXPORT_MODE` |
150
+
151
+ **Config file path:** Set `PINGOPS_CONFIG_FILE` to the path of your JSON or YAML file when using the register entry.
152
+
153
+ **Export mode:**
154
+
155
+ - **`batched`** — Best for long-running processes; spans are sent in batches (default).
156
+ - **`immediate`** — Best for serverless/short-lived processes; each span is sent as it finishes to reduce loss on freeze/exit.
157
+
158
+ ### Config file examples
159
+
160
+ **JSON (`pingops.config.json`):**
161
+
162
+ ```json
163
+ {
164
+ "apiKey": "your-api-key",
165
+ "baseUrl": "https://api.pingops.com",
166
+ "serviceName": "my-service",
167
+ "debug": false,
168
+ "exportMode": "batched",
169
+ "batchSize": 50,
170
+ "batchTimeout": 5000,
171
+ "captureRequestBody": false,
172
+ "captureResponseBody": false
173
+ }
174
+ ```
175
+
176
+ **YAML (`pingops.config.yaml`):**
177
+
178
+ ```yaml
179
+ apiKey: your-api-key
180
+ baseUrl: https://api.pingops.com
181
+ serviceName: my-service
182
+ debug: false
183
+ exportMode: batched
184
+ batchSize: 50
185
+ batchTimeout: 5000
186
+ ```
187
+
188
+ ---
189
+
190
+ ## API Reference
32
191
 
33
192
  ### `initializePingops(config)`
34
193
 
35
- Initializes the PingOps SDK with OpenTelemetry.
194
+ Initializes the PingOps SDK: sets up OpenTelemetry `NodeSDK`, registers the PingOps span processor, and enables HTTP and Undici (fetch) instrumentation.
195
+
196
+ **Overloads:**
36
197
 
37
- **Configuration:**
198
+ - `initializePingops(config: PingopsProcessorConfig): void`
199
+ - `initializePingops(configFilePath: string): void`
200
+ - `initializePingops({ configFile: string }): void`
201
+
202
+ **Example:**
38
203
 
39
204
  ```typescript
40
- interface PingopsInitConfig {
41
- apiKey?: string; // Defaults to PINGOPS_API_KEY env var
42
- baseUrl: string; // Required
43
- serviceName: string; // Required
44
- debug?: boolean;
45
- headersAllowList?: string[];
46
- headersDenyList?: string[];
47
- domainAllowList?: DomainRule[];
48
- domainDenyList?: DomainRule[];
49
- batchSize?: number; // Default: 50
50
- batchTimeout?: number; // Default: 5000ms
51
- }
205
+ import { initializePingops } from "@pingops/sdk";
206
+
207
+ initializePingops({
208
+ baseUrl: "https://api.pingops.com",
209
+ serviceName: "my-service",
210
+ apiKey: process.env.PINGOPS_API_KEY,
211
+ exportMode: "immediate", // e.g. for serverless
212
+ });
213
+ ```
214
+
215
+ Calling `initializePingops` again after the first successful call is a no-op (idempotent).
216
+
217
+ ---
218
+
219
+ ### `shutdownPingops()`
220
+
221
+ Gracefully shuts down the SDK and flushes remaining spans. Returns a `Promise<void>`.
222
+
223
+ **Example:**
224
+
225
+ ```typescript
226
+ import { shutdownPingops } from "@pingops/sdk";
227
+
228
+ process.on("SIGTERM", async () => {
229
+ await shutdownPingops();
230
+ process.exit(0);
231
+ });
52
232
  ```
53
233
 
54
- ### `pingops.startSpan(name, attributes, fn)`
234
+ ---
235
+
236
+ ### `startTrace(options, fn)`
55
237
 
56
- Creates a manual span for custom instrumentation.
238
+ 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.
239
+
240
+ **Parameters:**
241
+
242
+ - `options.attributes` — Optional [PingopsTraceAttributes](#pingopstraceattributes) to attach to the trace and propagate to spans.
243
+ - `options.seed` — Optional string; when provided, a deterministic trace ID is derived from it (useful for idempotency or correlation with external systems).
244
+ - `fn` — `() => T | Promise<T>`. Your code; runs inside the new trace and attribute context.
245
+
246
+ **Returns:** `Promise<T>` — The result of `fn`.
247
+
248
+ **Example:**
57
249
 
58
250
  ```typescript
59
- import { pingops } from "@pingops/sdk";
251
+ import { startTrace, initializePingops } from "@pingops/sdk";
252
+
253
+ initializePingops({ baseUrl: "...", serviceName: "my-api" });
60
254
 
61
- await pingops.startSpan(
62
- "external.api.call",
255
+ const data = await startTrace(
63
256
  {
64
- customer_id: "cust_123",
65
- correlation_id: "req_456",
66
- "custom_attributes.request_type": "webhook",
257
+ attributes: {
258
+ userId: "user-123",
259
+ sessionId: "sess-456",
260
+ tags: ["checkout", "v2"],
261
+ metadata: { plan: "pro", region: "us" },
262
+ captureRequestBody: true,
263
+ captureResponseBody: true,
264
+ },
265
+ seed: "order-789", // optional: stable trace ID for this order
67
266
  },
68
- async (span) => {
69
- // Your code here
70
- const result = await fetch("https://api.example.com/data");
71
- return result.json();
267
+ async () => {
268
+ const res = await fetch("https://api.stripe.com/v1/charges", { ... });
269
+ return res.json();
72
270
  }
73
271
  );
74
272
  ```
75
273
 
76
- The span is automatically ended when the function completes or throws an error.
274
+ ---
77
275
 
78
- ### `shutdownPingops()`
276
+ ### `getActiveTraceId()`
79
277
 
80
- Gracefully shuts down the SDK and flushes remaining spans.
278
+ Returns the trace ID of the currently active span, or `undefined` if there is none.
279
+
280
+ **Example:**
81
281
 
82
282
  ```typescript
83
- import { shutdownPingops } from "@pingops/sdk";
283
+ import { getActiveTraceId } from "@pingops/sdk";
84
284
 
85
- await shutdownPingops();
285
+ const traceId = getActiveTraceId();
286
+ console.log("Current trace:", traceId);
86
287
  ```
87
288
 
88
- ## Domain Filtering
289
+ ---
89
290
 
90
- Control which domains and paths are captured:
291
+ ### `getActiveSpanId()`
292
+
293
+ Returns the span ID of the currently active span, or `undefined` if there is none.
294
+
295
+ **Example:**
91
296
 
92
297
  ```typescript
93
- initializePingops({
94
- // ... other config
95
- domainAllowList: [
298
+ import { getActiveSpanId } from "@pingops/sdk";
299
+
300
+ const spanId = getActiveSpanId();
301
+ ```
302
+
303
+ ---
304
+
305
+ ### `PingopsTraceAttributes`
306
+
307
+ Type for attributes you can pass into `startTrace({ attributes })`:
308
+
309
+ | Field | Type | Description |
310
+ |-------------------------|--------------------------|-------------|
311
+ | `traceId` | `string` | Override trace ID (otherwise one is generated or derived from `seed`) |
312
+ | `userId` | `string` | User identifier |
313
+ | `sessionId` | `string` | Session identifier |
314
+ | `tags` | `string[]` | Tags for the trace |
315
+ | `metadata` | `Record<string, string>` | Key-value metadata |
316
+ | `captureRequestBody` | `boolean` | Override request body capture for spans in this trace |
317
+ | `captureResponseBody` | `boolean` | Override response body capture for spans in this trace |
318
+
319
+ ---
320
+
321
+ ## Tracing
322
+
323
+ ### Why use `startTrace`?
324
+
325
+ - **Correlation** — Tie all outgoing calls in a request (or job) to one trace and to a user/session.
326
+ - **Stable IDs** — Use `seed` (e.g. request ID or order ID) to get a deterministic trace ID for logging or external systems.
327
+ - **Scoped body capture** — Enable `captureRequestBody` / `captureResponseBody` only for specific traces (e.g. a single webhook or LLM call) instead of globally.
328
+
329
+ ### Auto-initialization when using `startTrace`
330
+
331
+ 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.
332
+
333
+ ### Example: request-scoped trace
334
+
335
+ ```typescript
336
+ import { startTrace, getActiveTraceId, initializePingops } from "@pingops/sdk";
337
+
338
+ initializePingops({ baseUrl: "...", serviceName: "my-api" });
339
+
340
+ app.post("/webhook", async (req, res) => {
341
+ const result = await startTrace(
96
342
  {
97
- domain: "api.github.com",
98
- paths: ["/repos"],
99
- headersAllowList: ["authorization", "user-agent"],
343
+ attributes: {
344
+ userId: req.user?.id,
345
+ sessionId: req.sessionId,
346
+ tags: ["webhook"],
347
+ metadata: { provider: req.body.provider },
348
+ },
349
+ seed: req.headers["x-request-id"] ?? undefined,
100
350
  },
351
+ async () => {
352
+ await callExternalApi(req.body);
353
+ return { ok: true };
354
+ }
355
+ );
356
+
357
+ const traceId = getActiveTraceId();
358
+ res.setHeader("X-Trace-Id", traceId ?? "");
359
+ res.json(result);
360
+ });
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Filtering & Privacy
366
+
367
+ ### Domain allow/deny lists
368
+
369
+ Restrict which domains (and optionally paths) are captured:
370
+
371
+ ```typescript
372
+ initializePingops({
373
+ baseUrl: "https://api.pingops.com",
374
+ serviceName: "my-service",
375
+ domainAllowList: [
376
+ { domain: "api.github.com", paths: ["/repos"] },
377
+ { domain: ".openai.com" }, // suffix match
101
378
  {
102
- domain: ".openai.com", // Suffix match
379
+ domain: "generativelanguage.googleapis.com",
380
+ captureRequestBody: true,
381
+ captureResponseBody: true,
103
382
  },
104
383
  ],
105
384
  domainDenyList: [
106
- {
107
- domain: "internal.service.local",
108
- },
385
+ { domain: "internal.corp.local" },
109
386
  ],
110
387
  });
111
388
  ```
112
389
 
113
- ## Header Filtering
390
+ Each rule in `domainAllowList` / `domainDenyList` can include:
391
+
392
+ - `domain` — Exact or suffix (e.g. `.openai.com`) match.
393
+ - `paths` — Optional path prefixes to allow/deny.
394
+ - `headersAllowList` / `headersDenyList` — Header rules for that domain.
395
+ - `captureRequestBody` / `captureResponseBody` — Override body capture for that domain.
114
396
 
115
- Control which headers are captured:
397
+ ### Header allow/deny lists
398
+
399
+ Control which headers are included on captured spans (global default; domain rules can refine):
116
400
 
117
401
  ```typescript
118
402
  initializePingops({
119
- // ... other config
120
- headersAllowList: ["user-agent", "x-request-id"],
121
- headersDenyList: ["authorization", "cookie"],
403
+ baseUrl: "https://api.pingops.com",
404
+ serviceName: "my-service",
405
+ headersAllowList: ["user-agent", "x-request-id", "content-type"],
406
+ headersDenyList: ["authorization", "cookie", "x-api-key"],
122
407
  });
123
408
  ```
124
409
 
410
+ Deny list takes precedence over allow list. Sensitive headers are redacted by default; use `headerRedaction` in config for custom behavior.
411
+
412
+ ### Request/response body capture
413
+
414
+ - **Global:** `captureRequestBody` and `captureResponseBody` in config.
415
+ - **Per-domain:** Same flags on a [DomainRule](#domain-allowdeny-lists).
416
+ - **Per-trace:** `captureRequestBody` / `captureResponseBody` in [PingopsTraceAttributes](#pingopstraceattributes) in `startTrace`.
417
+
418
+ Body size is capped by `maxRequestBodySize` and `maxResponseBodySize` (default 4096 bytes each). Larger bodies are truncated.
419
+
420
+ ---
421
+
125
422
  ## Integration with Existing OpenTelemetry
126
423
 
127
- If you already have OpenTelemetry set up, you can use just the `PingopsSpanProcessor`:
424
+ 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`:
128
425
 
129
426
  ```typescript
427
+ import { NodeSDK } from "@opentelemetry/sdk-node";
130
428
  import { PingopsSpanProcessor } from "@pingops/otel";
131
- import { getTracerProvider } from "@opentelemetry/api";
132
429
 
133
- const processor = new PingopsSpanProcessor({
134
- apiKey: "your-api-key",
135
- baseUrl: "https://api.pingops.com",
136
- serviceName: "my-service",
430
+ const sdk = new NodeSDK({
431
+ spanProcessors: [
432
+ new PingopsSpanProcessor({
433
+ apiKey: "your-api-key",
434
+ baseUrl: "https://api.pingops.com",
435
+ serviceName: "my-service",
436
+ exportMode: "batched",
437
+ domainAllowList: [{ domain: "api.example.com" }],
438
+ }),
439
+ ],
440
+ // your existing instrumentations, resource, etc.
137
441
  });
138
442
 
139
- const tracerProvider = getTracerProvider();
140
- // Add processor to your existing tracer provider
443
+ sdk.start();
141
444
  ```
142
445
 
446
+ 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.
447
+
448
+ ---
449
+
143
450
  ## What Gets Captured
144
451
 
145
- - **HTTP Requests**: All outgoing HTTP requests (via `http` module in Node.js)
146
- - **Fetch API**: All `fetch()` calls (universal JS)
147
- - **GenAI Calls**: LLM API calls that follow OpenTelemetry GenAI semantic conventions
452
+ - **Outgoing HTTP** Requests made with Node’s `http` / `https` (e.g. many HTTP clients under the hood).
453
+ - **Outgoing fetch** Requests made with the global `fetch` (in Node.js 18+ this is implemented by Undici; both are instrumented).
148
454
 
149
- ## What Doesn't Get Captured
455
+ Only **CLIENT** spans with HTTP (or supported semantic) attributes are exported to PingOps; server-side and internal spans are filtered out.
150
456
 
151
- - Incoming requests (server-side)
152
- - Internal spans (non-CLIENT spans)
153
- - Spans without HTTP or GenAI attributes
457
+ ---
154
458
 
155
459
  ## Requirements
156
460
 
157
- - **Node.js**: Requires Node.js 20+ (for native fetch support) or Node.js 18+ with fetch polyfill
461
+ - **Node.js** **20**
462
+ - **ESM** — The package is published as ES modules; use `import` and, if needed, `"type": "module"` or `.mjs`.
463
+
464
+ ---
465
+
466
+ ## Summary
467
+
468
+ | Goal | What to do |
469
+ |------|------------|
470
+ | Install | `pnpm add @pingops/sdk` |
471
+ | Auto-init from env | `node --require @pingops/sdk/register your-app.js` or `import "@pingops/sdk/register"` first |
472
+ | Manual init | `initializePingops({ baseUrl, serviceName, ... })` before any HTTP usage |
473
+ | Config from file | `PINGOPS_CONFIG_FILE=./pingops.config.yaml` or `initializePingops("./pingops.config.json")` |
474
+ | Trace with context | `startTrace({ attributes: { userId, sessionId, tags, metadata }, seed? }, async () => { ... })` |
475
+ | Get current IDs | `getActiveTraceId()`, `getActiveSpanId()` |
476
+ | Graceful shutdown | `await shutdownPingops()` |
477
+
478
+ For more detail on types and options, see the [Configuration](#configuration) and [API Reference](#api-reference) sections above.