Skip to main content
This guide covers instrumenting real services: classes with many methods, gRPC handlers, and existing code you don’t want to restructure. For a single free function, the Quickstart shows the minimal form. What to instrument: you don’t need VPoints everywhere. Pick the functions tests need to control or observe — payment charges, calls to external providers, decision points (fraud checks, feature gates), and notification sends are the usual candidates.

Register a service class

When a service has several related operations, register the whole class or instance — each instrumented method becomes a VPoint.
Language-specific notes:
  • Pythonvz.register() takes one function, class, or instance per call; @vz.component registers a class at definition time; vz.discover() scans the calling module and registers everything. Registration blocks until the registry confirms the schema (up to 5s, fail-open).
  • Go — method names convert PascalCase → kebab-case, prefixed by the varianz:"..." struct tag (CalculatePricepricing/calculate-price). Call through the returned proxy (proxy.Call(ctx, "CalculatePrice", req)) or generate typed proxies with varianz-gen.
  • Java/Kotlin — the annotation processor generates a <ClassName>VPoints aggregate at compile time; the varianz-agent intercepts calls to the annotated methods at class-load time, so ordinary calls (service.calculate(...)) route through the pipeline with no code changes. RegistryBinding.init() is idempotent and must run before your server starts serving.
  • TypeScript — for plain objects instead of classes, use createVPoints(namespace, api); call through the returned instrumented proxy (or pass { patch: 'mutate' } to instrument the object in place). Calls made on the original object with the default wrap mode are not intercepted.

Migrate an existing function

Rename the original, wrap it, keep every caller working:
The same shape works in every language — in Python and TypeScript the decorator does the wrapping, and in Java/Kotlin the agent rewrites the method in place, so no rename is needed at all.

gRPC handlers

Don’t instrument gRPC handler methods that take StreamObserver or servicer plumbing directly — the schema would be polluted with transport types. Extract the business logic into a plain-typed method and instrument that:
In CEL expressions, use the proto message’s simple name (ChargeResponse, not hipstershop.ChargeResponse). In Go, CEL struct literals handle nested proto messages directly — Money { currency_code: "USD", units: 42 } inside a parent literal works without flattening. Remember to add the session interceptor to your server — without it, test sessions never reach these VPoints.

Field names in schemas

Schemas use each language’s natural field naming, so CEL expressions match what your API already looks like:
  • Go — precedence: varianz:"name" tag → protobuf tag → json tag → snake_case of the Go name. Codegen types (oapi-codegen, sqlc, protoc) work automatically via their json tags. varianz:"-" excludes a field; varianz:"note,opaque" passes it through without exposing it to CEL.
  • Python — annotation-derived; dataclasses, Pydantic models, attrs classes, and proto messages are introspected. opaque=[...] marks parameters that pass through uninspected.
  • Java/Kotlin — fields discovered from getters or public fields, converted camelCase → snake_case for CEL (basePricebase_price). Kotlin val maps to read-only, var to read-write.
  • TypeScript — the scanner derives fields from your TS types; @field({ kind: ... }) overrides individual kinds.

Context functions

Register helper functions your CEL expressions can call as ctx.<name>(...) — useful for domain logic tests shouldn’t have to re-implement:
Then in a stage: ctx.applyTax(invoke(), 0.1). Functions can be global (available to every VPoint) or scoped to a profile.

Verify it worked

Start the service with VARIANZ_ENABLED=true and a registry endpoint, and watch the startup log for the single [varianz] ENABLED (...) line. Then confirm registration from the test side: insert a stage with await_sync_or_fail and check it reports one confirmed subscriber, or query the catalog with the list_vpoints MCP tool. A sync result of “0 of 0 subscribers” means the service isn’t connected — see How tests work.