Isolate a User Session in Datadog Synthetics with proxymock
A customer pings support: “I tried to check out twice this morning and got a 500 each time, but it works fine for everyone else.” The session ID is in the email. You have full request/response capture in your environment, you have Datadog Synthetics already running browser checks against the same flow, and you still spend the next two hours grepping logs because none of those tools let you say “show me just this user’s requests, in order, and re-run them.”
That last step, replaying one user’s session as a real test, is the gap. This post walks through how to close it with proxymock export datadog-synthetics, which ships in the latest proxymock release. Session isolation uses the exporter’s --filter option alongside --service, --limit, and bundle layout flags.
Why a session ID is the right lens
When a single customer reports an intermittent failure, the useful unit of work is not “the checkout endpoint” or “all 500s in the last hour.” It is the exact sequence of API calls that customer’s browser made, in the order they happened, with the bodies they sent.
That sequence is hiding inside the larger pile of recorded traffic. To pull it out you need a stable correlation key that:
- is present on every request the user makes within a session,
- you control (so it survives a load balancer, a cookie reset, or a proxy hop), and
- does not collide with other users’ requests over the same window.
In practice that key is almost always one of:
- a
X-Session-ID(orX-Trace-Id,X-Request-ID) header set by your gateway or front-end, - a sticky cookie like
sessionid=…that your auth layer mints, - a
suborsidclaim baked into a JWT inAuthorization: Bearer …, or - a tenant-scoped query parameter (
?session=abc123) when the front-end is older.
If you have any one of these, you have everything you need to reduce a noisy capture to a single user’s story.
The workflow in one diagram
flowchart LR
A["Recorded RRPairs\n./proxymock"] -->|export| B["proxymock export\ndatadog-synthetics\n--filter '...'"]
B -->|emit| C["Datadog Synthetics\nmultistep test"]
Three steps: capture, filter-and-export, run. Datadog gives you recorders for building Synthetics tests, but those are meant for an engineer to walk through the product, not as an always-on mirror of production. You cannot leave them running to faithfully replicate real users; you get whatever sequence you remembered to click through, which often misses retries, ordering quirks, and payloads that only show up when a customer is actually stuck. Exporting from proxymock closes that gap: you feed Synthetics real user API traffic from your capture pipeline instead of approximating it with a self-recorded tour.
The new Datadog Synthetics exporter
proxymock export datadog-synthetics is new end to end: it emits Datadog Synthetics API or multistep tests from recorded RRPairs, with --service (host filter) and --limit (cap on emitted tests) for coarse slices (useful for “give me 50 tests against api.example.com”) and --filter for the session-shaped case: “give me only the 8 requests this one user made between 09
--filter accepts the standard Speedscale traffic filter expression, the same grammar proxymock search and speedctl create snapshot --filter already use. A few examples that all parse:
# Pin to one session via a header your gateway sets
--filter '(header[X-Session-ID] IS "alice-abc123")'
# Same idea, but the session ID lives in a query param
--filter '(query_param[session] IS "alice-abc123")'
# Just the writes a user attempted
--filter '(header[X-Session-ID] IS "alice-abc123") AND (command IS "POST")'
# All of one user's failed requests across a window
--filter '(header[X-Session-ID] IS "alice-abc123") AND (status NOT "200")'
The grammar is intentionally explicit: parentheses around each predicate, an uppercase operator (IS, NOT, CONTAINS, REGEX), and string values in double quotes. AND/OR connect predicates. If you have ever written a Datadog log query, the shape will feel familiar.
A few practical notes:
- Header keys are exact-match. If your gateway emits
X-Session-Id, use that exact casing. - The
ISoperator on header values does substring matching, which is usually what you want for tokens likeBearer eyJhbG...sub=alice-abc123. UseREGEXif you need to anchor. commandis the protocol-agnostic verb; for HTTP it’s the method (GET,POST, …). Filtering by command on top of a session ID is the fastest way to keep just the writes.
End-to-end: from a support ticket to a Synthetics test
Walk through the full loop. Assume ./proxymock already contains a recording window that includes the affected user.
1. Pull just that user’s traffic
A dry run with --limit 0 is the fastest way to confirm your filter actually selects the user before you commit to writing a bundle. (--limit 0 is currently treated as “unlimited,” so to validate the predicate cheaply just run the export and look at the skip counts in the sidecar variables file.)
proxymock export datadog-synthetics \
--in ./proxymock \
--out alice-session.json \
--bundle multistep \
--filter '(header[X-Session-ID] IS "alice-abc123")'
Open alice-session.variables.md next to the bundle. The “Skipped during export” section tells you how many RRPairs were dropped by --filter versus by --service versus by content-type. If filtered by --filter is suspiciously high (say, every single request), the predicate didn’t match. The most common cause is header-key casing (your gateway might emit X-SessionId, not X-Session-ID). Adjust and re-run.
2. Pick the right bundle layout
Two layouts are worth knowing for session work:
--bundle singleemits one Synthetics API test per request. Useful when you want each call individually pinned in alerts, or when the session is more than 10 requests long and you want broad coverage.--bundle multistepemits one Synthetics multistep test, one step per request, ordered by capture timestamp. This is the layout that actually re-creates the user’s flow, and it’s the one that will surface “step 4 fails when run after step 3,” which is the whole point of replaying a session.
Datadog caps multistep tests at 10 steps. If the affected session is longer, narrow it further:
# Just the failing leg of the checkout
--filter '(header[X-Session-ID] IS "alice-abc123") AND (url CONTAINS "/checkout")'
3. Let the exporter wire up variables for you
In multistep mode, the exporter scans each step’s JSON response for values that show up in a later step’s request and rewrites them as {{ EXTRACTED_<n> }}, automatically attaching a json_path parser to the producing step. That means the test you ship to Datadog is not a brittle replay of yesterday’s tokens — it’s an executable contract that says “step 1 produces a token, step 2 must use it, step 3 must use the order ID step 2 returned.”
For session-isolated bundles this matters more than it does for full exports. A single user’s flow tends to be tightly coupled (login → cart → checkout → confirmation), and the auto-extraction recovers the exact dependencies that were live when the customer hit the bug.
sequenceDiagram
participant S1 as Step 1: POST /login
participant E as Exporter
participant S2 as Step 2: POST /cart
participant S3 as Step 3: POST /checkout
S1->>E: response {"token": "xyz"}
E->>S2: inject {{ EXTRACTED_1 }}
S2->>E: response {"order_id": "456"}
E->>S3: inject {{ EXTRACTED_2 }}
4. Hand the bundle to Datadog
datadog-ci synthetics run-tests --files alice-session.json
Or push it through the Synthetics API:
curl -X POST https://api.datadoghq.com/api/v1/synthetics/tests/api \
-H "DD-API-KEY: $DD_API_KEY" \
-H "DD-APPLICATION-KEY: $DD_APP_KEY" \
-H "Content-Type: application/json" \
--data-binary @alice-session.json
Sensitive headers (Authorization, Cookie, X-Api-Key, …) were already redacted into placeholders like {{ AUTHORIZATION_1 }}. Define the corresponding global variables once in Synthetics → Settings → Global Variables using the values from alice-session.variables.md, and the bundle is portable: the same JSON can run from your laptop, your CI, or a Datadog private location, and you have not committed live credentials anywhere.
Why this beats reproducing the bug by hand
The default playbook for “one customer hit a bug nobody else hit” is to read the logs, guess the inputs, and try to reproduce it locally with curl. (If you’re still building out your Datadog testing pipeline, this walkthrough covers the broader setup.) That works when the bug is shallow. It falls apart when the failure depends on:
- a token that has since been rotated,
- ordering between two writes that race only at the user’s actual latency,
- a body field your front-end stopped sending last week,
- or a permission that exists only on that tenant.
Replaying the session as a Synthetics test sidesteps all four. The token is captured at recording time; ordering is preserved by --bundle multistep; the body is byte-for-byte what the front-end actually sent; the permission travels with the redacted-and-restored auth header. You are not reasoning about what the user probably did. You are running what they did.
That distinction is also why session replay catches regressions traditional synthetics miss. A scripted browser test exercises the happy path you wrote down six months ago. A session-filtered replay exercises the path your customer took yesterday, including the weird back-button retry on step 3 that nobody would have thought to script.
A few patterns that pay off
Once you can export filtered traffic straight into Synthetics, a few workflows become natural:
- Per-incident regression tests. When you ship a fix for a customer-reported bug, capture a 5-minute window after the fix, isolate the original session ID one more time, and check the resulting bundle into your CI. You now have a Synthetics test that fails the moment that exact regression returns.
- Tenant-scoped canaries. Use
--filter '(header[X-Tenant-Id] IS "...")'instead of a session header, and you have a canary that exercises one tenant’s real traffic against a new build. Useful before rolling out schema changes that only some tenants depend on. - Compliance-grade reproductions. When a security or compliance team needs evidence of how a specific user’s request was processed, the bundle plus the variables file is a self-contained, runnable artifact you can attach to the ticket.
Wrap-up
The mechanics are simple: capture once, filter precisely, export to a format your monitoring stack already runs. The command is a one-liner. The payoff is that “I can’t reproduce it locally” stops being a valid answer to a customer ticket.
If you want to try this against your own traffic, start with the Datadog Synthetics export guide and use --filter with whatever session key your gateway already sets (along with --in, --out, and bundle options as needed). If you do not have a session key on every request yet, that’s the first task. Add one. Everything else gets easier.