BrowserStack AI Evals
Tracing

Setup & Installation

Install the BrowserStack AI Evals SDK and initialize the client for TypeScript, Python, or Java.

Setup & Installation

Installation

npm install @browserstack/ai-sdk
pip install browserstack-ai-sdk

Gradle:

implementation 'com.browserstack:aisdk:LATEST_VERSION'

Maven:

<dependency>
  <groupId>com.browserstack</groupId>
  <artifactId>aisdk</artifactId>
  <version>LATEST_VERSION</version>
</dependency>

Client Initialization

The SDK provides three initialization paths. Pick the one that matches your use case:

1. Auto-instrumentation (simplest)

Import the instrument bundle as the first import in your entry file. This patches supported LLM libraries (OpenAI, Anthropic, etc.) at startup and reads credentials from env vars.

// Must be the very first import — before any LLM provider imports
import '@browserstack/ai-sdk/instrument';

import OpenAI from 'openai';

const openai = new OpenAI();
// All OpenAI calls are now auto-traced

2. Observe.init() — global init with control

Use when you want explicit control over when tracing starts, or need to call Observe.setAttribute() / Observe.getClient() from anywhere in your code.

import { Observe } from '@browserstack/ai-sdk';

await Observe.init({
  publicKey: process.env.AISDK_PUBLIC_KEY,
  secretKey: process.env.AISDK_SECRET_KEY,
});

3. new AISDK() — explicit client

Use when you need a client instance to call CRUD methods (datasets, prompts, experiments) or manual tracing (client.trace(...), client.score(...)).

import { AISDK } from '@browserstack/ai-sdk';

const client = new AISDK({
  publicKey: process.env.AISDK_PUBLIC_KEY,
  secretKey: process.env.AISDK_SECRET_KEY,
});

These paths are not mutually exclusive. A common setup is auto-instrument at the entry file, then new AISDK() wherever you need CRUD or manual tracing.

The SDK provides two initialization paths that work together:

  • Observe.init() — sets up global auto-instrumentation. Patches OpenAI, Anthropic, etc. at import time.
  • AISDK() — client instance for CRUD APIs (datasets, prompts, experiments) and manual tracing.

Most applications use both: Observe.init() at startup for auto-tracing, plus AISDK() when you need to call CRUD methods.

import os
from browserstack_ai_sdk import Observe

Observe.init(
    public_key=os.environ["AISDK_PUBLIC_KEY"],
    secret_key=os.environ["AISDK_SECRET_KEY"],
)

Create a client for CRUD operations and manual tracing:

import os
from browserstack_ai_sdk import AISDK

client = AISDK(
    public_key=os.environ["AISDK_PUBLIC_KEY"],
    secret_key=os.environ["AISDK_SECRET_KEY"],
)

TestOps is the main entry point. All clients are lazily initialized on first access.

import com.browserstack.aisdk.TestOps;

// From environment variables (recommended)
TestOps sdk = TestOps.fromEnv();

// Or using the builder pattern
TestOps sdk = TestOps.builder()
    .publicKey(System.getenv("AISDK_PUBLIC_KEY"))
    .secretKey(System.getenv("AISDK_SECRET_KEY"))
    .build();

The Observe Namespace

Beyond init(), the Observe namespace exposes helpers for interacting with the current trace.

MethodDescription
Observe.init(config?)Initialize global tracing. Returns Promise<void>.
Observe.getClient(config?)Get the singleton AISDK instance created by init().
Observe.setAttribute(key, value)Attach an attribute to the current trace.
Observe.TraceAttributeEnum of standard attribute keys: SESSION_ID, USER_ID, TRACE_INPUT, TRACE_OUTPUT.
Observe.withTrace(name, baggage, fn)Run fn inside a named trace, optionally propagating baggage.
Observe.getBaggage() / Observe.setBaggage(str)Read/write OpenTelemetry baggage for cross-service context propagation.
Observe.trace / Observe.context / Observe.propagationRaw OpenTelemetry APIs for advanced use cases.

Example — attach a session ID to the current trace:

import { Observe } from '@browserstack/ai-sdk';

Observe.setAttribute(Observe.TraceAttribute.SESSION_ID, 'session-abc-123');
Observe.setAttribute(Observe.TraceAttribute.USER_ID, 'user-42');
MethodDescription
Observe.init(**kwargs)Initialize global tracing. Returns Optional[TestOpsTracing].
Observe.shutdown()Shut down tracing and flush pending spans.
Observe.is_initialized()Returns True if init() has been called.
Observe.set_attribute(key, value)Attach an attribute to the current trace.
Observe.TraceAttributeEnum of standard attribute keys: SESSION_ID, USER_ID, etc.

Example — attach a session ID to the current trace:

from browserstack_ai_sdk import Observe

Observe.set_attribute(Observe.TraceAttribute.SESSION_ID, "session-abc-123")
Observe.set_attribute(Observe.TraceAttribute.USER_ID, "user-42")

Environment Variables

VariableDescription
AISDK_PUBLIC_KEYAPI public key (preferred)
AISDK_SECRET_KEYAPI secret key (preferred)
AISDK_WRITE_ONLY_KEYWrite-only key for tracing-only access
AISDK_REST_API_URLREST API endpoint override
AISDK_INGESTION_URLTrace ingestion endpoint override

Set them in your .env file and initialize with no explicit credentials:

AISDK_PUBLIC_KEY=pk-...
AISDK_SECRET_KEY=sk-...
import { AISDK } from '@browserstack/ai-sdk';

const client = new AISDK(); // reads from env vars
VariableDescription
AISDK_PUBLIC_KEYAPI public key
AISDK_SECRET_KEYAPI secret key
AISDK_WRITE_ONLY_KEYWrite-only key for tracing-only access
AISDK_REST_API_URLREST API endpoint override
AISDK_INGESTION_URLTrace ingestion endpoint override

Call Observe.init() with no arguments to read from environment:

AISDK_PUBLIC_KEY=pk-...
AISDK_SECRET_KEY=sk-...
from browserstack_ai_sdk import Observe

Observe.init()  # reads from environment

The SDK reads credentials in this resolution order:

  1. Values set explicitly via Builder
  2. AISDK_* environment variables
  3. TESTOPS_* environment variables (legacy fallback)
  4. Hardcoded URL defaults
VariablePurposeRequired
AISDK_PUBLIC_KEYAPI public keyYes
AISDK_SECRET_KEYAPI secret keyYes
AISDK_REST_API_URLREST API endpointNo
AISDK_INGESTION_URLTrace ingestion endpointNo
AISDK_WEB_URLWeb app URLNo

The SDK also auto-loads a .env file from the current or parent directories. System environment variables always take precedence.

Constructor Options

new AISDK(config?: TestOpsConfig)
OptionTypeDefaultDescription
publicKeystringAISDK_PUBLIC_KEY envYour API public key.
secretKeystringAISDK_SECRET_KEY envYour API secret key.
writeOnlyKeystringAISDK_WRITE_ONLY_KEY envWrite-only key for tracing-only access (no read APIs).
enabledbooleantrueEnable or disable the SDK.
environmentstringEnvironment name attached to all traces (e.g. "production").
fileLogbooleanfalseEnable file-based logging for debugging.
family4 | 6Force IPv4 or IPv6 connections.
agentTestOpsAgentCustom http.Agent / https.Agent for connection pooling.
flushIntervalnumberSDK defaultInterval in ms between auto-flushes.
maxBatchSizenumberSDK defaultMaximum number of events per flush batch.

baseUrl is not a constructor option — to override the API endpoint, use the AISDK_REST_API_URL environment variable.

Observe.init() parameters:

ParameterTypeDefaultDescription
public_keystrAISDK_PUBLIC_KEY envYour public API key.
secret_keystrAISDK_SECRET_KEY envYour secret API key.
write_only_keystrAISDK_WRITE_ONLY_KEY envWrite-only key for tracing-only access.
hoststrIngestion host override.
environmentstrEnvironment name attached to all traces.
service_namestr"default"Service name attached to spans.
service_versionstrService version attached to spans.
tracing_enabledboolTrueEnable or disable tracing.
flush_atintSDK defaultNumber of events to buffer before flushing.
flush_intervalfloatSDK defaultInterval in seconds between auto-flushes.
sample_ratefloat1.0Trace sampling rate between 0.0 and 1.0.

AISDK() accept the same parameters.

Optional builder methods:

TestOps sdk = TestOps.builder()
    .publicKey("...")
    .secretKey("...")
    .restApiUrl("https://your-proxy.example.com")     // override REST endpoint
    .ingestionUrl("https://your-proxy.example.com")   // override ingestion endpoint
    .webAppUrl("https://your-proxy.example.com")      // override web app URL
    .build();

Write-Only Key

For tracing-only deployments where read access to datasets, prompts, or other APIs should be restricted:

import { AISDK } from '@browserstack/ai-sdk';

const client = new AISDK({
  writeOnlyKey: process.env.AISDK_WRITE_ONLY_KEY,
});
from browserstack_ai_sdk import Observe

Observe.init(write_only_key=os.environ["AISDK_WRITE_ONLY_KEY"])

The Java SDK does not currently support a separate write-only key. Use the standard public/secret key pair for all access.

Limitations

With a write-only key, only tracing/ingestion endpoints are accessible. The following will fail with an authentication error:

  • client.prompt.get() / client.prompt.create() / client.prompt.list()
  • client.datasets.* (list, create, items, runs)
  • client.experiments.* / client.experimentRuns.*
  • client.evalsList.* / client.evals.evaluate()
  • client.tools.*

Use the full public/secret key pair for any of these.

Flush and Shutdown

The SDK batches trace data and sends it asynchronously. Always flush before your process exits to ensure all pending data is sent.

import { AISDK } from '@browserstack/ai-sdk';

const client = new AISDK({
  publicKey: process.env.AISDK_PUBLIC_KEY,
  secretKey: process.env.AISDK_SECRET_KEY,
});

try {
  // ... application logic
} finally {
  await client.shutdown(); // flushes and closes connections
}
MethodDescription
flush(): Promise<void>Flush all pending trace events.
flushAsync(): Promise<void>Same as flush() but resolves immediately and flushes in the background.
shutdown(): Promise<void>Flush synchronously and shut down the client.
shutdownAsync(): Promise<void>Non-blocking shutdown variant.

In short-lived scripts or serverless functions, always call await client.shutdown() before the process exits. Without it, buffered traces may be lost.

from browserstack_ai_sdk import AISDK

client = AISDK(
    public_key=os.environ["AISDK_PUBLIC_KEY"],
    secret_key=os.environ["AISDK_SECRET_KEY"],
)

try:
    # ... your application logic
    pass
finally:
    client.flush()

For scripts or short-lived processes, use the client as a context manager:

from browserstack_ai_sdk import AISDK

with AISDK(
    public_key=os.environ["AISDK_PUBLIC_KEY"],
    secret_key=os.environ["AISDK_SECRET_KEY"],
) as client:
    # LLM calls here are automatically flushed on exit
    pass

If you use the @observe decorator to trace functions, flushing is still required at process exit. See the Manual Tracing page for decorator usage.

// Flush without shutting down (e.g., between tests)
sdk.flush();

// Flush and shut down background threads (call at application exit)
sdk.shutdown();

Use a JVM shutdown hook for long-running processes:

TestOps sdk = TestOps.fromEnv();

Runtime.getRuntime().addShutdownHook(new Thread(sdk::shutdown));

Traces are batched and flushed automatically every 5 seconds or when the buffer reaches 5 items. Always call sdk.flush() before exit to ensure all buffered traces are sent.