New: Debug encrypted microservice traffic with Speedscale's eBPF collector Read the announcement

OpenTelemetry Trace Testing for CI Release Gates


OpenTelemetry is great at answering one question: “what just broke?” The problem is that most teams need a different answer first: “what is about to break in this release?” That is where trace-based testing comes in, especially for teams running a vendor-neutral OTel stack (Collector + Tempo/Jaeger + Prometheus) and needing deterministic release gates.

Instead of treating traces as dashboard artifacts, treat them as test input. Capture real traffic behavior from production, replay it in CI against your candidate build, and fail the pipeline when behavior changes in ways users would notice. If you run AI-generated PRs, this is even more important, because agents can produce code quickly but still miss production edge cases unless those edge cases are executable during validation.

How this is different from dashboard-driven workflows

The key difference is where the release gate gets its truth source.

WorkflowPrimary inputTypical outputLimitation before deploy
Dashboard-firstcharts and alertshuman triageproves issues after impact
OTel trace testingspan-level runtime behaviordeterministic CI gaterequires replay profile hygiene

In other words: dashboards are for diagnosis, trace-based testing is for prevention.

Why traditional pre-release testing misses regressions

Most teams combine three things before deploy:

  1. Unit tests
  2. Integration tests with synthetic fixtures
  3. Staging smoke tests

That stack catches obvious failures, but still misses high-cost regressions:

  • subtle response-shape drift
  • dependency timeout behavior under realistic concurrency
  • edge-case payload combinations no one modeled in test fixtures
  • retry/idempotency bugs that only appear with production traffic patterns

Observability tools usually detect these after release, which means your feedback loop starts after user impact. Trace-based testing moves that signal left.

What trace-based testing actually means

Trace-based testing is not just “asserting on spans.” It is a closed validation loop:

  1. Capture real request and dependency behavior from production.
  2. Transform sensitive or environment-specific fields.
  3. Replay traffic against a pre-release build in isolated CI.
  4. Compare behavior against baseline expectations.
  5. Gate merges/deploys when diffs exceed thresholds.

The key idea is simple: your release should prove compatibility with reality, not just with handcrafted tests.

flowchart LR
    A[Capture Traffic] --> B[Sanitize Data]
    B --> C[Replay in CI]
    C --> D[Compare Baseline]
    D --> E{Thresholds Passed?}
    E -- Yes --> F[Merge]
    E -- No --> G[Fail Gate]

Where OpenTelemetry fits (and where it doesn’t)

OpenTelemetry gives you distributed context, span attributes, and latency/error telemetry. That is valuable for selecting and scoping what to validate.

But OTel alone is not a replay system. You still need:

  • reproducible request/response payloads
  • dependency mocks or traffic-backed simulation
  • deterministic pass/fail policy in CI

Use OTel as the discovery and prioritization layer — it tells you which flows are highest risk and worth validating; use traffic replay as the verification layer that proves whether the candidate build actually handles them.

Map OTel data to replay assets

The most useful way to make this OTel-native is to map telemetry primitives to concrete test assets.

OTel signalWhat it revealsReplay assetCI assertion
span.name + route attrsendpoint behaviorendpoint-scoped replay profilestatus + contract stability
http.status_code trendsfailure patternsnegative-case traffic sliceerror-rate threshold
duration histogramslatency driftbaseline latency profilep95/p99 regression threshold
dependency spans (db, rpc)upstream couplingdependency mock/replay bundletimeout + retry correctness

This is the part many teams miss. OTel is already giving you prioritization data, but you still need replay assets that turn that data into pass/fail behavior before merge.

OTel-native extraction pattern

Before the CI gate, define how traces are selected and exported from your OTel workflow. A minimal pattern:

  1. Select one service and one high-risk route from OTel traces.
  2. Export representative request windows for that route.
  3. Convert selected trace windows into replay profiles.
  4. Sanitize and version those profiles.

The collector snippet below is a real config pattern, but it is intentionally partial (processors only) so you can drop it into your existing collector setup rather than replace your full config.

Example OTel Collector processor strategy:

processors:
  filter/replay_candidates:
    traces:
      span:
        - 'attributes["http.route"] == "/api/v2/orders"'
        - 'attributes["service.name"] == "checkout-service"'
  tail_sampling/replay_priority:
    decision_wait: 10s
    policies:
      - name: errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: slow_requests
        type: latency
        latency:
          threshold_ms: 300

Two practical tips make this more robust in real OTel deployments:

  1. Use semantic conventions consistently (service.name, http.route, http.method, http.status_code) so replay profile selection does not drift across teams.
  2. Keep a small “critical routes” allowlist in version control so new endpoints do not silently bypass replay gates.

Then run a lightweight pipeline pattern in CI:

# .github/workflows/trace-gate.yml
name: Trace Validation Gate

on:
  pull_request:
    branches: [main]

jobs:
  replay-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build candidate
        run: ./scripts/build.sh
      - name: Start app under test
        run: ./scripts/start-test-env.sh
      - name: Replay production traffic profile
        run: ./scripts/replay-profile.sh critical-checkout-flow
      - name: Evaluate regression thresholds
        run: ./scripts/assert-thresholds.sh

Example threshold policy:

  • fail if new 5xx rate increases by more than 0.5%
  • fail if p95 latency regresses by more than 20%
  • fail if response contract diffs appear on protected endpoints

These thresholds should be strict for money paths (checkout, auth, billing) and looser for non-critical workflows. The goal is not zero regressions; it is catching the regressions that matter before they reach users.

A minimal implementation walkthrough (OTel first)

The fastest way to adopt this is a one-endpoint pilot, not a platform rewrite. Start with a single endpoint that has both high user impact and recent incident history. Use OTel trace data to pick that endpoint (error-heavy or latency-volatile), capture a representative traffic window, sanitize secrets and tenant identifiers, and store it as a replay profile owned by the service team. Then run that profile on every pull request in a disposable environment.

In practice, teams usually add four artifacts to the repo:

  1. A replay profile definition (what traffic is included and excluded)
  2. A transform policy (what fields are masked or rewritten)
  3. A threshold file (error and latency guardrails)
  4. A CI job that runs replay and publishes a diff report

Here is what a minimal replay profile definition looks like in practice:

# replay-profiles/checkout-critical.yaml
profile: checkout-critical
source: production
window: 24h
filter:
  endpoints:
    - POST /api/v2/orders
    - GET /api/v2/cart/:id
    - POST /api/v2/payments/authorize
  min_requests: 50
  exclude_status: [401, 429]
target:
  host: http://localhost:8080
  timeout: 5s

And a corresponding transform policy that handles secrets and tenant identifiers:

# transforms/sanitize.yaml
rules:
  - match: header.Authorization
    action: replace
    value: "Bearer test-token-replay"
  - match: body.$.customer_id
    action: hash
  - match: body.$.payment.card_number
    action: mask
    pattern: "****-****-****-{last4}"
  - match: header.X-Tenant-Id
    action: replace
    value: "replay-tenant-001"

Once this is in place, each failed gate becomes highly actionable. Engineers see exactly which endpoint changed behavior, what payload shape drifted, and whether the regression came from app logic or a dependency integration path. In practice, the CI job fails with a structured diff report rather than a generic error. A typical output looks like this:

GATE FAILED: checkout-critical (3 regressions)

POST /api/v2/orders
  status_code: 200 → 422 on 4.2% of requests
  response.error_code: null → "INVENTORY_UNAVAILABLE" (new field)

POST /api/v2/payments/authorize
  p95_latency_ms: 312 → 589 (88% increase, threshold: 20%)

GET /api/v2/cart/:id
  response.items[*].price_cents: PASS (no drift)

That output maps directly to a line of code or a dependency configuration change. The engineer doesn’t have to reproduce the failure in staging — the replay engine already exercised the production traffic pattern and surfaced exactly where behavior diverged.

One nuance worth getting right early is threshold calibration. Start permissive (50% latency regression, 2% error rate) and tighten as your traffic profile matures. A gate that fires on legitimate performance improvements trains the team to dismiss failures, while a gate that only fires on real regressions builds trust quickly and gets adopted by other service teams.

Another consideration is replay profile versioning. The production traffic slice that covers your current behavior is only valid for a window of time, and a checkout flow that works against last month’s traffic may look different after a pricing-model change. Version your replay profiles alongside your service and re-capture a fresh traffic window when the service contract changes intentionally.

This same loop also gives AI agents better feedback. Instead of generic “tests failed” output, they get concrete runtime diffs they can patch against.

sequenceDiagram
    participant PR as Pull Request
    participant CI as CI Pipeline
    participant RP as Replay Engine
    participant APP as Candidate Build
    participant REP as Diff Report

    PR->>CI: Trigger validation job
    CI->>RP: Start replay profile
    RP->>APP: Send production traffic slice
    APP-->>RP: Return responses and metrics
    RP->>REP: Compute behavior diffs
    REP-->>CI: Pass/fail decision
    CI-->>PR: Gate result + artifacts

Real example: checkout timeout regression caught in CI

Here is a concrete implementation from a checkout service where payment authorization occasionally timed out under production load. The team wanted one thing: block PRs that reintroduced the failure.

Service context:

  • Service: checkout-service
  • Critical endpoint: POST /api/v2/payments/authorize
  • Incident pattern from traces: upstream payment dependency exceeded 2.5s, app retries amplified latency, then returned 502

The team implemented this in one sprint.

1) Select the route from OTel traces

They filtered traces by service and route, then exported a representative 24-hour window containing both normal and slow dependency responses.

# OTel route selection filter example
service.name == "checkout-service"
http.route == "/api/v2/payments/authorize"
duration_ms >= 2500 OR status_code == ERROR

2) Build a replay profile from that window

They checked in a route-specific profile and kept it versioned with the service code.

# replay-profiles/payments-authorize-critical.yaml
profile: payments-authorize-critical
source: production
window: 24h
filter:
  endpoints:
    - POST /api/v2/payments/authorize
  min_requests: 75
target:
  host: http://localhost:8080
  timeout: 8s

3) Sanitize sensitive fields

# transforms/payments-sanitize.yaml
rules:
  - match: header.Authorization
    action: replace
    value: "Bearer replay-token"
  - match: body.$.card.number
    action: mask
    pattern: "****-****-****-{last4}"
  - match: body.$.customer.id
    action: hash

4) Add a merge gate in CI

# .github/workflows/payments-replay-gate.yml
name: payments-replay-gate

on:
  pull_request:
    branches: [main]

jobs:
  replay-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and start service
        run: docker compose up -d --build
      - name: Replay critical payment flow
        run: |
          proxymock replay \
            --in ./proxymock/recorded/payments-authorize-critical \
            --test-against http://localhost:8080 \
            --fail-if "requests.failed != 0" \
            --fail-if "latency.p95 > 450"

5) What a real failure looked like

On one PR, a retry policy refactor increased payment retries from 2 to 4 on timeout. Unit tests passed. Integration tests passed. Replay gate failed:

GATE FAILED: payments-authorize-critical (2 regressions)

POST /api/v2/payments/authorize
  latency.p95: 388 -> 612 (threshold: 450)
  requests.failed: 0 -> 3

The author reverted that retry change, reran CI, and merged safely. No post-deploy incident, no rollback, no “why did this only fail in prod” thread.

How to choose the first workflow to validate

Do not start with a giant “replay everything” initiative. Start narrow.

Pick one workflow with all three traits:

  1. High user impact (auth, checkout, provisioning)
  2. Frequent change velocity
  3. Existing incident history

Then ship one deterministic replay gate for that path. Once the team trusts it, expand endpoint by endpoint.

Common mistakes (and fixes)

Mistake: Overfitting to one “golden” trace
Fix: Validate a representative traffic slice, not one perfect request.

Mistake: No data sanitization strategy
Fix: Apply transforms for PII, tokens, and tenant identifiers before replay.

Mistake: Treating replay as load test only
Fix: Use replay first for correctness and contract validation, then for performance.

Mistake: Non-actionable gate failures
Fix: Emit diff reports that map directly to endpoint, payload shape, and dependency callsite.

Why this matters for AI-authored code

AI assistants are excellent at local code synthesis, but weak at production-specific behavior unless you feed them executable context. An agent can write a correct-looking refactor of your payment service and still break the retry-on-timeout behavior that only surfaces under realistic concurrency with your actual payment processor response times. Unit tests will not catch it. The agent did not know to look for it.

Trace-based testing creates that context in your delivery system:

  • PR is generated
  • replay gate runs real behavior
  • diffs surface concrete failures
  • agent or human patches with evidence

That converts “prompt and pray” into a measurable, repeatable verification workflow. When the gate fails, the agent gets a structured diff it can act on instead of a vague stack trace it has to interpret. In practice this means fewer review cycles because the agent sees the production regression, patches it, and re-triggers the gate without requiring a human to manually diagnose what production behavior the AI missed.

What to do next

If you already run OpenTelemetry, you are closer than you think. Pair this with wiremock vs mockserver vs proxymock when you need to align on mock strategy before rolling out replay gates across teams.

  1. Select one high-impact workflow.
  2. Capture a representative production traffic slice.
  3. Add one replay gate to CI with explicit fail thresholds.
  4. Expand coverage only after your first gate proves stable.

Observability tells you what happened, and trace-based testing helps ensure the same failure does not ship again.

To see this in practice, start with proxymock and pair it with a docs quickstart that turns real traffic into runnable pre-release validation.

Get started for free

ProxyMock desktop or Speedscale Cloud — choose your path.