Pick the smallest scope that proves the thing
Not every Varianz test needs a coordinator. Match the scope to what you’re actually testing:
Cover a branch locally first, then keep a smaller number of coordinator-backed and end-to-end tests for the delivery path a local test deliberately doesn’t exercise. Making an E2E test the first — or only — test of a VPoint’s behavior is the common mistake.
The rest of this page is the coordinator-backed lifecycle. For local stages, everything below (sessions, sync, the session header) simply doesn’t apply.
The coordinator-backed lifecycle
Every coordinator-backed Varianz test, in every framework, follows the same six steps:- A test session is created (automatically, by the fixture or extension).
- Stages are inserted — CEL expressions bound to specific VPoints.
- Sync — the test waits until connected services confirm they received the stages.
- Trigger — the test calls the service, passing the session ID.
- Assert — on response values and captured samples.
- Cleanup — stages removed, session destroyed (automatic).
x-varianz-id see them. Production traffic flows through unmodified.
What must be running
Coordinator-backed tests are true integration tests. Three things must be up and connected to each other:- A coordinator — see Running the coordinator locally.
- Your instrumented service(s) — connected to that coordinator, with
VARIANZ_ENABLED=true(plus, for Java/Kotlin, the agent attached), andVARIANZ_INSECURE_ALLOW_PLAINTEXT=trueif the coordinator is plaintext. - The test process — connected to the same coordinator via the framework fixture, with the same two environment variables. Test fixtures fail fast with a clear error if the SDK is disabled.
The sync barrier
await_sync_or_fail(timeout) (awaitSyncOrFail/AwaitSyncOrFail) blocks until every currently connected subscriber whose VPoints match your stages confirms receipt, and fails the test on timeout. Always call it between inserting stages and triggering the service — insertion is asynchronous, and without the barrier your trigger can race ahead of stage delivery.
Zero subscribers
A result of “0 of 0 subscribers confirmed” means no connected service matched your stages — usually the service under test isn’t connected to the coordinator yet. The barrier treats this as satisfied rather than blocking, and the session re-checks at teardown, printing a
[varianz] WARNING if nothing ever received your stages. When a stage doesn’t apply, look for that warning first — it points straight at the disconnected service.VARIANZ_ENABLED=true. Because the SDK is disabled by default, such a service starts and serves traffic normally but registers no VPoints — so it is never a subscriber, and every stage you insert reaches nobody. Check its startup log: one line, [varianz] DISABLED (source: …) instead of [varianz] ENABLED (…).
If sync reports 0 of N or times out with some subscribers missing, one of your services is connected but didn’t register the targeted VPoint — check the VPoint name (case-sensitive, including namespace prefixes) and that the service actually reached its registration code.
Lazy VPoints
With typed schemas — annotations, the scanner, or codegen, which is the recommended setup — VPoints register at startup and stages work from the very first request; nothing in this section applies. The rule: a VPoint defers its schema when any parameter it has to expose lacks a usable type annotation. A missing return annotation is not a trigger — that VPoint still registers at startup, with anunknown return kind. A parameter named in opaque= is exempt, which is why opaque=["context"] on a gRPC servicer method (self, request, context) keeps the signature eager rather than making it lazy.
You do not need a warm-up call. Attaching a stage performs the deferred resolution itself, so a stage inserted before the VPoint has ever run still applies on the very first call — including expressions that need the schema, like args.x or invoke():
Insertion vs. delivery
Inserting a stage resolves its routing target against the coordinator’s catalog. By default insertion is lazy: a target that matches nothing yet is stored unresolved and attaches when a matching VPoint appears — convenient for lazily-registered VPoints, but it means insert success alone doesn’t prove routing. Resolution is sticky: once inserted, a stage never migrates to a different VPoint. The sync barrier is the delivery check — it confirms subscribers connected at that moment, never future ones. The practical recipe: insert →await_sync_or_fail → trigger.
Triggering with the session ID
- HTTP: set the
x-varianz-idheader. From Python, use arequests.Session()with the header on the session object — barerequests.post(headers=...)loses headers on redirects. - gRPC: set
x-varianz-idmetadata on the call. - Browser (Playwright): the fixture sets the header on the
pagefor you.
Choose your framework
The test language is independent of the service language — see Test across services.
