reproitbackend

package module
v0.0.0-...-1b62d17 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

README

ReproIt backend adapter for Go

This module is an internal validation surface, not a published compatibility API. It is inactive unless a trusted request contains x-reproit-trace. It is a port of the Rust reference adapter (sdk/reproit-backend-rs) with the same bounds, redaction, and wire format, in plain Go with zero third-party dependencies in the core (net/http, encoding/json, sync).

Framework integrations pass their header lookup into TraceContextFromHeaders, start an operation with Begin, record only effects actually observed by the adapter, then call Finish and return Header() as x-reproit-events. Set EffectsComplete only when the adapter observed every persistent effect in the operation. Tenant and resource identifiers must be non-secret structural identifiers.

The adapter enforces bounded identifiers, 256 events, a 60 KB encoded header, typed effects, one return, no effects after return, hashed idempotency identity, and recursive structural redaction. GraphQL callers may attach parser-produced Selection mappings; never infer selections from response content.

Installing

The module path github.com/ReproIt/reproit/sdk/reproit-backend-go is not a published repository: the source lives in the main repository under sdk/reproit-backend-go. Vendor it or point at a checkout with a replace directive until it is published:

require github.com/ReproIt/reproit/sdk/reproit-backend-go v0.0.0
replace github.com/ReproIt/reproit/sdk/reproit-backend-go => /path/to/reproit/sdk/reproit-backend-go

net/http middleware and Fiber v2 adapter

The net/http middleware begins the trace from the decoded request (JSON body, decoded query values, lowercased headers), finishes it when the response is complete, and attaches x-reproit-events on scan-time requests. Handlers record observed effects through the recorder carried on the request context:

import reproit "github.com/ReproIt/reproit/sdk/reproit-backend-go"

config := reproit.NewCaptureConfig(
    "https://cloud.example.com/v1/capture-batches", // ingest endpoint
    "sk_live_...",                         // project API key (Authorization: Bearer)
    "app-id",                              // Cloud project app id
)
config.Build = "1.4.2"                     // optional deployment identity
capture := reproit.NewCapture(config)      // nil = disabled, host unaffected

mux := http.NewServeMux()
mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {
    if trace := reproit.FromRequest(r); trace != nil {
        _ = trace.Effect(reproit.EffectWrite, reproit.EffectOptions{
            Resource: "orders", Key: "1",
        })
    }
    // ...
})
handler := reproit.Middleware(reproit.MiddlewareOptions{Capture: capture})(mux)

Fiber v2 is a separate Go module (github.com/ReproIt/reproit/sdk/reproit-backend-go/fiber) so the core stays dependency-free. It is the same adapter behind Fiber's buffered request/response model; handlers fetch the recorder with reproitfiber.From(c):

import reproitfiber "github.com/ReproIt/reproit/sdk/reproit-backend-go/fiber"

app := fiber.New()
app.Use(reproitfiber.New(reproitfiber.Options{Capture: capture}))

Every adapter path fails closed: an instrumentation defect never breaks the request.

Production capture mode (off by default)

Capture mode uploads finished traces to Cloud ingest without requiring x-reproit-trace. It is config-gated: nothing leaves the process unless the host constructs a Capture. NewCapture(config) returns nil (capture disabled, host unaffected) when the config is unusable. capture.Record(trace) never blocks, never panics, and never surfaces errors.

Eligibility: an operation needs a stable HTTP 5xx or marked agent oracle, complete effects, and a pre-operation replay seed. success == false alone is not an oracle. Healthy operations are not uploaded. HealthySamplePerMille remains available for source compatibility but has no effect. An eligible capture is posted as one universal capture-batch-v1 containing exactly that operation. It carries the full redacted start, effects, and return sequence for deterministic local replay:

# pull the occurrence the capture became, and re-execute it locally:
reproit occ_<id>

Bounds, all fixed: queue depth 64 operations (drop-oldest on overflow), one operation per batch, 48 KB capture payload (trailing effect events dropped first, captureDroppedEffects counts them), bounded flush interval (floor 100 ms), per-request timeout, and at most RetryLimit (cap 5) retries; 4xx responses are never retried. Redaction runs in Begin/Effect/Finish, before anything is queued. Uploads run on one background goroutine, off the request path. Canonical wire bytes (compact, recursively sorted keys) are pinned against the Node adapter by a golden test. sdk/test/oracle_contract_test.js pins the backend-server-error tagging contract.

Capsule parity: exchange capture and hermetic replay

The boundary is explicit and opt-in (Go has no monkeypatching): route outbound HTTP through Transport (or the client WrapClient returns) and database traffic through the SQLDriver wrap of any database/sql driver, or the explicit RunDB closure. Every dependency exchange (request AND response) is recorded on the ambient trace, bounded (8 KiB body budget with full-byte sha256 identity beyond it, 32 name-sorted headers, 64 rows, 128 stream chunk boundaries) and redacted at source. Streaming responses (SSE / chunked) record their observed chunk boundaries as the app consumes the body; an abandoned body records nothing.

sql.Register("reproit-pg", &reproit.SQLDriver{Base: pqDriver})
db, _ := sql.Open("reproit-pg", dsn)
client := reproit.WrapClient(nil)

With REPROIT_REPLAY naming a capture payload, the SAME boundaries serve the recorded exchanges: no socket is opened and SQLDriver.Open returns a connect stub, so the app boots with every dependency down. Matching is strict per-operation ordinals; recorded $reproit placeholders wildcard; the first unmatched call emits a REPROIT:DIVERGENCE stderr line (byte-identical to the Node reference, bodyDelta naming the first differing chat message or byte offset) and answers 599 (HTTP) or an error (db). The envelope pins TZ, the ReplayNow clock offset, and the seeded RNG (math/rand's global source plus NewReplayRNG).

Named capability gaps, recorded rather than papered over:

  • time.Now cannot be patched process wide; code must read reproit.ReplayNow() to see the capture moment.
  • math/rand/v2's global source cannot be reseeded; only the v1 global source and NewReplayRNG pin. crypto/rand is unpinnable by design.
  • The database/sql driver API exposes no server command tag, so the recorded command is derived from the statement's leading verb.
  • Context-less driver calls (driver.Stmt.Exec/Query without context) carry no ambient trace and pass through unrecorded.

Agent oracle API

LLM/agent operations can mark authored failure assertions on their own trace with trace.Oracle(id, detail). The id must be one of the registry agent oracle ids (agent-response-content, agent-guardrail-violation, agent-loop-bound-exceeded, exported as AgentResponseOracle / AgentGuardrailOracle / AgentLoopBoundOracle); unknown ids return ErrInvalidOperation so a typo cannot mint an oracle category. The marker rides as an emit effect on the resource reproit-oracle, so the scan-time wire vocabulary is unchanged. Semantics match the Node reference exactly: a marked operation is ALWAYS captured by capture mode (like a 5xx, even when the return reports success), and the capture batch's failure observation carries the marked id as a contract-violation (an authored assertion), where a bare 5xx stays the exception it always was. MarkedOracle(events) returns the first marked id on a finished trace's events.

if err := trace.Oracle(reproit.AgentGuardrailOracle, map[string]any{
    "tool": "delete_order",
}); err != nil {
    // unknown oracle id: fix the constant, do not invent one
}

CI capture mode (the flaky-CI wedge)

reproitci (same module, github.com/ReproIt/reproit/sdk/reproit-backend-go/reproitci) binds a testing.T test to a trigger identity that is the TEST, not an inbound HTTP request. The wire is the existing capture payload: the identity rides in the existing operation field as test:<suite>#<test>, the oracle is the existing backend-authored-invariant registry id (a test IS an authored invariant), and the markers are the existing structured stderr lines (REPROIT:CI-TEST, REPROIT:CI-CAPSULE, REPROIT:DIVERGENCE). No new protocol fields, no new oracle ids.

func TestOrderTotal(t *testing.T) {
    ct := reproitci.Wrap(t, "checkout")
    total, err := OrderTotal(ct.Context(), reproit.WrapClient(nil), configURL, 100)
    if err != nil {
        ct.Fatalf("order total: %v", err)
    }
    if total != 125 {
        ct.Fatalf("order total = %v, want 125", total)
    }
}
  • REPROIT_CI_CAPTURE=1: each wrapped test runs under its own capture-envelope trace; outbound calls carry ct.Context() so the SDK boundaries record every dependency exchange. A FAILING test spools a version-2 capsule to a bounded on-disk spool (REPROIT_CI_SPOOL, default .reproit/ci-spool; total-bytes cap REPROIT_CI_SPOOL_MAX, default 16 MiB, clamped to [4 KiB, 64 MiB]; over-cap capsules are dropped and counted in dropped.count, never silently) and announces it with a REPROIT:CI-CAPSULE stderr line.
  • REPROIT_REPLAY=<capsule>: the SAME wrapper skips every test but the capsule's named one, the SDK serves the recorded exchanges in process (no upstream, no database), and the observed result is reported as a REPROIT:CI-TEST marker for reproit check.
  • Neither env set: Wrap is inert.

reproit check <capsule> --exec "<test command>" re-runs the single named test directly:

reproit check capsule.json --exec \
  "go -C <dir> test -count=1 -run '^TestOrderTotal\$' 1>&2"

Failure identity: assertions made through the wrapper (ct.Errorf / ct.Fatalf / ...) record the bounded message that reproit check compares between the recorded run and a replay; a failure raised on the bare *testing.T still fails and spools, but with an empty identity (check then treats any replayed failure as the recorded one). Two Go mechanics are explicit where Node hides them: outbound calls must carry Context() (no ambient async storage), and the replay command must redirect 1>&2 in local directory mode, because go test merges the test binary's stderr into stdout and package-list mode buffers a passing binary's output away. Honest limit, same as every SDK: replay pins the envelope and the recorded exchanges; a race the boundary cannot see is reported Inconclusive, never a fake reproduction.

Level matrix against the Node reference

Same level in ALL surfaces the Node SDK has; genuinely impossible surfaces are named rows, never silent downgrades.

Node surface Go Level
Scan-time trace (BackendTrace, bounds, redaction, canonical wire) Begin/Effect/Finish/Header Level (byte-parity golden tests)
Framework adapters (Express, Fastify) net/http middleware, Fiber v2 module Level (per-ecosystem frameworks)
Production capture mode (Capture) NewCapture/Record/Flush Level
Agent oracle API (trace.oracle, marked capture, contract-violation) trace.Oracle, MarkedOracle Level
Exchange capture (http/db, stream chunk boundaries) Transport/WrapClient, RunDB, SQLDriver Level, but opt-in (below)
Hermetic replay (ordinal match, $reproit wildcards, REPROIT:DIVERGENCE) same, byte-identical marker Level
Envelope pinning (TZ, clock, seeded RNG) TZ + ReplayNow + math/rand v1 + NewReplayRNG Level, named gaps (below)
CI capture mode (ci.suite, spool caps, REPROIT:CI-TEST) reproitci.Wrap, same caps and markers Level

Named impossible surfaces (Go the language, not this port):

  • Automatic instrumentation: Node monkeypatches http/fetch/pg process wide; Go cannot. The boundary is explicit and opt-in; a client not routed through it is invisible to capture and unavailable at replay.
  • Ambient trace propagation: Node's AsyncLocalStorage finds the trace implicitly; Go threads context.Context (middleware does it for handlers, reproitci hands it to tests).
  • time.Now cannot be patched process wide; code must read reproit.ReplayNow().
  • math/rand/v2's global source cannot be reseeded; only the v1 global source and NewReplayRNG pin. crypto/rand is unpinnable by design.
  • The database/sql driver API exposes no server command tag; the recorded command is derived from the statement's leading verb. Context-less driver calls carry no ambient trace and pass through unrecorded.

Tests

cd sdk/reproit-backend-go
go test ./...        # unit + net/http e2e + reproitci (child-process suite), zero dependencies
cd fiber && go test ./...  # Fiber v2 adapter (separate module)
node ../test/backend_replay_parity_test.js  # byte parity against the Node reference
../../validation/backend/go-hermetic-e2e/run.sh  # money test under PORTABILITY
../../validation/backend/go-flaky-ci-e2e/run.sh  # flaky-CI wedge, six legs

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

View Source
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"
)
View Source
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
)
View Source
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
)
View Source
const DivergenceMarker = "REPROIT:DIVERGENCE "

DivergenceMarker prefixes the structured divergence line, byte-identical to the Node reference's.

Variables

View Source
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.

AgentOracles is the closed set of agent oracle ids Oracle accepts.

Functions

func CanonicalJSON

func CanonicalJSON(value any) []byte

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

func DeterminismEnvelope(observedAt any) map[string]any

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

func MarkedOracle(events []map[string]any) string

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

func RecordExchange(ctx context.Context, protocol string, request, response any)

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

func ReplayNow() time.Time

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.

func Replaying

func Replaying() bool

Replaying reports whether this process is serving a recorded capture instead of touching live dependencies.

func WrapClient

func WrapClient(client *http.Client) *http.Client

WrapClient returns a copy of client whose Transport records exchanges. A nil client wraps http.DefaultClient.

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

func (c *Capture) Flush(timeout time.Duration) bool

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

type DBError struct {
	Message string
	Code    string
}

DBError is a recorded statement failure. It implements error so RunDB can return recorded failures unchanged at replay.

func (*DBError) Error

func (e *DBError) Error() string

type DBOutcome

type DBOutcome struct {
	Command  string
	RowCount uint64
	Rows     []any
}

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

type EffectOptions struct {
	Resource string
	Key      string
	Tenant   string
	Event    string
	Detail   any
}

EffectOptions carries the optional Effect parameters. Detail must be an object; only its (redacted) before/after/payload fields are kept.

type ExchangeOptions

type ExchangeOptions struct {
	Resource string
	Key      string
	Exchange any
}

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

type ExchangeStats struct {
	CapturedExchanges uint64
	TruncatedBodies   uint64
	FailedCaptures    uint64
}

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

type HTTPInput struct {
	Body    any
	Path    map[string]any
	Query   map[string]any
	Headers map[string]any
}

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.

func (HTTPInput) Value

func (in HTTPInput) Value() map[string]any

Value produces the canonical start-event input object.

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.

func (*ReplayRNG) Float64

func (r *ReplayRNG) Float64() float64

Float64 returns the next draw in [0, 1), matching the Node and Rust SDKs' stream shape.

type SQLDriver

type SQLDriver struct {
	Base driver.Driver
}

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)

func (*SQLDriver) Open

func (d *SQLDriver) Open(dsn string) (driver.Conn, error)

Open implements driver.Driver. In replay mode the base driver is never touched: the returned stub serves recorded exchanges, so the app boots with the database stopped.

type Selection

type Selection struct {
	SchemaPath    string
	ResponsePath  string
	TypeCondition string
}

Selection is a GraphQL selection mapping (parser-produced only).

func NewSelection

func NewSelection(schemaPath, responsePath string) *Selection

NewSelection returns nil on an invalid path, matching the Rust constructor.

func (*Selection) WithTypeCondition

func (s *Selection) WithTypeCondition(condition string) *Selection

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.

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(request *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL