{"_id":"@scope3/observability-js","_rev":"2-da3985fc3ade3bce4478ed4b3fcc7cc6","name":"@scope3/observability-js","dist-tags":{"latest":"2.1.1"},"versions":{"2.1.0":{"name":"@scope3/observability-js","version":"2.1.0","keywords":["observability","sentry","opentelemetry","tracing","pyroscope"],"license":"MIT","_id":"@scope3/observability-js@2.1.0","maintainers":[{"name":"benminer","email":"bdminer96@gmail.com"},{"name":"scope3data","email":"corpit@scope3.com"}],"homepage":"https://github.com/scope3data/observability-js","bugs":{"url":"https://github.com/scope3data/observability-js/issues"},"dist":{"shasum":"bfc4c0b3a28d97ee92305906ddcdff25103be807","tarball":"https://registry.npmjs.org/@scope3/observability-js/-/observability-js-2.1.0.tgz","fileCount":9,"integrity":"sha512-Q9wEYIUdn5uPTy196ijSMynvcuPzqrfR6OAdwwyT13SFCP/EsEehSz8kss3ntW1uufI+5DOfNATqQu0qq/W/hg==","signatures":[{"sig":"MEQCIEK54acZHHndIZhj2QEiq+enosTEvgewU95cu5N63W9JAiBqx3D/FEV0IujH8nRQHXFLyWumakvXlA7MXffJqnuC2Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":130649},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">= 20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"lint":"biome check .","test":"vitest run","build":"tsup","prepare":"husky","test:ci":"vitest run --reporter=verbose","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"scope3data","email":"corpit@scope3.com"},"repository":{"url":"git+https://github.com/scope3data/observability-js.git","type":"git"},"_npmVersion":"11.6.2","description":"Unified observability (Sentry, OpenTelemetry, Pyroscope) for Node.js services","directories":{},"lint-staged":{"*.{js,ts,cjs,mjs,json,jsonc}":["biome check --write --no-errors-on-unmatched"]},"_nodeVersion":"24.13.0","dependencies":{"jose":"^6.1.0","@sentry/node":"10.34.0","@pyroscope/nodejs":"^0.4.8","@opentelemetry/api":"^1.9.0","@sentry/opentelemetry":"10.34.0","@sentry/profiling-node":"10.34.0","@opentelemetry/resources":"^2.6.1","@opentelemetry/sdk-trace-node":"^2.6.1","@opentelemetry/semantic-conventions":"^1.40.0","@opentelemetry/exporter-trace-otlp-http":"^0.214.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","husky":"^9.1.7","semver":"^7.7.4","vitest":"^4.0.18","typescript":"^5.5.0","@types/node":"^22.0.0","lint-staged":"^16.3.3","@biomejs/biome":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/observability-js_2.1.0_1779377237876_0.8019968207762205","host":"s3://npm-registry-packages-npm-production"}},"2.1.1":{"name":"@scope3/observability-js","version":"2.1.1","description":"Unified observability (Sentry, OpenTelemetry, Pyroscope) for Node.js services","keywords":["observability","sentry","opentelemetry","tracing","pyroscope"],"homepage":"https://github.com/scope3data/observability-js","bugs":{"url":"https://github.com/scope3data/observability-js/issues"},"repository":{"type":"git","url":"git+https://github.com/scope3data/observability-js.git"},"license":"MIT","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","lint":"biome check .","lint:fix":"biome check --write .","test":"vitest run","test:ci":"vitest run --reporter=verbose","prepare":"husky","prepublishOnly":"npm run build"},"lint-staged":{"*.{js,ts,cjs,mjs,json,jsonc}":["biome check --write --no-errors-on-unmatched"]},"dependencies":{"@opentelemetry/api":"^1.9.0","@opentelemetry/exporter-trace-otlp-http":"^0.214.0","@opentelemetry/resources":"^2.6.1","@opentelemetry/sdk-trace-node":"^2.6.1","@opentelemetry/semantic-conventions":"^1.40.0","@pyroscope/nodejs":"^0.4.8","@sentry/node":"10.34.0","@sentry/opentelemetry":"10.34.0","@sentry/profiling-node":"10.34.0","jose":"^6.1.0"},"devDependencies":{"@biomejs/biome":"^2.0.0","@opentelemetry/context-async-hooks":"^2.6.1","@types/node":"^22.0.0","husky":"^9.1.7","lint-staged":"^16.3.3","semver":"^7.7.4","tsup":"^8.0.0","typescript":"^5.5.0","vitest":"^4.0.18"},"engines":{"node":">= 20"},"publishConfig":{"access":"public"},"gitHead":"885d537ed08a5f3e507e6663d367ddbd355aab6f","_id":"@scope3/observability-js@2.1.1","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-0WtGuqnWB0DBGh4qmL3Iz2nwyfBJthKbG8my+XL6k3rEt71GkwcoFwJP9hSZqVf/yk0jXRQ/YoGCnoCwfH58Jg==","shasum":"a17ccd22e22c18572c5df85561e0bd908d8e4b80","tarball":"https://registry.npmjs.org/@scope3/observability-js/-/observability-js-2.1.1.tgz","fileCount":9,"unpackedSize":143011,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@scope3%2fobservability-js@2.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCq/uzwOEUTGCJalFBNpfaPHW0Bd5ax0/TVLYJ35oAvzAIgWtQ49VPy+B4RB6Sas6aRdApQYOjeiMP7IJdlXHcNEBM="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:aa481c18-7804-42ca-a172-1f13bee6a798"}},"directories":{},"maintainers":[{"name":"benminer","email":"bdminer96@gmail.com"},{"name":"scope3data","email":"corpit@scope3.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/observability-js_2.1.1_1785366144127_0.5400848849771607"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-21T15:27:17.671Z","modified":"2026-07-29T23:02:24.609Z","2.1.0":"2026-05-21T15:27:18.022Z","2.1.1":"2026-07-29T23:02:24.251Z"},"bugs":{"url":"https://github.com/scope3data/observability-js/issues"},"license":"MIT","homepage":"https://github.com/scope3data/observability-js","keywords":["observability","sentry","opentelemetry","tracing","pyroscope"],"repository":{"type":"git","url":"git+https://github.com/scope3data/observability-js.git"},"description":"Unified observability (Sentry, OpenTelemetry, Pyroscope) for Node.js services","maintainers":[{"name":"benminer","email":"bdminer96@gmail.com"},{"name":"scope3data","email":"corpit@scope3.com"}],"readme":"# @scope3/observability-js\n\nUnified observability for Node.js services. A single `init()` call wires up [Sentry](https://sentry.io) (error monitoring, tracing, and profiling), [Pyroscope](https://pyroscope.io) (continuous CPU and heap profiling), and optionally the [OpenTelemetry](https://opentelemetry.io) SDK with OTLP trace export.\n\n## Installation\n\n```sh\nnpm install @scope3/observability-js\n```\n\nRequires Node.js >= 24.\n\n## Bundler configuration\n\nThis package maintains a singleton initialization state. If your application uses a bundler (e.g. esbuild) with **multiple entry points**, you must externalize this package so all entry points share the same installed copy at runtime rather than each getting their own inlined copy with separate state.\n\nWithout this, `instrument.js` (which calls `init()`) and `server.js` (which uses span/error helpers) end up with separate unconnected copies of the package — `init()` only initializes one of them.\n\n**esbuild (`build.js`):**\n\n```js\nconst EXTERNAL_PACKAGES = [\n  '@scope3/observability-js',\n  // ...\n]\n```\n\nThis is not required when running with a dev server (e.g. `tsx`) since Node's module cache naturally deduplicates the package.\n\n## Quick start\n\nCall `init()` once at process startup, before anything else runs:\n\n```ts\nimport { init } from '@scope3/observability-js'\n\ninit({\n  serviceName: 'my-service',\n  sentry: {\n    dsn: process.env.SENTRY_DSN,\n  },\n})\n```\n\n`environment` defaults to `NODE_ENV` and `release` defaults to `COMMIT_SHA`, so those fields can typically be omitted.\n\n## Configuration\n\nAll options are passed to `init()` as an `ObservabilityConfig` object.\n\n### Top-level fields\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `serviceName` | `string` | — | **Required.** Identifies this service in Sentry, Pyroscope, and OTLP traces. |\n| `environment` | `string` | `NODE_ENV \\|\\| 'development'` | Deployment environment. |\n| `release` | `string` | `COMMIT_SHA \\|\\| 'unknown'` | Release identifier attached to Sentry events and traces. |\n| `enableOtel` | `boolean` | `false` | Enable the OpenTelemetry SDK. Required when using OTLP export or the tracing helpers. |\n| `tracerName` | `string` | `serviceName` | Name used to obtain the OTel tracer instance. |\n| `sentry` | `SentryConfig` | — | Sentry configuration. See below. |\n| `pyroscope` | `PyroscopeConfig` | — | Pyroscope configuration. See below. |\n| `otlp` | `OtlpConfig` | — | OTLP trace export configuration. See below. |\n| `filters` | `FilterConfig` | — | Filtering rules for traces, errors, and breadcrumbs. See below. |\n| `shouldDropError` | `(error: unknown) => boolean` | — | Predicate to suppress specific errors from Sentry. Return `true` to drop. |\n| `classifyError` | `(error: unknown) => 'drop' \\| 'warning' \\| undefined` | — | Classify errors before sending to Sentry. `'drop'` suppresses the error, `'warning'` downgrades its severity level, `undefined` leaves it unchanged. |\n\n### `SentryConfig`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `dsn` | `string` | — | Sentry DSN. Required for Sentry to activate in deployed environments. |\n| `enabled` | `boolean` | auto | Override automatic enable/disable logic. By default Sentry is enabled when a DSN is present and the environment is `production` or `staging`. |\n| `sampleRate` | `number` | `0.25` (prod), `1.0` (other) | Fraction of transactions sent to Sentry, between 0 and 1. |\n| `profileSampleRate` | `number` | `sampleRate` | Fraction of sampled transactions to profile, between 0 and 1. |\n\n### `PyroscopeConfig`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `enabled` | `boolean` | auto | Override automatic enable/disable logic. By default Pyroscope is enabled in `production` and `staging`. |\n| `serverAddress` | `string` | in-cluster address (deployed), `http://localhost:4040` (local) | Pyroscope server address. |\n| `tags` | `Record<string, string>` | `{}` | Additional tags attached to all profiles. `environment` is always included automatically. |\n\n### `OtlpConfig`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `endpoint` | `string` | — | OTLP collector endpoint (e.g. `http://otel-collector:4318`). OTLP export is disabled when omitted. |\n| `headers` | `Record<string, string>` | `{}` | HTTP headers sent with every export request. |\n| `sampleRate` | `number` | `1.0` | Fraction of traces to export via OTLP, between 0 and 1. |\n\n### `FilterConfig`\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `ignoredRoutes` | `string[]` | `[]` | Routes never sampled by Sentry (e.g. `['/health', '/metrics']`). Trailing slashes and query strings are handled automatically. |\n| `ignoredBreadcrumbCategories` | `string[]` | `['http', 'fetch', 'xhr']` | Breadcrumb categories to apply URL-pattern filtering to. |\n| `ignoredBreadcrumbPatterns` | `RegExp[]` | Pyroscope + PostHog patterns | URL patterns for breadcrumbs that should be dropped entirely. |\n\n## API\n\n### Initialization\n\n#### `init(config: ObservabilityConfig): void`\n\nInitialize Sentry, Pyroscope, and (optionally) the OpenTelemetry SDK. Idempotent — subsequent calls are no-ops.\n\nIn `test` environments (`NODE_ENV=test`) all instrumentation is skipped and only the config is resolved, so tests remain fast and side-effect free.\n\n```ts\ninit({\n  serviceName: 'my-service',\n  enableOtel: true,\n  sentry: { dsn: process.env.SENTRY_DSN },\n  otlp: { endpoint: process.env.OTLP_ENDPOINT },\n  filters: {\n    ignoredRoutes: ['/health', '/health/liveness', '/metrics'],\n  },\n  shouldDropError: (error) =>\n    error instanceof MyClientError && error.code === 'NOT_FOUND',\n  classifyError: (error) => {\n    if (error instanceof TransportError && error.transient) return 'warning'\n    return undefined\n  },\n})\n```\n\n#### `resetForTesting(): void`\n\nReset the initialized state so `init()` can be called again. For use in test teardown only — do not call in production code.\n\n```ts\nafterEach(() => {\n  resetForTesting()\n})\n```\n\n### Tracing\n\nAll tracing helpers require `enableOtel: true` in the config passed to `init()`.\n\nAll span helpers accept an optional `tracerName` as their last argument. See [Library usage](#library-usage) for when to use this.\n\n#### `getTracer(name?: string): Tracer`\n\nReturn an OpenTelemetry tracer by name. Safe to call without `init()` — when no OTel provider is registered the returned tracer is a no-op. When `name` is omitted, falls back to the tracer name from `init()` config, or `'observability-js'` if `init()` has not been called.\n\nUse this when you need direct access to the tracer (e.g. to build custom instrumentation). For most cases, prefer `startSpan`, `startMCPToolSpan`, or `startManualSpan`.\n\n```ts\nconst tracer = getTracer('adcp-client')\n```\n\n#### `startSpan<T>(op, name, attributes, callback, tracerName?): Promise<T>`\n\nStart a generic active span. Automatically sets status to `OK` on success, records the exception and sets status to `ERROR` on failure, and always ends the span.\n\n```ts\nconst result = await startSpan('db.query', 'fetch user', { 'db.table': 'users' }, async (span) => {\n  const user = await db.findUser(id)\n  setSpanAttributes(span, { 'user.id': user.id })\n  return user\n})\n```\n\n#### `startMCPToolSpan<T>(toolName, context, callback, tracerName?): Promise<T>`\n\nStart an active span for an MCP tool invocation. Automatically attaches `mcp.tool.name`, `mcp.session.id`, `mcp.transport`, `customer.id`, and `customer.company` as span attributes. Same automatic status and lifecycle management as `startSpan`.\n\n```ts\nconst result = await startMCPToolSpan('search_campaigns', { sessionId, customerId, company }, async (span) => {\n  return await searchCampaigns(args)\n})\n```\n\n#### `startManualSpan(op, name, attributes, tracerName?): Span`\n\nStart a span whose lifecycle is managed by the caller. Use this for streaming operations (e.g. SSE) where the work outlives a single async callback. The caller must call `span.end()` and set the span status.\n\nUnlike `startSpan` and `startMCPToolSpan`, this creates a non-active span — child operations will not automatically inherit it as their parent.\n\n```ts\nconst span = startManualSpan('http.server', 'stream response', { 'stream.id': id })\n\nstream.on('end', () => {\n  span.setStatus({ code: SpanStatusCode.OK })\n  span.end()\n})\n\nstream.on('error', (err) => {\n  span.setStatus({ code: SpanStatusCode.ERROR, message: err.message })\n  span.end()\n})\n```\n\n### Span utilities\n\n#### `setSpanAttributes(span, attributes): void`\n\nSet multiple attributes on a span in one call. `undefined` values are silently skipped.\n\n```ts\nsetSpanAttributes(span, {\n  'customer.id': customerId,\n  'request.size': body?.length,\n})\n```\n\n#### `setSpanError(span, isError): void`\n\nMark a span as representing a tool-level error result — distinct from an unhandled exception. Sets the `mcp.tool.result.is_error` attribute and, when `true`, sets the span status to `ERROR`.\n\n```ts\nconst response = await callTool(args)\nsetSpanError(span, response.isError)\n```\n\n### Error capture\n\n#### `captureToolError(error, toolName, context): void`\n\nCapture an error thrown during an MCP tool invocation and report it to Sentry. `toolName` and `sessionId` are attached as indexed Sentry tags. `customerId`, `company`, and `args` are attached as extra context. `args` is JSON-serialised and truncated to 2000 characters.\n\n```ts\ntry {\n  await runTool(args)\n} catch (error) {\n  captureToolError(error, 'search_campaigns', { sessionId, customerId, company, args })\n  throw error\n}\n```\n\n#### `captureServiceError(error, serviceName, context): void`\n\nCapture a general service error and report it to Sentry. `serviceName`, `sessionId`, and `customerId` are attached as indexed tags. All remaining context fields are attached as extras.\n\n```ts\ncaptureServiceError(error, 'billing-service', {\n  customerId,\n  sessionId,\n  invoiceId,\n})\n```\n\n## Library usage\n\nAll span helpers are safe to call without `init()` having been called first. When no OTel provider is registered, spans are no-ops and callbacks execute normally. This makes it possible for a library (e.g. `adcp-client`) to emit spans that automatically flow into the consuming application's traces when that application has called `init({ enableOtel: true })`.\n\n### Pattern\n\nIn your library, pass your library name as the `tracerName` argument to any span helper. Do not call `init()` — that is the responsibility of the consuming application.\n\n```ts\nimport { startSpan, setSpanAttributes } from '@scope3/observability-js'\n\nasync function executeToolCall(toolName: string, agentId: string, params: unknown) {\n  return startSpan(\n    'adcp.tool.call',\n    `adcp/tool/${toolName}`,\n    {\n      'adcp.tool.name': toolName,\n      'adcp.agent.id': agentId,\n    },\n    async (span) => {\n      const result = await callAgent(toolName, params)\n      setSpanAttributes(span, { 'adcp.task.status': result.status })\n      return result\n    },\n    'adcp-client', // tracerName — namespaces spans under this library\n  )\n}\n```\n\nWhen the consuming application (e.g. `agentic-api`) calls `init({ enableOtel: true })`, the OTel SDK is registered globally and picks up spans from all tracer names — including `'adcp-client'`. No changes are required in the consuming application beyond what it already does.\n\n## Automatic behaviors\n\nSeveral things happen without any additional configuration:\n\n- **`JWTExpired` errors are silently dropped** from Sentry — expired tokens are expected and not actionable.\n- **Error classification runs in order**: `shouldDropError` is checked first, then `classifyError`, then the built-in `JWTExpired` filter. The first match wins.\n- **Pyroscope initialization failures are caught** — if Pyroscope fails to start, the error is reported to Sentry and the process continues normally.\n- **Pyroscope and PostHog breadcrumbs are filtered** from Sentry by default via `filters.ignoredBreadcrumbPatterns`.\n- **Test environments are inert** — when `NODE_ENV=test`, `init()` resolves the config but skips all instrumentation. No Sentry, no Pyroscope, no OTel side effects.\n- **Pyroscope wall-clock and heap profiling** are both enabled by default (`collectCpuTime: true`, heap sampling every 512 KB).\n- **OTLP spans are batched** with a max batch size of 512, a 5-second flush interval, and a 30-second export timeout.\n\n## Environment variables\n\n| Variable | Description |\n|---|---|\n| `NODE_ENV` | Determines the environment when `config.environment` is omitted. `production` and `staging` activate Sentry and Pyroscope automatically. |\n| `COMMIT_SHA` | Used as the `release` identifier when `config.release` is omitted. |\n\n## Contributing\n\nThis package is open source under the [MIT License](./LICENSE). Contributions are welcome via pull request against the `main` branch.\n\nBefore submitting a PR, make sure the following pass locally:\n\n```sh\nnpm run build\nnpm run typecheck\nnpm run lint\nnpm test\n```\n\n### Releasing\n\nReleases are automated. When a pull request is merged to `main` with a bumped version in `package.json`, the release workflow will:\n\n1. Compare the new version against the latest git tag using semver\n2. If the version is greater, create a git tag, a GitHub Release, and publish to npm\n3. Prerelease versions (`alpha`, `beta`, `rc`) are published to the `next` npm tag; stable versions to `latest`\n\n## License\n\n[MIT](./LICENSE) - Copyright (c) 2026 Scope3 Data, Inc.\n","readmeFilename":"README.md"}