shadow-runner
v0.1.0
Published
Silent production code tester — run new code alongside old code, compare outputs, zero risk to users
Maintainers
Readme
shadow-runner 🏃♂️💨
Silent production code tester — run new code alongside old code in production, compare outputs, zero risk to users.
Why?
You write new logic. Tests pass. But you're scared to deploy because real production traffic is unpredictable.
| Existing Tools | What They Miss | |---|---| | A/B testing libs | Changes UI, affects users | | Feature flags | Still runs one path at a time | | Staging environments | Fake data, not real traffic | | Load testing | Volume only, not logic correctness |
shadow-runner is the only zero-risk production validator — it mirrors real requests, runs both old + new logic in parallel, and silently logs any differences.
Install
npm install shadow-runnerQuick Start
import { shadow } from 'shadow-runner';
// OLD logic (currently live)
function calculateDiscount(user) {
return user.isPremium ? 0.2 : 0.1;
}
// NEW logic (you want to test safely)
function calculateDiscountV2(user) {
return user.isPremium ? 0.25 : user.orders > 10 ? 0.15 : 0.1;
}
// Wrap with shadow — users ONLY get old result
// New logic runs silently and differences are logged
const safeDiscount = shadow(calculateDiscount, calculateDiscountV2, {
sampleRate: 0.3, // Run on 30% of traffic
logDiffs: true, // Log when outputs differ
timeout: 100, // Kill shadow if it takes > 100ms
onDiff: (old, next, input) => {
metrics.track('discount_diff', { old, next, user: input.id });
},
});
// Use exactly like before — zero risk
const discount = safeDiscount(currentUser);What It Captures
[shadow-runner] DIFF DETECTED
Input: {"id":"u_991","isPremium":false,"orders":14}
Old: 0.1
New: 0.15
Duration: old=2ms, new=3ms
Sample: 312 / 1040 calls (30%)
Match: 94.2% identical so far ✅API Reference
shadow(oldFn, newFn, options?)
Returns a drop-in replacement for oldFn that silently runs newFn in the background.
Options
| Option | Type | Default | Description |
|---|---|---|---|
| sampleRate | number | 1 | Fraction of calls to sample (0–1) |
| logDiffs | boolean | true | Log diff details to console |
| timeout | number | 5000 | Kill shadow execution after N ms |
| onDiff | function | — | Callback when outputs differ |
| when | function | — | Only shadow when predicate returns true |
| autoCutover | object | — | Auto-promote when confidence is high |
| async | boolean | auto | Force async mode |
| serializer | function | — | Transform values before comparing |
| reporter | Reporter | ConsoleReporter | Custom reporter instance |
Returned Function
The returned function has the same signature as oldFn, plus:
.stats— LiveShadowStatssnapshot.disable()— Stop sampling.enable()— Resume sampling
Advanced Features
Conditional Sampling
shadow(oldFn, newFn, {
when: (input) => input.country === 'PK',
});Auto-Cutover
shadow(oldFn, newFn, {
autoCutover: {
threshold: 99.5, // 99.5% match rate
minSamples: 5000, // After 5000 real calls
onReady: () => deploy('v2'),
},
});Custom Serializer
shadow(oldFn, newFn, {
serializer: (val) => JSON.stringify(val, null, 2),
});Silent Reporter (for testing)
import { shadow, SilentReporter } from 'shadow-runner';
const reporter = new SilentReporter();
const wrapped = shadow(old, fresh, { reporter });
wrapped(input);
console.log(reporter.diffs); // All diffs
console.log(reporter.samples); // All samplesFramework Adapters
Express
import { shadowMiddleware } from 'shadow-runner/express';
app.get('/api/price',
shadowMiddleware(oldPriceHandler, newPriceHandler, {
sampleRate: 0.2,
})
);Fastify
import { shadowHandler } from 'shadow-runner/fastify';
fastify.get('/api/price', shadowHandler(
async (request, reply) => ({ price: 100 }),
async (request, reply) => ({ price: 95 }),
{ sampleRate: 0.3 }
));tRPC
import { shadowProcedure } from 'shadow-runner/trpc';
const getUser = shadowProcedure(
async ({ input, ctx }) => db.users.findById(input.id),
async ({ input, ctx }) => newDb.users.findById(input.id),
{ sampleRate: 0.5 }
);Architecture
shadow-runner/
├── src/
│ ├── shadow.ts # Core wrapper function
│ ├── sampler.ts # Traffic sampling logic
│ ├── differ.ts # Deep diff engine
│ ├── reporter.ts # Log / metrics / webhook output
│ ├── async-guard.ts # Isolates new code errors
│ ├── types.ts # TypeScript types
│ └── index.ts # Public API exports
├── adapters/
│ ├── express.ts # Express middleware
│ ├── fastify.ts # Fastify plugin
│ └── trpc.ts # tRPC middleware
├── tests/ # Comprehensive vitest suite
├── tsup.config.ts # Dual CJS + ESM build
└── package.jsonWho Needs This
- Fintech / e-commerce — refactoring pricing/discount logic
- Auth systems — testing new permission checks
- Recommendation engines — validating new algorithms
- Any team afraid to deploy refactors to production
Contributing
See CONTRIBUTING.md for guidelines.
License
MIT © usama
