New Relic Traces Behind a Failing Test
Most teams treat a red test as a binary: something broke, find it, fix it. The harder problem is that the test failure is often the last observable event in a chain that started three services and two async queues ago. The stack trace points at an assertion; the actual cause lives in a 400ms database lock that only shows up under load, in a staging environment, on the second retry. That gap — between the test result and the causal span — is where MTTR quietly inflates.
New Relic's distributed tracing gives you the full request graph. The problem is that most teams run it in production and ignore it in test automation, treating logs and traces in test automation as a production-only concern. That's the gap this article closes.
By the end, you'll be able to inject a New Relic trace ID into your JUnit XML output, query failing spans directly, and wire a Grafana panel that surfaces the slowest downstream call behind any red test — without leaving your dashboard.
Learn Node.js, Cucumber, GitHub Copilot, APIs, CI/CD, and modern automation by building a complete framework.
What a Distributed Trace Adds to a Test Failure
A distributed trace in New Relic is a directed acyclic graph of spans — each span representing a unit of work (HTTP call, DB query, queue publish) with a start time, duration, status, and parent/child relationship. When a test exercises an API endpoint, that request propagates a W3C traceparent header through every downstream service. New Relic captures each hop as a span under a single trace.id. A failing test that surfaces a 500 response may have a perfectly healthy application span at the top — and a timed-out Postgres query four levels deep.
In a modern test architecture, traces sit between your test runner and your observability backend. The test runner (Pytest, Playwright, k6) generates the assertion signal; the trace provides the execution context. Without linking the two, you're doing triage with half the data. This is especially acute for E2E and API contract tests, where the service under test is a black box from the test's perspective — the trace is the only window into what actually happened during that test invocation.
Injecting Trace IDs into Test Results and Querying Failing Spans
The linkage starts at the HTTP client. In your test setup, read the traceparent header from the response and attach it to the test's metadata. For Pytest, a session-scoped fixture handles this cleanly:
# conftest.py — attach New Relic trace ID to each test's JUnit XML properties
import pytest
@pytest.fixture(autouse=True)
def attach_trace_id(request, api_client):
response = api_client.last_response # your HTTP client wrapper
trace_id = response.headers.get("X-Trace-Id", "") # NR injects this on instrumented services
if trace_id:
request.node.user_properties.append(("nr_trace_id", trace_id))
Pytest's user_properties list writes into the <properties> block of JUnit XML when you run with --junitxml=results.xml. That gives you a nr_trace_id property on every test case — parseable by any results ingestion pipeline. From there, a small Python post-processor pulls the trace from New Relic's GraphQL API and annotates failures:
import requests, json, sys
NR_API_KEY = "NRAK-..."
NR_ACCOUNT_ID = "1234567"
GRAPHQL_URL = "https://api.newrelic.com/graphql"
def fetch_trace(trace_id: str) -> dict:
query = """
{
actor {
account(id: %s) {
tracing {
trace(traceId: "%s") {
spans {
id name duration.ms
attributes
error.message
}
}
}
}
}
}
""" % (NR_ACCOUNT_ID, trace_id)
r = requests.post(
GRAPHQL_URL,
headers={"API-Key": NR_API_KEY, "Content-Type": "application/json"},
json={"query": query},
timeout=10,
)
return r.json()["data"]["actor"]["account"]["tracing"]["trace"]
Run this against every failed test's nr_trace_id in CI post-step. Persist the span payload to PostgreSQL or ClickHouse alongside the test result row. A simple JOIN then tells you which downstream service appears in the error path most often across failures — a query that would otherwise require manually clicking through the New Relic UI for each incident. Teams that have wired this into their CI failure localization workflow report triage time dropping from 20+ minutes per failure to under 5 once the span data is co-located with the test result.
Performance Testing with Traces
For k6 load tests, the pattern shifts: you're not tracing a single request but looking for latency regressions at the P95/P99 band. Instrument your k6 script to emit the traceparent header, then query New Relic NRQL for span duration distribution during the test window:
-- NRQL: P95 span duration for checkout service during load test window
SELECT percentile(duration.ms, 95) FROM Span
WHERE service.name = 'checkout-api'
AND timestamp BETWEEN 1718000000000 AND 1718003600000
FACET name
LIMIT 20
Faceting by span name surfaces which specific operation — db.query.inventory_lock, cache.get.session — crossed the SLO threshold during the test run. This is far more actionable than a k6 summary showing "P95 latency: 420ms" with no attribution.
Where Trace-to-Test Linkage Breaks in Practice
The most common failure mode is header propagation gaps. If any service in the call chain doesn't forward the W3C traceparent header — a legacy internal library, a third-party SDK, a message queue consumer that doesn't instrument its outbound calls — the trace fragments. You get two orphaned traces instead of one connected graph, and the JOIN between test result and root cause span silently returns nothing. Audit your service instrumentation before trusting trace completeness; New Relic's "Distributed Tracing" UI will show disconnected roots as a symptom. This is the same coverage gap that passing traces can hide when span coverage is incomplete.
The second mistake is sampling rate mismatch. New Relic's default head-based sampling drops traces in high-throughput environments. If your staging environment runs at 10% sample rate, 90% of your test-triggered traces never land in NRQL — and your post-processor silently gets empty results. For test environments specifically, set NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=true and configure tail-based sampling or 100% sample rate on the test-origin service. Don't inherit production sampling config in staging without reviewing it.
Myths About Traces and Test Automation Teams
Myth 1: Traces are for production incidents, not test runs. This is the most expensive misunderstanding. A failing E2E test in CI is a production incident that hasn't happened yet. The trace from that test run is the cheapest reproduction you'll ever get — a controlled, repeatable request graph with known inputs. Teams that treat traces as a post-production tool miss the window where fixing the root cause costs the least. If you're already thinking about how to structure your observability stack, the quality engineering stack in 2026 treats trace correlation in CI as a first-class capability, not an afterthought.
Myth 2: A passing trace means the test covered the right spans. A test can pass while exercising only a shallow subset of the service graph — cached responses, feature-flagged code paths, mocked dependencies. The trace looks clean because nothing errored; it doesn't mean the critical path was exercised. Correlating trace span count and depth against a baseline is a better coverage signal than pass/fail alone. Myth 3: Trace-based triage only helps with latency. Span attributes carry error messages, queue depths, cache hit/miss flags, and feature flag states — any of which can explain a functional failure that has nothing to do with timing.
The practical starting point: instrument one service, inject nr_trace_id into your JUnit XML for that service's tests, and run the GraphQL fetcher on your next red build. You don't need a full observability overhaul — you need one trace linked to one failure to see the pattern. From there, if you're dealing with repeated failures that look similar across runs, embedding-based deduplication across span error messages is the logical next layer.
Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.