github.com/Project-White-Rabbit/bitfab-go. It requires Go 1.25 or later. Runtime dependencies are github.com/google/uuid, go.opentelemetry.io/otel, go.opentelemetry.io/otel/trace, and go.opentelemetry.io/otel/sdk.
Framework Integrations
No framework-native adapters are shipped for Go yet. Instrument Go code manually withclient.Span or client.Start. See the Go SDK guide for the walkthrough. The Frameworks overview page tracks framework coverage across every SDK.
Package Constants
Version is the value attached to SDK requests. The two byte limits are transport safeguards. The SDK enforces them automatically. They’re exported so a caller can validate capture sizes before tracing.
Constructor
NewClient
Option
WithServiceURL
WithEnabled
enabled is false, Span still executes the callback. Start returns a no-op *ActiveSpan. No data is sent to the API.
WithAPIKey
apiKey argument of NewClient. Use it to construct a client purely from options. Whichever one is set last wins.
WithStrict
NewClient panics instead of disabling tracing. It’s off by default. That way, a missing telemetry key never crashes the host app. Turn it on in standalone programs where an untraced run is a failure you want surfaced immediately.
Transport environment variables
Commit ref environment variables
CommitRef
commit_ref on every root trace completion. sha is the commit. branch is the checked-out branch, or nil when detached or unknown. dirty is true when the working tree had uncommitted or untracked changes, false when it was clean, and nil when the SDK could not tell, which is always the case when the ref came from environment variables rather than git. remote is the origin URL reduced to host/owner/repo with any credentials removed, so a CI checkout token never reaches the trace. root_sha is the repository’s first commit, so two checkouts of the same repository match even without a remote.
Resolution order is BITFAB_COMMIT_SHA, then the deploy platform’s build variables, then git in the process’s working directory (see Commit ref environment variables). Environment resolution is synchronous and free. The git path runs once per process in a goroutine, with a two second timeout per command, so it never sits on the code path that ran the traced function. A trace that completes before it lands ships without a commit_ref, and a process with neither variables nor a repository never sends one. The result, including a negative one, is memoized for the life of the process. Set BITFAB_DISABLE_COMMIT_REF to opt the process out entirely. Nil pointer fields serialize as JSON null.
type Client
Opaque struct. Construct via NewClient.
Span lookup types
(c *Client) Span
fn inside a traced span. fn’s return value is captured as the span output.
Returns: fn’s (any, error) return values.
Errors: Only fn’s own error. It’s captured on the span. It’s also returned to the caller. Tracing never fails the call. An unknown WithType value degrades to custom with a one-time warning, instead of returning an error. An internal instrumentation failure runs fn untraced instead of crashing the host.
(c *Client) Start
defer span.End().
Returns a child context that carries the span. It also returns an *ActiveSpan for recording data. When enabled is false, it returns the original context instead. The returned *ActiveSpan is then a zero value whose methods are all no-ops.
Spans exist only where you create them. Nesting between spans is automatic, but only among spans that already exist. Wrapping just the outermost function, for example, records a trace with a single node. See Instrumentation for more.
(c *Client) FlushTraces
timeout. Returns false when an export failed or the deadline expired. Use it for a mid-run flush, such as before reading a span back with GetTraceSpan.
(c *Client) Close
false when an export failed or the deadline expired. A closed client no longer records spans.
Go has no atexit. Always defer client.Close(...) in main().
(c *Client) GetFunction
(c *Client) GetTraceSpan
traceID is the canonical Bitfab trace ID. Set exactly one of the span’s Bitfab SpanLookup.ID or SpanLookup.Name. Name lookup defaults to the last match. Use FirstSpanOccurrence or SpanOccurrenceAt(index) to override it. A miss returns nil, nil.
Datasets
client.Datasets holds a *DatasetsClient for the authenticated organization. It exposes the same operations the Bitfab MCP tools expose to a coding agent.
Saveis an upsert keyed on(TraceFunctionKey, Name).Createdistruefor a new dataset andfalsewhen an existing one was updated. An emptyDescriptionleaves an existing description untouched.Listscopes toparams.TraceFunctionKeywhen set. The zero value lists every dataset in the organization.DatasetcarriesID,TraceFunctionKey,Name,Description(*string),TraceCount,Graders([]DatasetGraderRefwithIDandName),CreatedAt, andUpdatedAt.ListTracesreturnsDatasetTraceIDs{DatasetID, TraceIDs}. This is the same membership a replay selects withReplayOptions.DatasetID.AddTracesandAddGraderseach accept 1 to 100 ids. They report partial acceptance instead of failing outright. An id outside the organization, or under another trace function, comes back inSkippedTraceIDsorSkippedGraderIDs. An id already present comes back inAlreadyPresentTraceIDsorAlreadyAssignedGraderIDs.RemoveTracesnever deletes a trace. It only removes its membership in the dataset. Ids that were not members come back inNotPresentTraceIDs.RemoveGradersreportsNotAssignedGraderIDsthe same way.RerunGradersre-scores every trace in the dataset.RerunGradersOptions.GraderIDsdefaults to every assigned grader. Passing an unassigned id fails the call. With the zero-value options,RerunGraderswaits for the run to finish. It polls everyPollInterval, which defaults to 1 second, up toTimeout, which defaults to 90 seconds. It returns the lastRunstate it saw. SetNoWaitto return as soon as the run is queued instead. A request that matches an in-flight run joins it, reported asJoinedExisting. Cancellingctxwhile waiting returnsctx.Err().GraderRerunhasStatus(aGraderRerunStatus, one ofGraderRerunPending,GraderRerunRunning,GraderRerunCompleted,GraderRerunErrored, withTerminal()),GraderIDs,Progress(*GraderRerunProgresswhile running),Result(*GraderRerunResultwhen completed), andError.- A dataset id from another organization fails with a 404 status error.
Replay
(c *Client) Replay
Replay fetches historical traces for traceFunctionKey. It re-runs their recorded inputs through fn. It waits for every emitted trace to persist. Then it completes the resulting experiment.
fn must be a non-nil function. It may take context.Context as its first parameter, followed by any number of JSON-decodable typed parameters. It may return zero or more values, plus an optional final error. A single non-error return becomes ReplayItem.Result. Multiple returns become []any instead. Replay catches a panic from fn. It stores the panic as that item’s TraceError.
Every invocation is automatically wrapped in a root span under traceFunctionKey. A production function that uses Start and End receives the replay root’s context. As a result, its existing spans become children of that root.
type ReplayOptions
Limit defaults to 5. It accepts values from 1 through 5000. MaxConcurrency defaults to 10. TraceIDs accepts at most 100 IDs. When it’s present, it determines the item count on its own. Limit is then omitted from the start request.
When CodeChangeFiles is nil, replay first looks for BITFAB_CODE_CHANGE_PATH. If that’s not set, it captures the rename-aware Git diff against trunk instead. An explicit slice always wins over the automatic capture. Set DisableCodeChangeCapture, or the BITFAB_DISABLE_CODE_CHANGE_CAPTURE environment variable, to opt out entirely. BITFAB_CODE_CHANGE_BASE overrides trunk detection.
Mock defaults to MockMarked. MockNone and MockAll select the other two strategies. MockOverrides take precedence over overrides registered on the client. Both take precedence over the base strategy. Mock interception only applies to closure-style child Client.Span calls, because those calls own execution. Manual Start/End instrumentation cannot prevent caller-owned code from running.
DatasetID and DatasetIDs are the same selector at two counts, so set one of them: one dataset on DatasetID, several on DatasetIDs. Several replays the union of their traces, deduped where they overlap, graded by the union of their graders, and attributes the experiment to every one of them, so it appears under each dataset’s experiments.
A non-nil DBBranch enables a historical database branch. An empty DBBranchOptions uses the connected project’s defaults. Set MinCU, MaxCU, or WarmupSQL to tune provisioning instead. Resolution happens inside the bounded item workers.
Replay mock types
Resolve takes precedence over Value. A nil Value is a valid flat override. The first matching override wins. GetOriginalOutput fetches the matched historical output lazily. It memoizes that output per replay item.
mocked: true, mockTarget: "output", and a mockSource of recorded or override.
Bound replay functions
Client.Replay reject a mismatch before starting the experiment. Plain callables remain valid for handler-instrumented workflows with no declared Go root.
Historical database branch
GetCurrentReplayBranch returns that item’s *ReplayBranch. Outside replay, or when the source trace has no resolvable snapshot, it returns nil instead. DatabaseURL() is the only accessor that exposes the connection string. Calling it also marks the trace’s db_snapshot_usage.accessed flag. JSON encoding and String() both omit the URL. The branch also exposes its Neon branch ID, environment key, expiration, snapshot timestamp, console URL, read-only flag, region, and source trace ID as direct fields. Any other field the server adds later comes through Extra.
Resolution failures produce a *DBBranchReplayError on the item’s ReplayError. It preserves Code, OriginalTraceID, and Cause. ReplayItem.DBSnapshotRef and ReplayItem.DBBranchTimings expose the historical pin and the server-measured provisioning phases.
Replay input adaptation
fn. That same slice is stored as ReplayItem.Input. An adapter error becomes the item’s ReplayError. It does not stop other items from running.
Replay results and errors
ReplayItem carries everything about one replayed trace, including:
Input(the adapted recorded input),Result, andOriginalOutput- A compatible
Errormessage, plus the actualTraceErrorandReplayError - The replay’s own
DurationMS, plus the original trace’s duration, token, and model measurements - The replay’s own token usage
- The original trace and span lineage
- The new server
TraceID - The two trace outlines
TraceOutline is the replayed trace’s span tree. OriginalTraceOutline is the original trace’s span tree. Neither carries inputs or outputs. Each span in an outline records its name, type, nesting, start order, duration, tokens, model, errors, and whether it was mocked.
Both outlines are nil until the run completes, so they’re also nil in every OnItemFinish event. They’re nil against an older server that doesn’t build them, too. TraceOutline is nil for one more case, an item whose replay produced no trace at all.
These outlines exist for grading. Comparing the two trees shows whether a replay reached its output by the same path as the original.
Per-item function and setup errors stay on their items. A run-wide persistence or completion failure returns *ReplayError. Use errors.As and errors.Unwrap to inspect it without losing the partial items it collected.
Replay progress and serialization
OnItemStart fires when a worker begins an item. This happens before replay loads its inputs, prepares mocks, or resolves a database branch. Its Item field carries only the original trace and span lineage at that point, because the rest of ReplayItem doesn’t exist yet.
OnItemFinish fires exactly once per item, as it finishes. Items finish in completion order, not input order. OnItemFinish carries the full ReplayItem.
Both callbacks report running totals, Completed, Succeeded, Errored, and Total. Replay doesn’t know pass or fail at finish time, since verdicts are assigned later. The totals only split runs that completed without error from runs that errored.
A panicking callback is recovered. It never crashes the run.
ReportReplayProgress writes one @@bitfab:progress {json} line to stderr. Pass it from both callbacks:
OnItemFinish flushes the finished trace. When that trace’s delivery is acknowledged, OnItemFinish fills the event’s server TraceID directly from the successful ingestion response. If per-item delivery can’t be confirmed, TraceID stays nil in the event instead. The run-wide persistence barrier then falls back to the status endpoint before filling in the final result. Replay aggregates token usage only at completion. Because of that, token usage appears only in the final ReplayResult, not in progress events. A panic inside a callback is swallowed.
If BITFAB_REPLAY_RESULT_PATH is set, a successful Replay writes the structured result JSON there with one trailing newline.
Payload serialization helpers
Error, Unwrap, MarshalJSON, String, and Format are documented with their owning replay types, rather than as independent entry points. TraceState is exported. This lets current-span handles share state across package boundaries. It’s transport plumbing, though, not an application-facing API.
type SpanFunc
type SpanOption
WithName
traceFunctionKey for Span, or the spanName arg for Start.
WithType
"llm", "agent", "function", "guardrail", "handoff", "custom". An unknown value warns once. It then degrades to "custom" without changing the user’s result. Defaults to "custom".
WithMockOnReplay
Span for recorded-output substitution under MockMarked. It has no effect outside replay. A selected occurrence that is absent from the historical tree returns an error without executing the real callback.
WithMockOutputType
T before a mocked closure-style Span returns it. Use it for structs, slices, and other concrete outputs that would otherwise arrive as JSON-shaped map[string]any or []any values. Primitive and deliberately dynamic outputs do not need it.
WithFunctionName
span_data.function_name.
WithInput
Span. For Start, use ActiveSpan.SetInput.
WithCaptureWhen
CaptureWhenNested records the span only when ctx contains an active Bitfab parent span. Without a parent, Span runs the callback untraced. Start returns the original context with a no-op ActiveSpan instead. The default is CaptureWhenAlways. An unknown value warns once. It then falls back to that default.
type Function
(*Client).GetFunction(key). Fluent wrapper that binds traceFunctionKey.
(f *Function) Span
(f *Function) Start
type ActiveSpan
Returned by Start. All methods are safe on nil, safe under recover(), and idempotent where noted.
(s *ActiveSpan) SetInput
nil receiver.
(s *ActiveSpan) SetOutput
(s *ActiveSpan) SetError
(s *ActiveSpan) AddContext
span_data.contexts. No-op when context == nil.
(s *ActiveSpan) SetPrompt
span_data.prompt. No-op on empty string.
(s *ActiveSpan) End
sync.Once. Sends the span in a background goroutine. Any panic inside the send path is recovered. That keeps the host app from crashing.
Trace-Level API
type ContextEntry
type CurrentSpan
GetCurrentSpan returns nil outside a span. Both methods are safe on a nil receiver. They return an empty string in that case.
type CurrentTrace
GetCurrentTrace.
(ct *CurrentTrace) TraceID
nil receiver. It returns an empty string in that case.
(ct *CurrentTrace) SetSessionID
(ct *CurrentTrace) SetName
name column. Unset, the trace is titled by its trace function key instead. Empty strings are ignored. It’s safe on a nil receiver.
(ct *CurrentTrace) SetMetadata
(ct *CurrentTrace) AddContext
(ct *CurrentTrace) Drop
- It scrubs any payloads that already raced out ahead of the flag, meaning the trace, its external trace, and any sibling spans
- It deletes the archived S3 objects
- It marks the trace
droppedinstead ofcompleted, keeping only a skeleton audit row
Drop is safe on a nil receiver. That makes it a no-op, so calling GetCurrentTrace(ctx).Drop() outside a span does nothing. Drop never panics.
GetCurrentTrace
nil when ctx carries no active span. Callers must nil-check.
Concurrency Model
- Nested span context is carried on
context.Context. It’s safe across goroutines that inherit that context - Goroutines that do not inherit the context will not see the parent span
- Spans are queued on a private OpenTelemetry batching worker. Each client gets its own worker, started lazily on the first span it sends. Queueing itself never blocks.
FlushTracesandCloseare what drain the worker - Trace state is stored in a mutex-protected package-level map, keyed by
traceID