Browse documentation

Instrument

JavaScript and TypeScript

Instrument Node.js, browser, Express, Fastify, NestJS GraphQL, Prisma, and React applications with the Anectico SDK.

Use @anectico/sdk for Node.js, browser, Express, Fastify, NestJS GraphQL, Prisma, and React applications. The base package sends traces, metrics, logs, errors, diagnostic events, and customer identity; replay and LLM wrappers are opt-in subpath imports.

Install and start

After the registry release:

npm install @anectico/sdk

During early access, use the package artifact or install path supplied during onboarding if the registry package is not available.

import { init } from '@anectico/sdk';

const anectico = await init({
  apiKey: process.env.ANECTICO_API_KEY,
  serviceName: 'orders-api',
  serviceVersion: process.env.GIT_SHA,
  environment: process.env.NODE_ENV,
});

Keep one client for the process. Call await anectico.stop() during graceful shutdown so buffered telemetry can finish exporting.

For a raw Node HTTP server in managed mode, exclude health and internal control traffic from the generic incoming HTTP spans:

const anectico = await initNode({
  apiKey: process.env.ANECTICO_API_KEY,
  serviceName: 'orders-api',
  ignoreIncomingRequestPaths: ['/healthz', '/ready', '/internal/*'],
});

The equivalent ANECTICO_IGNORE_INCOMING_REQUEST_PATHS environment variable is a comma-separated list. Exact paths and trailing-* prefixes are supported, and query strings are ignored for matching.

initNode() provides default SIGTERM and SIGINT flushing only while Anectico is the sole listener for that signal. When your application registers its own handler—for example to drain a queue or close WebSocket sessions—it owns the ordering. Stop accepting work, close the connections and end their spans, then await anectico.stop(). Anectico detects application listeners whether they were registered before or after initNode() and will not race them by stopping the exporter first. Concurrent stop() calls share the same in-flight shutdown. The first uncaught exception or unhandled rejection owns one fatal capture, flush, and exit sequence, so a secondary terminal event cannot return early and cut off the original flush.

For Kubernetes, set terminationGracePeriodSeconds to cover application draining plus Anectico’s two managed shutdown phases. Identity/group delivery is bounded by shutdownTimeout, then the trace, metric, and log providers shut down concurrently under another shutdownTimeout; the defaults therefore need at least 10 seconds after application work closes.

SIGKILL, kill -9, forced zero-grace pod deletion, and host loss cannot run JavaScript handlers. They create no terminal capture and perform no final flush. The lost tail may include active spans, in-flight identity/group requests, up to maxQueueSize ended spans and logs per batched signal (2,048 by default), and metrics accumulated since the last metricExportInterval. Use SIGTERM and a non-zero grace period for deploys and ordinary shutdowns.

Serverless invocation lifecycle

Initialize one client promise at module scope so a warm AWS Lambda-style runtime reuses the same providers. At the end of every invocation, call await anectico.flush() rather than stop(): flush() waits for identity requests and forces buffered traces, logs, and metrics out before the runtime may freeze, while keeping the client running for the next invocation.

import { context } from '@opentelemetry/api';
import { contextWithDistinctId, initNode } from '@anectico/sdk/node';

const anecticoPromise = initNode({
  apiKey: process.env.ANECTICO_API_KEY,
  serviceName: 'invoice-automation',
});

export async function handler(event: { customerId: string }) {
  const anectico = await anecticoPromise;
  const invocationContext = contextWithDistinctId(context.active(), event.customerId);

  try {
    return await context.with(invocationContext, async () => {
      const span = anectico.startSpan('lambda.invoke');
      try {
        return await automateInvoice(event);
      } finally {
        span.end();
      }
    });
  } finally {
    await anectico.flush();
  }
}

Do not call initNode() inside the handler or call stop() after a successful invocation. Repeated in-handler initialization creates competing providers, while stop() permanently closes the client. Reserve stop() for controlled process teardown. Keep person identity invocation-scoped with contextWithDistinctId; do not switch the process-global identity for concurrent requests.

Capture a connected operation

anectico.identify('user_8842', { email: 'buyer@acme.example', plan: 'pro' });

const span = anectico.startSpan('process-order');
try {
  await processOrder();
} catch (error) {
  anectico.captureError(error as Error, { tags: { component: 'checkout' } });
  throw error;
} finally {
  span.end();
}

Call anectico.reset() on logout. In a browser it first stops active replay recorders and flushes their prior-user tail before rotating identity/session. It also clears uncaptured breadcrumbs, groups, and global error-user context; other already-captured telemetry keeps its original attribution. Start a new recorder after identifying the next user if replay should resume. In a multi-user server, prefer framework request context over changing the process-wide identity for every request.

Record an application metric

anectico.recordMetric('checkout.duration_ms', durationMs, {
  route: '/checkout',
  result: 'success',
});

recordMetric records one histogram observation and reuses the instrument by name. Keep labels low-cardinality; do not add customer, order, request, trace, or session IDs. Use an existing OpenTelemetry metrics pipeline when you need an explicit counter or gauge.

Express, Fastify, and service propagation

import express from 'express';
import { AnecticoClient } from '@anectico/sdk';
import { anecticoExpressErrorHandler, anecticoExpressMiddleware } from '@anectico/sdk/node';

const app = express();
const anectico = await new AnecticoClient({
  apiKey: process.env.ANECTICO_API_KEY,
  serviceName: 'orders-api',
}).start();

app.use(anecticoExpressMiddleware(anectico, {
  requestIdentityHook: request =>
    typeof request.headers['x-customer-id'] === 'string'
      ? request.headers['x-customer-id']
      : undefined,
}));
// Register routes here.
app.use(anecticoExpressErrorHandler(anectico)); // Always last.

Resolve Express identity from server-authenticated request state with requestIdentityHook; the SDK scopes it to that request, propagates it to trusted downstream services, and applies it to the preload HTTP span. Do not call the process-global identify() method for each concurrent server request.

For Fastify, register the Anectico plugin before application hooks and routes. Resolve identity from authenticated request state; do not call the process-global identify() method for each concurrent server request.

import Fastify from 'fastify';
import { initNode, anecticoFastifyPlugin } from '@anectico/sdk/node';

const app = Fastify();
const anectico = await initNode({
  apiKey: process.env.ANECTICO_API_KEY,
  serviceName: 'payments-api',
  propagateTraceHeaderUrls: ['https://authorization.internal.example'],
});

await app.register(anecticoFastifyPlugin, {
  client: anectico,
  ignorePaths: ['/health', '/internal/*'],
  requestIdentityHook: request =>
    typeof request.headers['x-customer-id'] === 'string'
      ? request.headers['x-customer-id']
      : undefined,
});

The Fastify plugin keeps trace and identity context active across async lifecycle hooks and route handlers. It ends each request span once on a response, timeout, request-body abort, or response-socket disconnect. Exact and trailing-* ignorePaths suppress both the plugin span and the generic incoming HTTP span installed by the Node preload. When requestIdentityHook resolves an authenticated customer, that identity is applied to both spans so the complete server trace has one customer even though the preload span starts before Fastify authentication.

The middleware extracts W3C trace context. Identity baggage is ignored by default because a public client can forge it. Change trustIncomingIdentity to true only behind a gateway that strips and recreates client-supplied baggage.

For outbound Node http/https propagation, preload the SDK before application modules and allow only trusted destinations:

node --import @anectico/sdk/node/register ./app.mjs
import { initNode } from '@anectico/sdk/node';

const anectico = await initNode({
  apiKey: process.env.ANECTICO_API_KEY,
  serviceName: 'orders-api',
  propagateTraceHeaderUrls: ['https://billing.internal.example'],
});

Trusted Node fetch calls are traced and receive traceparent plus baggage automatically. The preload is still required when http/https modules may load before Anectico; the fetch wrapper itself is installed by initNode, either framework integration, or the preload.

The preload, initNode, Express middleware, and Fastify plugin share one idempotent setup path, so using them together does not register OpenTelemetry globals more than once. If your application installs a global propagator before Anectico, Anectico preserves it; include both W3C Trace Context and W3C Baggage in that propagator when you want anectico.distinct_id to cross trusted service boundaries.

If the application already owns a complete OpenTelemetry SDK, do not add the preload or let Anectico register a second set of providers and instrumentations. Start the application SDK first and pass openTelemetryMode: 'existing' to initNode(). Anectico then uses the registered global tracer, meter, and logger while the application retains flush and shutdown ownership. See OpenTelemetry migration.

NestJS, Apollo GraphQL, and Prisma

Use the same preload before the NestJS entry point:

node --require @anectico/sdk/node/register dist/main.js

For an ESM build, use node --import @anectico/sdk/node/register dist/main.mjs. The preload registers GraphQL and Prisma instrumentation before those packages load. Anectico records named operation spans, bounded resolver spans, and Prisma operation/query children without customer-authored spans.

Privacy defaults are fixed: GraphQL literal values become *, variables are not attached to spans, repeated list paths are merged, and trivial property resolvers are omitted. When Apollo returns GraphQL errors with HTTP 200, Anectico marks the named operation span as failed, records the original resolver exception, and adds graphql.error.count. Parameterize Prisma queries and avoid including secrets in exception messages.

If a fatal Node exception or unhandled rejection occurs during startup, Anectico prints the original error and stack to stderr before its best-effort flush. The SDK does not hide the diagnostic by installing its process handlers.

Next.js App Router and Server Actions

Use Next’s root instrumentation.ts hook to start one server client, and preload Anectico before the Next production server so generic inbound HTTP instrumentation is installed before Next imports Node’s HTTP modules:

NODE_OPTIONS='--require @anectico/sdk/node/register' next start
// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME !== 'nodejs') return;
  const { getAnectico } = await import('./src/observability');
  await getAnectico();
}

export async function onRequestError(error: unknown) {
  if (process.env.NEXT_RUNTIME !== 'nodejs') return;
  const { getAnectico } = await import('./src/observability');
  const anectico = await getAnectico();
  anectico.captureError(
    error instanceof Error ? error : new Error('Next request failed'),
    { tags: { 'error.mechanism': 'next.onRequestError' } },
  );
}

getAnectico() should memoize one initNode() promise for the process. Configure propagateTraceHeaderUrls with only the trusted origins that Server Actions call, including the application’s own origin when an Action uses an internal Route Handler. This lets the browser request, Server Action, outbound fetch, and Route Handler retain one trace and anectico.distinct_id:

const anectico = await initNode({
  apiKey: process.env.ANECTICO_API_KEY,
  serviceName: 'subscription-commerce',
  propagateTraceHeaderUrls: ['https://shop.example.com'],
});

Next emits App Router render, Action, Route Handler, and fetch spans through the global OpenTelemetry provider. For a business Action, give the active span a safe operation name and bounded correlation attributes with @opentelemetry/api. Never attach FormData, action payloads, cookies, authorization headers, payment tokens, or unbounded business objects. Use the same safe idempotency identifier on failure and retry, and capture the exception once so one failure remains visible without multiplying Issue occurrences.

Initialize @anectico/sdk/browser from a Client Component after hydration and start @anectico/sdk/replay only after consent. The browser key is intentionally public: scope it to the project and grant only ingest:write, analytics:write, and—when recording—replay:write. Keep management and read scopes on the server.

React and browser credentials

import { AnecticoClient } from '@anectico/sdk';
import { AnecticoErrorBoundary, AnecticoProvider } from '@anectico/sdk/react';

const anectico = await new AnecticoClient({
  apiKey: import.meta.env.VITE_ANECTICO_API_KEY,
  serviceName: 'web',
  release: import.meta.env.VITE_APP_RELEASE,
}).start();

root.render(
  <AnecticoProvider client={anectico}>
    <AnecticoErrorBoundary client={anectico} fallback={<ErrorPage />}>
      <App />
    </AnecticoErrorBoundary>
  </AnecticoProvider>,
);

Use a project-scoped custom key with ingest:write and analytics:write in browser builds. Add replay:write only when recording replay. Client-side feature flags additionally need flags:read; because that exposes flag configuration to the client, never put secrets in targeting rules or payloads. Never expose other read, management, or MCP scopes. Restrict cross-origin propagation with propagateTraceHeaderCorsUrls.

Logs, replay, and AI calls

Use the client’s log and error methods for manual signals. Import @anectico/sdk/replay only on pages where browser replay is needed; inputs are masked by default. The @anectico/sdk/openai and @anectico/sdk/anthropic wrappers record non-streaming model calls, with content capture off by default.

Verify and recover

Generate one request for a known test customer, then open Customers and confirm the error, trace, and logs share that customer and trace. If nothing arrives, enable ANECTICO_DEBUG=true, confirm the key and endpoint, and stop the process gracefully before checking again.