Documentation
¶
Overview ¶
Production capture mode: config-gated upload of complete failed operation traces to the Repro It Cloud ingest endpoint (`/v1/capture-batches`).
Go port of sdk/reproit-backend-rs/src/capture.rs. Scan-time tracing stays untouched: this file only adds a place to hand a finished BackendTrace when no `x-reproit-trace` header exists. A stable 5xx or marked agent oracle, complete effects, and a pre-operation replay seed are required before queueing.
Everything is bounded and capture failure is invisible to the host app: a fixed-depth queue drops oldest on overflow, batches and retries are capped, uploads run on one background goroutine, and Record never blocks or panics.
Outbound-exchange capture and hermetic replay for reproit-backend-go.
Go port of the Node SDK's instrument.js + replay.js, following the Rust SDK's precedent: Go has no monkeypatching, so the boundary is explicit and OPT-IN. Route outbound HTTP through a Transport (or the client WrapClient returns) and database statements through RunDB, and every dependency exchange (request AND response) is recorded onto the ambient request trace, bounded and redacted at source.
With REPROIT_REPLAY naming a `reproit-backend-capture` payload, the SAME entry points serve the recorded exchanges instead: strict per-protocol ordinal matching, `$reproit` redaction placeholders match any value, a truncated-at-capture body fails closed, and the first unmatched call emits a structured `REPROIT:DIVERGENCE` stderr line and answers 599 (HTTP) or an error (db). No live dependency is touched in replay mode.
Capture failure is invisible to the host app: an instrumentation defect never breaks the caller's request.
net/http middleware for reproit-backend-go.
Scan-time: inert unless the request carries `x-reproit-trace`; the finished trace is returned as the `x-reproit-events` response header. Production: pass a Capture and every request is traced and handed to the sampler instead. Handlers record observed effects via FromRequest / FromContext. Every adapter path fails closed: instrumentation errors never reach the host app.
Bodies are buffered up to a fixed cap so the start/return events carry the decoded JSON payloads; larger or non-JSON bodies are traced without content. Router path parameters are not part of the canonical input here.
Insertion-ordered JSON values for hermetic replay.
The REPROIT:DIVERGENCE marker line must be BYTE-identical across SDKs, and the Node reference emits it with JSON.stringify: object keys in insertion order, compact separators, minimal escapes. Go's map[string]any loses key order and encoding/json escapes HTML, so replay decodes capture payloads into an ordered representation (omap) and re-encodes them with a writer that matches JSON.stringify byte for byte. Capture-side events keep using plain maps and CanonicalJSON (sorted keys), which is the frozen wire; the ordered layer exists only where Node's insertion order is the contract.
Hermetic replay for reproit-backend-go.
When REPROIT_REPLAY names a `reproit-backend-capture` payload, the same boundaries that record exchanges at capture time SERVE them instead, so the application re-executes against exactly what production saw with no live dependencies.
Determinism is a contract here, not a similarity score. Matching is strict per-operation ordinals: within one operation (method plus path+query for HTTP, statement text for the database) exchanges are consumed in recorded order, so pooled database clients and LLM tool-call loops that interleave operations still match exactly. Recorded `$reproit` redaction placeholders match any value at their position; nothing else is tolerated. The first unmatched call is a DIVERGENCE: reported as a structured `REPROIT:DIVERGENCE` stderr line (with a `bodyDelta` naming WHERE the bodies differ; chat-shaped bodies name the first differing message index), then answered 599 (HTTP) or an error (db), never a fuzzy match. The marker line is BYTE-identical to the Node reference's (see ordered.go).
The envelope pins the replay's determinism: TZ comes from the capture, ReplayNow returns the clock offset to the capture moment, and the seeded stream drives both math/rand's global source and ReplayRNG. Named gaps, documented rather than papered over: time.Now cannot be patched process wide (code reading it directly sees the real clock; use ReplayNow), math/rand/v2's global source cannot be reseeded, and crypto/rand is unpinnable by design. Honesty note: the seed makes REPLAY runs deterministic; it does not reproduce the randomness the app drew in production.
database/sql driver wrap for reproit-backend-go.
SQLDriver decorates any database/sql driver at the driver.Driver boundary, the Go analogue of the Node reference wrapping `pg.Client.prototype.query`: statements executed through database/sql are recorded on the ambient trace as `pg`-shaped exchanges (text, values, command, rowCount, rows), and with REPROIT_REPLAY set the SAME driver serves the recorded results without ever opening the underlying driver: Open returns a stub connection, so the app boots with the database down.
Coverage is the context-carrying surface database/sql actually uses: QueryContext / ExecContext on the connection and on prepared statements. The context is where the ambient trace lives, so the non-context forms (driver.Stmt Exec/Query, driver.Queryer) pass through unrecorded rather than half-recorded, exactly as the Node wrapper passes exotic query shapes through. Named gap: the driver API exposes no server command tag, so the recorded `command` is derived from the statement's leading verb.
Package reproitbackend is the experimental Go backend trace adapter.
Go port of sdk/reproit-backend-rs. Scan-time: services activate this adapter only when a trusted request carries `x-reproit-trace`. The resulting response header (`x-reproit-events`) contains bounded, trace-bound, structurally redacted events. Production: the optional, config-gated capture mode (capture.go) self-samples finished traces with a stable failure oracle and posts them to Cloud ingest. It is not a public compatibility surface while backend contracts remain experimental.
Wire parity with the Rust adapter: events serialize as compact JSON with recursively sorted keys (serde_json's BTreeMap order), and the header is unpadded base64url of that encoding. Zero third-party dependencies.
Index ¶
- Constants
- Variables
- func CanonicalJSON(value any) []byte
- func ContextWithTrace(ctx context.Context, trace *BackendTrace) context.Context
- func DeterminismEnvelope(observedAt any) map[string]any
- func Init()
- func MarkedOracle(events []map[string]any) string
- func Middleware(options MiddlewareOptions) func(http.Handler) http.Handler
- func RecordExchange(ctx context.Context, protocol string, request, response any)
- func ReplayNow() time.Time
- func Replaying() bool
- func WrapClient(client *http.Client) *http.Client
- type BackendTrace
- func (t *BackendTrace) Effect(kind EffectKind, opts EffectOptions) error
- func (t *BackendTrace) Events() []map[string]any
- func (t *BackendTrace) Exchange(kind EffectKind, opts ExchangeOptions) error
- func (t *BackendTrace) Finish(output any, status int, success, effectsComplete bool) error
- func (t *BackendTrace) Finished() bool
- func (t *BackendTrace) Header() (string, error)
- func (t *BackendTrace) Oracle(id string, detail any) error
- type BeginOptions
- type Capture
- type CaptureConfig
- type CaptureStats
- type DBError
- type DBOutcome
- type EffectKind
- type EffectOptions
- type ExchangeOptions
- type ExchangeStats
- type HTTPInput
- type MiddlewareOptions
- type ReplayRNG
- type SQLDriver
- type Selection
- type TraceContext
- type Transport
Constants ¶
const ( // CaptureFormat identifies the replayable capture object attached to the // finding context (`context.reproitCapture`). CaptureFormat = "reproit-backend-capture" CaptureVersion = 1 // ServerErrorOracle is the first-class registry oracle id for an // operation that returned HTTP 5xx. ServerErrorOracle = "backend-server-error" // Agent oracle vocabulary (registry ids, lowest confidence tier): // authored assertions an LLM/agent operation marks on its own trace via // `trace.Oracle(id, detail)`. A marked operation is always captured and // its failure observation carries the marked id instead of the 5xx // default. AgentResponseOracle = "agent-response-content" AgentGuardrailOracle = "agent-guardrail-violation" AgentLoopBoundOracle = "agent-loop-bound-exceeded" // OracleMarkerResource is the effect resource that carries an oracle // marker on the trace. A marker is an `emit` effect so the scan-time // wire shape stays inside the existing event vocabulary. OracleMarkerResource = "reproit-oracle" )
const ( // MaxExchangeBodyBytes is the inline body budget per exchange side. // Beyond it the body is dropped and only provable identity (byte count + // sha256) remains. MaxExchangeBodyBytes = 8 * 1024 // MaxStreamChunks caps recorded stream chunk boundaries per exchange // (SSE / chunked responses, the LLM streaming shape). Beyond it the // boundaries are marked truncated and replay fails closed rather than // serve a wrong stream shape. MaxStreamChunks = 128 )
const ( // MaxEvents bounds the events one trace may hold (start + effects + return). MaxEvents = 256 // MaxHeaderBytes bounds the encoded `x-reproit-events` response header. MaxHeaderBytes = 60000 )
const DivergenceMarker = "REPROIT:DIVERGENCE "
DivergenceMarker prefixes the structured divergence line, byte-identical to the Node reference's.
Variables ¶
var ( ErrInvalidOperation = errors.New("reproit trace rejected input: InvalidOperation") ErrAlreadyFinished = errors.New("reproit trace rejected input: AlreadyFinished") ErrTooManyEvents = errors.New("reproit trace rejected input: TooManyEvents") ErrHeaderTooLarge = errors.New("reproit trace rejected input: HeaderTooLarge") )
Trace rejection reasons, mirroring the Rust TraceError variants.
var AgentOracles = []string{ AgentResponseOracle, AgentGuardrailOracle, AgentLoopBoundOracle, }
AgentOracles is the closed set of agent oracle ids Oracle accepts.
Functions ¶
func CanonicalJSON ¶
CanonicalJSON encodes a normalized value as compact JSON with recursively sorted object keys: byte-identical to the Rust adapter's serde_json (BTreeMap) encoding of the same events.
func ContextWithTrace ¶
func ContextWithTrace(ctx context.Context, trace *BackendTrace) context.Context
ContextWithTrace makes trace the ambient trace for Transport and RunDB calls made with the returned context. The net/http middleware does this automatically; call it directly only for hand-rolled servers and fixtures.
func DeterminismEnvelope ¶
DeterminismEnvelope builds a standalone determinism envelope for callers that write capture payloads themselves (fixtures, file sinks) instead of uploading through a Capture. Pass the first event's `at` stamp when one exists, nil otherwise.
func Init ¶
func Init()
Init loads the replay session (when REPROIT_REPLAY is set) and pins the process envelope. Idempotent; the first Transport or RunDB call triggers it lazily, but calling it from main pins TZ before time-zone-sensitive code runs.
func MarkedOracle ¶
MarkedOracle returns the first agent oracle marked on a finished trace's events, or "".
func Middleware ¶
func Middleware(options MiddlewareOptions) func(http.Handler) http.Handler
Middleware wraps an http.Handler with the trace adapter.
func RecordExchange ¶
RecordExchange records one exchange on the ambient trace directly. It is the escape hatch for protocols this SDK has no boundary for; request and response are recorded as given (bounded and redacted by the trace layer).
func ReplayNow ¶
ReplayNow is the envelope-pinned clock: in replay mode it returns the current time offset to the capture moment; outside replay it is time.Now. Go cannot patch time.Now process wide (a named gap), so code that must see the recorded moment reads this instead.
Types ¶
type BackendTrace ¶
type BackendTrace struct {
// contains filtered or unexported fields
}
BackendTrace records one operation as bounded, redacted events. Safe for concurrent use by the request goroutine and the adapter.
func Begin ¶
func Begin(context *TraceContext, operation string, opts BeginOptions) (*BackendTrace, error)
Begin starts an operation trace with a redacted canonical start event.
func FromContext ¶
func FromContext(ctx context.Context) *BackendTrace
FromContext returns the request's trace recorder, or nil when the request is not being traced.
func FromRequest ¶
func FromRequest(r *http.Request) *BackendTrace
FromRequest returns the request's trace recorder, or nil when the request is not being traced.
func (*BackendTrace) Effect ¶
func (t *BackendTrace) Effect(kind EffectKind, opts EffectOptions) error
Effect records one observed effect. Fails after finish.
func (*BackendTrace) Events ¶
func (t *BackendTrace) Events() []map[string]any
Events returns a snapshot of the recorded events (event maps are shared; treat them as read-only).
func (*BackendTrace) Exchange ¶
func (t *BackendTrace) Exchange(kind EffectKind, opts ExchangeOptions) error
Exchange records one captured dependency exchange as an effect event. The exchange is redacted like every other value before it enters the trace.
func (*BackendTrace) Finish ¶
func (t *BackendTrace) Finish(output any, status int, success, effectsComplete bool) error
Finish records the single return event. A second finish fails.
func (*BackendTrace) Finished ¶
func (t *BackendTrace) Finished() bool
Finished reports whether the return event has been recorded.
func (*BackendTrace) Header ¶
func (t *BackendTrace) Header() (string, error)
Header encodes the finished trace as the `x-reproit-events` value: unpadded base64url over canonical JSON, bounded at MaxHeaderBytes.
func (*BackendTrace) Oracle ¶
func (t *BackendTrace) Oracle(id string, detail any) error
Oracle marks an agent oracle on the trace: an authored assertion that this operation violated its own contract (response content/shape, guardrail, loop bound). The marker rides as an `emit` effect so the wire vocabulary is unchanged; capture mode always uploads a marked operation and its failure observation carries the marked id. Unknown ids are rejected so a typo cannot mint an oracle category.
type BeginOptions ¶
type BeginOptions struct {
SpanID string
Tenant string
IdempotencyKey string
Input any
Selections []Selection
}
BeginOptions carries the optional Begin parameters. Empty strings mean absent; IdempotencyKey is hashed before it enters any event.
type Capture ¶
type Capture struct {
// contains filtered or unexported fields
}
Capture is the handle to the capture worker. Safe for concurrent use; all users share one queue and one upload goroutine.
func NewCapture ¶
func NewCapture(config CaptureConfig) *Capture
NewCapture starts capture mode. Returns nil (capture disabled, host unaffected) when the config is unusable: empty endpoint/key or identifiers the ingest protocol would reject.
func (*Capture) Context ¶
func (c *Capture) Context() *TraceContext
Context synthesizes a trace context for capture-mode operations, replacing the scan-time `x-reproit-trace` header requirement.
func (*Capture) Flush ¶
Flush blocks up to timeout until every queued operation has been sent (or dropped). Returns false on timeout. Intended for tests, examples, and graceful shutdown; request handling never needs it.
func (*Capture) Record ¶
func (c *Capture) Record(trace *BackendTrace)
Record hands a finished trace to the sampler. Unfinished traces are ignored. Never blocks and never fails visibly; overflow drops the oldest queued operation.
func (*Capture) Stats ¶
func (c *Capture) Stats() CaptureStats
Stats returns a snapshot of the capture counters.
type CaptureConfig ¶
type CaptureConfig struct {
// Endpoint is the full ingest URL, e.g.
// `https://cloud.example.com/v1/capture-batches`.
Endpoint string
// APIKey is the project API key, sent as `Authorization: Bearer`.
APIKey string
// AppID is the Cloud project app id the batches are posted under.
AppID string
// Build is an optional build/version identity stamped on batches.
Build string
// Commit is the code identity for the capture. When unset, REPROIT_COMMIT
// then GITHUB_SHA are consulted; never derived by shelling out to git.
Commit string
// HealthySamplePerMille remains for source compatibility. Healthy
// operations are never uploaded as capture batches.
HealthySamplePerMille int
// FlushInterval is the gather window before a pending batch is sent.
FlushInterval time.Duration
// RequestTimeout is the per-request upload timeout.
RequestTimeout time.Duration
// RetryLimit is the upload retries per batch after the first attempt
// (5xx/network only). Capped at 5.
RetryLimit int
}
CaptureConfig configures capture mode. Build with NewCaptureConfig so the defaults match the other backend SDKs.
func NewCaptureConfig ¶
func NewCaptureConfig(endpoint, apiKey, appID string) CaptureConfig
NewCaptureConfig returns a config with a 3 s flush interval, a 5 s request timeout, and 2 retries.
type CaptureStats ¶
type CaptureStats struct {
CapturedOperations uint64
DroppedOperations uint64
SentBatches uint64
FailedBatches uint64
}
CaptureStats is a point-in-time snapshot of the capture counters.
type DBError ¶
DBError is a recorded statement failure. It implements error so RunDB can return recorded failures unchanged at replay.
type DBOutcome ¶
DBOutcome is the recorded result of one statement.
func RunDB ¶
func RunDB( ctx context.Context, text string, values []any, live func() (DBOutcome, error), ) (DBOutcome, error)
RunDB routes one database statement through the exchange boundary.
Capture mode runs live and records the statement with its outcome; replay mode serves the recorded outcome and never calls live, so no database is touched. Go has no driver to monkeypatch, so anything not routed through RunDB is invisible to capture and unavailable at replay.
type EffectKind ¶
type EffectKind string
EffectKind is the closed set of typed effects a handler may record.
const ( EffectRead EffectKind = "read" EffectWrite EffectKind = "write" EffectDelete EffectKind = "delete" EffectEmit EffectKind = "emit" EffectCall EffectKind = "call" )
type EffectOptions ¶
EffectOptions carries the optional Effect parameters. Detail must be an object; only its (redacted) before/after/payload fields are kept.
type ExchangeOptions ¶
ExchangeOptions carries the captured dependency exchange: the request the app sent and the response the dependency returned. This is what hermetic local replay serves; evaluation oracles ignore it.
type ExchangeStats ¶
ExchangeStats is a point-in-time snapshot of the instrument counters.
func InstrumentStats ¶
func InstrumentStats() ExchangeStats
InstrumentStats returns a snapshot of the outbound-exchange counters.
type HTTPInput ¶
HTTPInput is the canonical decoded OpenAPI input. Framework adapters must provide decoded values (including slices for repeated query/header parameters), never raw query strings whose serialization is ambiguous.
type MiddlewareOptions ¶
type MiddlewareOptions struct {
// Capture enables production capture mode; nil keeps scan-time only.
Capture *Capture
// Operation names the traced operation; default `METHOD /path`.
Operation func(*http.Request) string
// Tenant extracts a non-secret tenant identifier; default none.
Tenant func(*http.Request) string
// EffectsComplete asserts the adapter observed every persistent effect.
EffectsComplete bool
}
MiddlewareOptions configures the net/http middleware. The zero value is a scan-time-only adapter with `METHOD /path` operation names.
type ReplayRNG ¶
type ReplayRNG struct {
// contains filtered or unexported fields
}
ReplayRNG is the deterministic xorshift64* stream seeded from the capture's replaySeed. It pins REPLAY determinism only; it does not reproduce the randomness the app drew in production.
func NewReplayRNG ¶
func NewReplayRNG() *ReplayRNG
NewReplayRNG returns the seeded stream, or nil outside replay mode or when the capture carries no seed.
type SQLDriver ¶
SQLDriver wraps Base so database/sql traffic crosses the exchange boundary. Register it under its own name:
sql.Register("reproit-pg", &reproitbackend.SQLDriver{Base: pqDriver})
db, err := sql.Open("reproit-pg", dsn)
type Selection ¶
Selection is a GraphQL selection mapping (parser-produced only).
func NewSelection ¶
NewSelection returns nil on an invalid path, matching the Rust constructor.
func (*Selection) WithTypeCondition ¶
WithTypeCondition returns nil when the condition is not a bare valid name.
type TraceContext ¶
type TraceContext struct {
TraceID string
Actor string
ActionIndex uint32
Build string
ConfigContract string
// CaptureEnvelope stamps capture-mode-only determinism fields on every
// event: wall-clock `at` and monotonic `monoNs`. Scan-time traces never
// carry them, so the `x-reproit-events` wire stays byte-stable.
CaptureEnvelope bool
ReplaySeed string
}
TraceContext identifies the trusted scan-time trace (or a synthesized capture-mode context). Empty strings mean absent.
func TraceContextFromHeaders ¶
func TraceContextFromHeaders(get func(name string) string) *TraceContext
TraceContextFromHeaders builds a context from a request header lookup (empty string means the header is missing). Returns nil when no valid `x-reproit-trace` is present: the adapter stays inert.
type Transport ¶
type Transport struct {
Base http.RoundTripper
}
Transport records every round trip as an exchange on the ambient trace, or serves the recorded exchange when REPROIT_REPLAY is set. Base defaults to http.DefaultTransport.
Nothing is automatic: a client that does not use this Transport is invisible to capture and unavailable at replay.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Command contractsample emits the shared backend-SDK contract sample for sdk/test/backend_batch_test.js: one scan-time trace (for the header) and the 5xx capture batch a real Capture posts for the same failed operation, received through a local stub ingest server.
|
Command contractsample emits the shared backend-SDK contract sample for sdk/test/backend_batch_test.js: one scan-time trace (for the header) and the 5xx capture batch a real Capture posts for the same failed operation, received through a local stub ingest server. |
|
Command hermeticfixture is the money-test fixture, Go flavor: a net/http app whose GET /quote operation 500s because an upstream pricing service returns {"prices": null} and the handler indexes into it.
|
Command hermeticfixture is the money-test fixture, Go flavor: a net/http app whose GET /quote operation 500s because an upstream pricing service returns {"prices": null} and the handler indexes into it. |
|
Command parityprobe is the Go side of sdk/test/backend_replay_parity_test.js: it loads the capsule from stdin as a REPROIT_REPLAY session and replays the harness's two probes through the real Transport boundary, printing the served SSE exchange (status, body, observed chunk split), the 599 divergence body, and the captured REPROIT:DIVERGENCE marker line as one JSON object on stdout.
|
Command parityprobe is the Go side of sdk/test/backend_replay_parity_test.js: it loads the capsule from stdin as a REPROIT_REPLAY session and replays the harness's two probes through the real Transport boundary, printing the served SSE exchange (status, body, observed chunk split), the 599 divergence body, and the captured REPROIT:DIVERGENCE marker line as one JSON object on stdout. |
|
Package reproitci is CI capture mode for reproit-backend-go: the flaky-CI wedge.
|
Package reproitci is CI capture mode for reproit-backend-go: the flaky-CI wedge. |