Setup
Install packages
npm install @abvdev/tracing @abvdev/otel
Add credentials
Add your ABV credentials to your environment variables. Make sure that you have a .env file in your project root and a package like dotenv to load the variables.
ABV_API_KEY="sk-abv-..."
ABV_BASE_URL="https://app.abv.dev" # US region
# ABV_BASE_URL="https://eu.app.abv.dev" # EU region
Initialize OpenTelemetry
Install the OpenTelemetry Node SDK package:
npm install @opentelemetry/sdk-node
Create a instrumentation.ts file that initializes the OpenTelemetry NodeSDK and registers the ABVSpanProcessor.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ABVSpanProcessor } from "@abvdev/otel";
const sdk = new NodeSDK({
spanProcessors: [new ABVSpanProcessor()],
});
sdk.start();
Modify instrumentation.ts file to use dotenv package to load the variables.
Additional parameters are provided to get trace visible in the UI immediately.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ABVSpanProcessor } from "@abvdev/otel";
const sdk = new NodeSDK({
spanProcessors: [
new ABVSpanProcessor({
apiKey: process.env.ABV_API_KEY,
baseUrl: process.env.ABV_BASE_URL,
exportMode: "immediate",
flushAt: 1,
flushInterval: 1,
additionalHeaders: {
"Content-Type": "application/json",
"Accept": "application/json"
}
})
],
});
sdk.start();
Import the instrumentation.ts file at the top of your application.
import "./instrumentation"; // Must be the first import
Masking
To prevent sensitive data from being sent to ABV, you can provide a mask function to the ABVSpanProcessor. This function will be applied to the input, output, and metadata of every observation.
The function receives an object { data }, where data is the stringified JSON of the attribute’s value. It should return the masked data.
instrumentation_masked.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ABVSpanProcessor } from "@abvdev/otel";
import { TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
import dotenv from "dotenv";
dotenv.config();
const sdk = new NodeSDK({
spanProcessors: [
new ABVSpanProcessor({
apiKey: process.env.ABV_API_KEY,
baseUrl: process.env.ABV_BASE_URL,
exportMode: "immediate",
flushAt: 1,
flushInterval: 1,
additionalHeaders: {
"Content-Type": "application/json",
"Accept": "application/json"
},
mask: ({ data }) => {
// A simple regex to mask credit card numbers
const maskedData = data.replace(
/\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g,
"***MASKED_CREDIT_CARD***"
);
return maskedData;
},
})
],
});
sdk.start();
Simple example with masked data
import "./instrumentation_masked";
import { startActiveObservation } from "@abvdev/tracing";
async function main() {
await startActiveObservation("my-masked-trace", async (span) => {
span.update({
input: "Hello, ABV! My card number is 5555-7777-3333-1111 ",
output: "This is my first trace! And my card number is 1111-1111-1111-1111",
});
});
}
main();
Masked data will be shown in UI:
Input: “Hello, ABV! My card number is ***MASKED_CREDIT_CARD*** ”
Output: “This is my first trace! And my card number is ***MASKED_CREDIT_CARD***“
Filtering Spans
You can provide a predicate function shouldExportSpan to the ABVSpanProcessor to decide on a per-span basis whether it should be exported to ABV.
Filtering spans may break the parent-child relationships in your traces. For example, if you filter out a parent span but keep its children, you may see “orphaned” observations in the ABV UI. Consider the impact on trace structure when configuring shouldExportSpan.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ABVSpanProcessor, ShouldExportSpan } from "@abvdev/otel";
import dotenv from "dotenv";
dotenv.config();
// Example: Filter out all spans from the 'express' instrumentation
const shouldExportSpan: ShouldExportSpan = ({ otelSpan }) =>
otelSpan.instrumentationScope.name !== "express";
const sdk = new NodeSDK({
spanProcessors: [new ABVSpanProcessor({ shouldExportSpan })],
});
sdk.start();
If you want to include only LLM observability related spans, you can configure an allowlist like so:
import { ShouldExportSpan } from "@abvdev/otel";
const shouldExportSpan: ShouldExportSpan = ({ otelSpan }) =>
["abv-sdk", "ai"].includes(otelSpan.instrumentationScope.name);
Sampling
ABV respects OpenTelemetry’s sampling decisions. You can configure a sampler in your OTEL SDK to control which traces are sent to ABV. This is useful for managing costs and reducing noise in high-volume applications.
Here is an example of how to configure a TraceIdRatioBasedSampler to send only 20% of traces:
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ABVSpanProcessor } from "@abvdev/otel";
import { TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
import dotenv from "dotenv";
dotenv.config();
const sdk = new NodeSDK({
// Sample 20% of all traces
sampler: new TraceIdRatioBasedSampler(0.2),
spanProcessors: [
new ABVSpanProcessor({
apiKey: process.env.ABV_API_KEY,
baseUrl: process.env.ABV_BASE_URL,
exportMode: "immediate",
flushAt: 1,
flushInterval: 1,
additionalHeaders: {
"Content-Type": "application/json",
"Accept": "application/json"
}
})
],
});
sdk.start();
For more advanced sampling strategies, refer to the OpenTelemetry JS Sampling Documentation.
Managing trace and observation IDs
In ABV, every trace and observation has a unique identifier. Understanding their format and how to set them is useful for integrating with other systems.
- Trace IDs are 32-character lowercase hexadecimal strings, representing 16 bytes of data
- Observation IDs (also known as Span IDs in OpenTelemetry) are 16-character lowercase hexadecimal strings, representing 8 bytes
While the SDK handles ID generation automatically, you may manually set them to align with external systems or create specific trace structures. This is done using the parentSpanContext option in tracing methods.
When starting a new trace by setting a traceId, you must also provide an arbitrary parent-spanId for the parent observation. The parent span ID value is irrelevant as long as it is a valid 16-hexchar string as the span does not actually exist but is only used for trace ID inheritance of the created observation.
You can create valid, deterministic trace IDs from a seed string using createTraceId. This is useful for correlating ABV traces with IDs from external systems, like a support ticket ID.
import "./instrumentation";
import { createTraceId, startObservation } from "@abvdev/tracing";
async function main() {
const externalId = "support-ticket-11112025";
// Generate a valid, deterministic traceId from the external ID
const abvTraceId = await createTraceId(externalId);
console.log(abvTraceId);
// You can now start a new trace with this ID
const rootSpan = startObservation(
"process-ticket",
{
input: "there is an input",
output: "here you are an output"
},
{
parentSpanContext: {
traceId: abvTraceId,
spanId: "0123456789abcdef", // A valid 16 hexchar string; value is irrelevant as parent span does not exist but only used for inheritance
traceFlags: 1, // mark trace as sampled
},
}
);
rootSpan.end();
// Later, you can regenerate the same traceId to score or retrieve the trace
const scoringTraceId = await createTraceId(externalId);
// scoringTraceId will be the same as abvTraceId
console.log(scoringTraceId);
}
main();
You may also access the current active trace ID via the getActiveTraceId function:
import "./instrumentation";
import { startActiveObservation, getActiveTraceId } from "@abvdev/tracing";
async function main() {
await startActiveObservation("my-first-trace", async (span) => {
span.update({
input: "Hello, ABV!",
output: "This is my first trace!",
});
const traceId = getActiveTraceId();
console.log(`Current trace ID: ${traceId}`);
});
}
main();
Logging
You can configure the global SDK logger to control the verbosity of log output. This is useful for debugging.
In code:
import { configureGlobalLogger, LogLevel } from "@abvdev/core";
// Set the log level to DEBUG to see all log messages
configureGlobalLogger({ level: LogLevel.DEBUG });
import "./instrumentation";
import { startActiveObservation, getActiveTraceId } from "@abvdev/tracing";
async function main() {
await startActiveObservation("my-first-trace", async (span) => {
span.update({
input: "Hello, ABV!",
output: "This is my first trace!",
});
const traceId = getActiveTraceId();
console.log(`Current trace ID: ${traceId}`);
});
}
main();
Available log levels are DEBUG, INFO, WARN, and ERROR.
Via environment variable:
You can also set the log level using the ABV_LOG_LEVEL environment variable.
Serverless environments
In short-lived environments such as serverless functions (e.g., Vercel Functions, AWS Lambda), you must explicitly flush the traces before the process exits or the runtime environment is frozen.
Generic Serverless function
Export the processor from your OTEL SDK setup file in order to flush it later.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ABVSpanProcessor } from "@abvdev/otel";
// Export the processor to be able to flush it
export const abvSpanProcessor = new ABVSpanProcessor();
const sdk = new NodeSDK({
spanProcessors: [abvSpanProcessor],
});
sdk.start();
In your serverless function handler, call forceFlush() on the span processor before the function exits.
import { abvSpanProcessor } from "./instrumentation";
export async function handler(event, context) {
// ... your application logic ...
// Flush before exiting
await abvSpanProcessor.forceFlush();
}
Vercel Cloud Functions
Export the processor from your instrumentation.ts file in order to flush it later.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ABVSpanProcessor } from "@abvdev/otel";
// Export the processor to be able to flush it
export const abvSpanProcessor = new ABVSpanProcessor();
const sdk = new NodeSDK({
spanProcessors: [abvSpanProcessor],
});
sdk.start();
In Vercel Cloud Functions, please use the after utility to schedule a flush after the request has completed.
import { after } from "next/server";
import { abvSpanProcessor } from "./instrumentation.ts";
export async function POST() {
// ... existing request logic ...
// Schedule flush after request has completed
after(async () => {
await abvSpanProcessor.forceFlush();
});
// ... send response ...
}
Isolated tracer provider
The ABV JS SDK uses the global OpenTelemetry TracerProvider to attach its span processor and create tracers that emit spans. This means that if you have an existing OpenTelemetry setup with another destination configured for your spans (e.g., Datadog), you will see ABV spans in those third-party observability backends as well.
If you’d like to avoid sending ABV spans to third-party observability backends in your existing OpenTelemetry setup, you will need to use an isolated OpenTelemetry TracerProvider that is separate from the global one.
If you would like to simply limit the spans that are sent to ABV and you have no third-party observability backend where you’d like to exclude ABV spans from, see filtering spans instead.
Using an isolated TracerProvider may break the parent-child relationships in your traces, as all TracerProviders still share the same active span context. For example, if you have an active parent span from the global TracerProvider but children from an isolated TracerProvider, you may see “orphaned” observations in the ABV UI. Consider the impact on trace structure when configuring an isolated tracer provider.
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { setABVTracerProvider } from "@abvdev/tracing";
// Create a new TracerProvider and register the ABVSpanProcessor
// do not set this TracerProvider as the global TracerProvider
const abvTracerProvider = new NodeTracerProvider({
spanProcessors: [new ABVSpanProcessor()],
})
// Register the isolated TracerProvider
setABVTracerProvider(abvTracerProvider)
Multi-project Setup
You can configure the SDK to send traces to multiple ABV projects. This is useful for multi-tenant applications or for sending traces to different environments. Simply register multiple ABVSpanProcessor instances, each with its own credentials.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { ABVSpanProcessor } from "@abvdev/otel";
import dotenv from "dotenv";
dotenv.config();
const sdk = new NodeSDK({
spanProcessors: [
new ABVSpanProcessor({
apiKey: "sk-abv-b14d5b78-de4e-4699-b48e-173d8dde1c12",
baseUrl: process.env.ABV_BASE_URL,
exportMode: "immediate",
flushAt: 1,
flushInterval: 1,
additionalHeaders: {
"Content-Type": "application/json",
"Accept": "application/json"
}
}),
new ABVSpanProcessor({
apiKey: "sk-abv-79dc2bb3-2c9e-4e11-80cd-f06b019b72f9",
baseUrl: process.env.ABV_BASE_URL,
exportMode: "immediate",
flushAt: 1,
flushInterval: 1,
additionalHeaders: {
"Content-Type": "application/json",
"Accept": "application/json"
}
})
],
});
sdk.start();
This configuration will send every trace to both projects. You can also configure a custom shouldExportSpan filter for each processor to control which traces go to which project.
Custom scores from browser
Sending custom scores directly from the browser is not yet supported in the TypeScript SDK.