Quick start
Get a real capture-and-replay loop working in about 10 minutes. By the end you will have:
- a local Softprobe runtime + Envoy sidecar running,
- a captured
*.case.jsonfile for a sample request, - a green Jest test that replays the capture without hitting the live upstream.
Prefer another language?
After you finish this walkthrough, see Replay in pytest, JUnit, or Go. The capture half is identical; only the test file changes.
Prerequisites
| Tool | Version | Why |
|---|---|---|
| Docker | 24+ | Runs the runtime, proxy, sample app, and sample upstream |
| Docker Compose | v2 (bundled) | Orchestrates the five services |
| Node.js | 20+ | Runs the Jest example |
You do not need a Kubernetes cluster, Istio, or a hosted Softprobe account for this walkthrough.
1. Clone the starter repository
git clone https://github.com/softprobe/softprobe.git
cd softprobeThe starter contains a runnable sample in e2e/: one sample app, one sample upstream, an Envoy configured with the Softprobe WASM filter, and the runtime.
2. Start the stack
docker compose -f e2e/docker-compose.yaml up --build --waitThis brings up five services:
| Service | Port | Role |
|---|---|---|
softprobe-runtime | 8080 | Control API + OTLP ingestion |
softprobe-proxy | 8082 (ingress), 8084 (egress) | Envoy + Softprobe WASM |
app | 8081 | Sample application under test |
upstream | 8083 | Sample HTTP dependency the app calls |
test-runner | — | Sanity health-check container |
When the command returns with no errors, all health checks passed. You can verify:
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8081/health3. Capture a real request
Create a capture session, send one request through the proxy, and close the session. The runtime writes a case file.
# 1) Start a capture session, grab the sessionId
SESSION_ID=$(curl -s -X POST http://127.0.0.1:8080/v1/sessions \
-H 'Content-Type: application/json' \
-d '{"mode":"capture"}' | jq -r .sessionId)
echo "Session: $SESSION_ID"
# 2) Drive the app THROUGH the ingress proxy, carrying the session header
curl -s -H "x-softprobe-session-id: $SESSION_ID" \
http://127.0.0.1:8082/hello
# 3) Close the session — this flushes the captured traces to disk
curl -s -X POST "http://127.0.0.1:8080/v1/sessions/$SESSION_ID/close"
ls -la e2e/captured.case.jsonYou now have a case file on disk. Open it — it is plain JSON with an array of OTLP-shaped traces, one per HTTP hop (ingress /hello, egress /fragment).
What just happened
The test client sent GET /hello through the ingress listener (:8082). Envoy forwarded it to the app container. The app then made an outbound GET /fragment call through the egress listener (:8084) to the upstream container. The Softprobe WASM filter reported every hop to the runtime over OTLP. On close, the runtime wrote those traces into e2e/captured.case.json.
4. Install the TypeScript SDK
mkdir -p my-first-replay && cd my-first-replay
npm init -y
npm install --save-dev jest ts-jest @types/jest typescript
npm install --save-dev @softprobe/softprobe-jsAdd a minimal jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};5. Write the replay test
Create fragment.replay.test.ts:
import path from 'path';
import { Softprobe } from '@softprobe/softprobe-js';
const softprobe = new Softprobe({ baseUrl: 'http://127.0.0.1:8080' });
describe('fragment replay', () => {
let sessionId = '';
let close = async () => {};
beforeAll(async () => {
const session = await softprobe.startSession({ mode: 'replay' });
sessionId = session.id;
close = () => session.close();
await session.loadCaseFromFile(
path.resolve(__dirname, '../softprobe/e2e/captured.case.json')
);
const hit = session.findInCase({
direction: 'outbound',
method: 'GET',
path: '/fragment',
});
await session.mockOutbound({
direction: 'outbound',
method: 'GET',
path: '/fragment',
response: hit.response,
});
});
afterAll(() => close());
it('replays /fragment through the mesh', async () => {
const res = await fetch('http://127.0.0.1:8082/hello', {
headers: { 'x-softprobe-session-id': sessionId },
});
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ message: 'hello', dep: 'ok' });
});
});What the test does
startSession({ mode: 'replay' })asks the runtime for a fresh session.loadCaseFromFileuploads the case file to the runtime and parses it in the SDK.findInCaseis an in-memory lookup — it throws if zero or multiple spans match, so ambiguity surfaces at authoring time, not test time.mockOutboundregisters a concrete mock rule on the runtime, using the captured response we found.- The test hits the SUT with the session header. The sidecar intercepts the
/fragmentcall and returns the mock instead of calling the live upstream.
6. Run it
npx jestExpected output:
PASS ./fragment.replay.test.ts
fragment replay
✓ replays /fragment through the mesh (27 ms)7. Prove that replay actually bypassed the upstream
Stop the upstream container and rerun the test — it should still pass, because /fragment is now served from the case:
docker compose -f ../softprobe/e2e/docker-compose.yaml stop upstream
npx jestStart it back up when you're done:
docker compose -f ../softprobe/e2e/docker-compose.yaml start upstreamYou're done
You have a working capture-replay loop in under 10 minutes. From here:
| I want to… | Read |
|---|---|
| Understand what each moving part does | Architecture |
| Capture a real production session and commit it | Capture your first session |
| Rewrite masked PII before replay | Write a hook |
| Run thousands of cases in CI | Run a suite at scale |
| Do the same thing in Python / Java / Go | Replay in pytest, JUnit, Go |
Troubleshooting
curl: (7) Failed to connect to 127.0.0.1 port 8080 — the stack isn't up yet. Re-run the docker compose … --wait command and watch for health-check failures.
findInCase threw: 0 matches — the capture step didn't include the span you're looking for. Open e2e/captured.case.json and search for /fragment; if it's missing, re-capture while the upstream is running.
Test hangs on fetch(…/hello) — the SUT may not be routing egress through the proxy. Check that the app container has EGRESS_PROXY_URL=http://softprobe-proxy:8084 set.
More at Troubleshooting.