Tracing
Tracing records what your application does as spans you can inspect in Braintrust. The .NET SDK is built on OpenTelemetry: you instrument AI calls by wrapping your provider clients (see Wrappers), and you trace your own application code with theActivitySource Braintrust provides. The APIs below set up that pipeline and let you flush and link to your traces.
Braintrust.Get
Gets the global Braintrust instance, creating it on the first call and returning that same instance afterward. This is your entry point to the SDK: it reads configuration and sets up the OpenTelemetry pipeline that exports spans to Braintrust.
Call Get() with no arguments to configure from environment variables, which is the most common setup. To configure it in code, build a BraintrustConfig and pass it to Get(config) (see Configuration).
#skip-compile
Braintrust
Overloads:
Get(): readsBraintrustConfig.FromEnvironment()and auto-manages OpenTelemetry.Get(BraintrustConfig config, bool autoManageOpenTelemetry = true): uses the supplied config. SetautoManageOpenTelemetrytofalseto attach Braintrust to an OpenTelemetry pipeline you manage yourself.Of(BraintrustConfig config, bool autoManageOpenTelemetry = true): creates a new, non-global instance.
Braintrust.GetActivitySource
Returns the System.Diagnostics.ActivitySource Braintrust uses to create spans. Pass it to the provider wrap methods, and use it to wrap your own application code so traced AI calls nest under your operations.
#skip-compile
System.Diagnostics.ActivitySource
Braintrust traces wrapped provider calls, but not the code around them, so wrapping that work in a span shows its structure in the trace and nests any traced AI calls underneath.
#skip-compile
Braintrust.GetProjectUriAsync
Builds the Braintrust UI URL for the configured organization and project. Use it to link from your own app or logs straight to the project in Braintrust.
#skip-compile
Task<Uri>
Use
uri.AbsoluteUri when converting the result to a string. Uri.ToString() unescapes the path, which produces broken links for org or project names that contain a space.BraintrustTracing.ForceFlush
Forces any pending spans to be sent to Braintrust, returning once they’re flushed or the timeout elapses.
You usually don’t need this. When the SDK manages OpenTelemetry (the default), it registers a process-exit hook that flushes on termination, so even short-lived processes export correctly with no extra work.
Call ForceFlush only when you opt out of auto-management (autoManageOpenTelemetry: false) and wire Braintrust into your own tracer provider, where that hook isn’t registered.
#skip-compile
bool (true when the flush completes within the timeout, or when no tracer provider exists)
Parameters:
timeoutMilliseconds(int): maximum time to wait for the flush. Defaults to10000.
Evaluations
An evaluation runs your task over a set of cases, scores each output, and reports the results, which is how you measure quality and catch regressions as you change prompts or models. These APIs build and run evaluations from your C# code.Braintrust.EvalBuilder
Defines an evaluation in code for input type TInput and output type TOutput: you give it cases, a task function, and one or more scorers, then call BuildAsync() to get an Eval. Call RunAsync() on it to run the task over every case, score the outputs, and log an experiment.
#skip-compile
Eval<TInput, TOutput>.Builder
Builder methods (each returns the Builder for chaining, except BuildAsync()):
Name(string)→Builder: experiment name.Cases(params DatasetCase[])→Builder: inline evaluation cases. Provide this orDataset().Dataset(IDataset)→Builder: run against a dataset instead of inlineCases. Pass an in-memory dataset fromDataset.Of(...)or a remote dataset frombraintrust.FetchDatasetAsync(...). When the eval uses a remote dataset, the experiment records the dataset id and version, and each result row links back to its source dataset record.TaskFunction(Func<TInput, TOutput>)→Builder(required): the task that produces the output to score. An async overload takingFunc<TInput, Task<TOutput>>is also available.Scorers(params IScorer[])→Builder: scorers to apply to each case. Required unlessClassifiersis set.Classifiers(params IClassifier[])→Builder: classifiers to apply to each case. Required unlessScorersis set.Tags(params string[])→Builder: experiment-level tags.Metadata(IDictionary<string, object>)→Builder: experiment-level metadata.MaxConcurrency(int?)→Builder: cap on how many cases run in parallel.ApiClient(BraintrustOpenApiClient)→Builder: supply a caller-owned API client. When omitted, the eval creates and owns its own client, which is disposed when the eval is disposed.BuildAsync()→Task<Eval<TInput, TOutput>>: builds the eval.
eval.RunAsync(), which returns an EvalResult. Call result.CreateReportString() for a human-readable CLI summary.
Eval<TInput, TOutput> implements IDisposable. Disposing the eval closes the HttpClient it opened for itself, so use using var eval = .... A BraintrustOpenApiClient you pass to ApiClient() is left for you to dispose.IScorer<TInput, TOutput>
Measures how good your task’s output is. Implement Score, which receives a TaskResult and returns one or more Score values. Implement the interface when you need full control over scoring.
#skip-compile
FunctionScorer<TInput, TOutput>
Creates a scorer from a function, without defining a class. Reach for it when your scoring logic is simple, such as comparing the output to the expected value.
#skip-compile
FunctionScorer(string name, Func<TOutput, TOutput, double> scorerFn): synchronous scorer, scoring from(expected, actual).FunctionScorer(string name, Func<TOutput, TOutput, Task<double>> scorerFn): asynchronous scorer.
IClassifier<TInput, TOutput> and FunctionClassifier<TInput, TOutput>
Categorize a task’s output instead of scoring it numerically. A classifier returns structured Classification items (an id, plus optional label and metadata) rather than a number, which is useful for labeling outputs by topic, intent, or failure type. Implement IClassifier’s Classify method, or create one from a function with FunctionClassifier.
Datasets
A dataset is the set of cases an evaluation runs against. The .NET SDK supports in-memory datasets defined in code and remote datasets fetched from Braintrust.Braintrust.FetchDatasetAsync
Fetches a dataset by name from the configured default project and returns it as an IDataset<TInput, TOutput> you can pass to the eval builder. The dataset id is resolved immediately, but rows are fetched lazily, a page at a time, as the eval reads them. When the eval uses a remote dataset, the experiment records the dataset id and version, and each result row links back to its source dataset record.
#skip-compile
Task<IDataset<TInput, TOutput>>
Parameters:
datasetName(string, required): name of the dataset within the configured project.version(string?): transaction id to pin the read to. Whennull(the default), the latest version is resolved at the start of each enumeration, so a dataset written to mid-run still produces a consistent snapshot.inputConverter(Func<JsonElement, TInput>?): deserializes a row’sinputfield. Whennull, the field is deserialized intoTInputautomatically.expectedConverter(Func<JsonElement, TOutput>?): deserializes a row’sexpectedfield. Becauseexpectedis optional in the dataset schema, rows that omit it cannot be read without a converter when the default deserializer cannot produce aTOutputfrom a null element.
DatasetCase.Of
A dataset case is a single example: an input paired with the output you expect. Use Of(input, expected) for the common case, or the longer overloads to attach case-level tags and metadata.
#skip-compile
DatasetCase<TInput, TOutput>
Overloads:
DatasetCase.Of(TInput input, TOutput expected): input and expected output.DatasetCase.Of(TInput input, TOutput expected, IReadOnlyList<string> tags): adds case-level tags.DatasetCase.Of(TInput input, TOutput expected, IReadOnlyList<string> tags, IReadOnlyDictionary<string, object> metadata): adds case-level tags and metadata.
Dataset.Of
Groups several cases into an in-memory dataset you can hand to an evaluation with the builder’s Dataset(...) method, as an alternative to passing cases inline. Build one with Dataset.Of(params DatasetCase[]).
#skip-compile
IDataset<TInput, TOutput>
Dataset.FetchFromBraintrustAsync
Fetches a dataset from Braintrust by project name and dataset name. Use this when you need a dataset outside the configured default project, or when you want to share a BraintrustOpenApiClient you already own.
#skip-compile
Task<IDataset<TInput, TOutput>>
Parameters:
apiClient(BraintrustOpenApiClient, required): caller-owned client. The method does not dispose it.projectName(string, required): project containing the dataset.datasetName(string, required): name of the dataset within that project.version(string?): transaction id to pin to.nullresolves the latest version at the start of each enumeration.inputConverter/expectedConverter: same as onFetchDatasetAsync.
Dataset.FromId
Returns a dataset object for a known dataset id without making any API requests. Rows are only fetched when the dataset is enumerated.
#skip-compile
IDataset<TInput, TOutput>
Parameters match FetchFromBraintrustAsync, except projectName is replaced by datasetId (string, required).
Wrappers
The .NET SDK instruments AI provider clients by wrapping them. Each wrapper takes theActivitySource from GetActivitySource() and returns an instrumented client whose calls are traced to Braintrust. For setup and the full list of supported integrations, see .NET SDK integrations.
BraintrustOpenAI.WrapOpenAI
Creates an instrumented OpenAIClient. Every call on the returned client is traced.
#skip-compile
OpenAIClient
Parameters:
activitySource(ActivitySource, required): source fromGetActivitySource().openAIApiKey(string, required): OpenAI API key.options(OpenAIClientOptions): optional OpenAI client options. Defaults tonull.
WithBraintrust (Anthropic)
Wraps an Anthropic client so every Messages.Create call (including streaming) emits a span. It’s an extension method, so you call it on the client itself.
#skip-compile
IAnthropicClient
Overloads:
WithBraintrust(bool captureMessageContent = true): uses the global Braintrust instance.WithBraintrust(ActivitySource activitySource, bool captureMessageContent = true): uses the suppliedActivitySource.
captureMessageContent to false to omit message content from spans.
BraintrustAzureOpenAI.WrapAzureOpenAI
Creates an instrumented AzureOpenAIClient, reusing the OpenAI instrumentation pipeline.
#skip-compile
AzureOpenAIClient
Overloads:
WrapAzureOpenAI(ActivitySource activitySource, Uri endpoint, string apiKey, ...): API key authentication.WrapAzureOpenAI(ActivitySource activitySource, Uri endpoint, TokenCredential credential, ...): Microsoft Entra ID authentication.
.WithBraintrust(activitySource) on a client you already created.
Agent Framework tracing
Traces Microsoft Agent Framework agents. Add the middleware to aChatClientBuilder, then wrap your agent.
#skip-compile
UseBraintrustTracing(activitySource)→ChatClientBuilder: traces both LLM calls and function calls.UseBraintrustLLMTracing(activitySource)→ChatClientBuilder: traces LLM calls only.UseBraintrustFunctionTracing(activitySource)→ChatClientBuilder: traces function (tool) calls only.WithBraintrustAgentTracing(activitySource)→AIAgent: wraps an agent so each run is captured as a span.
captureMessageContent flag, and function tracing accepts a captureToolArguments flag, to turn off content capture.
Configuration
Configure the SDK with environment variables, or programmatically withBraintrustConfig.Of, then pass the config to Braintrust.Get(config).
#skip-compile
Environment variables
BRAINTRUST_API_KEY(required): Braintrust API key. Also discoverable from a.env.braintrustfile.BRAINTRUST_DEFAULT_PROJECT_NAME: project that traced spans route to. Defaults todefault-dotnet-project.BRAINTRUST_DEFAULT_PROJECT_ID: project UUID. Takes precedence over the project name.BRAINTRUST_API_URL: Braintrust API URL. Defaults tohttps://api.braintrust.dev.BRAINTRUST_APP_URL: Braintrust app URL, used for permalinks. Defaults tohttps://www.braintrust.dev.BRAINTRUST_DEBUG: enable debug logging. Defaults tofalse.BRAINTRUST_ENABLE_TRACE_CONSOLE_LOG: print spans to the console. Defaults tofalse.BRAINTRUST_REQUEST_TIMEOUT: request timeout in seconds. Defaults to30.BRAINTRUST_TRACES_PATH: OpenTelemetry traces endpoint path. Defaults to/otel/v1/traces.BRAINTRUST_LOGS_PATH: OpenTelemetry logs endpoint path. Defaults to/otel/v1/logs.BRAINTRUST_ENVIRONMENT_TYPE: environment type for span-origin provenance (for example,ciorserver). Detected automatically from common CI, server, and .NET deployment runtimes when unset.BRAINTRUST_ENVIRONMENT_NAME: environment name for span-origin provenance (for example,github_actionsoraws_lambda). Only used whenBRAINTRUST_ENVIRONMENT_TYPEis also set.
API client
The .NET SDK ships a generated client for the Braintrust REST API, built from Braintrust’s public OpenAPI spec. Reach for it when you need direct REST access beyond what the higher-level tracing and eval APIs cover. The generated client ships inside theBraintrust.Sdk package and requires no separate install.
BraintrustOpenApiClient
BraintrustOpenApiClient is the SDK’s HTTP client. Its Api property gives you the generated client, already configured with your base URL, API key, and timeout.
#skip-compile
Braintrust instance via the OpenApiClient property:
#skip-compile
Objects property holds the results. Pass the last ID as starting_after to walk further pages.
The generated client is not covered by the SDK’s own compatibility promises: bumping the pinned OpenAPI spec ref can rename a class or change a signature.
DefaultBraintrustApiClient, BraintrustApiClient, and IBraintrustApiClient are deprecated as of v0.2.12. Use BraintrustOpenApiClient and its Api property instead.