An animated GIF that explains how Tusk's API testing product works by visualizing the Record and Replay modes of the Tusk SDK.
This project creates an animated explanation of Tusk Drift's API testing mechanism, which works by:
- Record Mode: Intercepting outbound API calls (SQL queries, HTTP requests, etc.) at the service boundary and storing them in Tusk Cloud
- Replay Mode: Running tests against a local service where the SDK replays cached responses instead of making real external calls
The animation helps developers understand how Tusk achieves fast, deterministic, and isolated API tests.
- Client / Tusk CLI: Left side box (dark gray for Client, purple for CLI)
- Service Under Test: Center gray box with purple dashed SDK border
- Database: Right side blue cylinder (active in Record, grayed out in Replay)
- Tusk Cloud: Bottom center purple cloud icon
- Animated Arrows: Show request/response flow with labels
Shows how the SDK captures API calls during normal operation:
Relevant Shapes:
- Client (left side, dark gray box)
- Service Under Test (center, gray box with black border)
- Tusk SDK (dashed purple border around Service)
- Database (right side, blue cylinder - full color, active)
- Tusk Cloud (bottom center, purple cloud icon)
Order of Events:
-
Client → Service (0.8s)
- Arrow animates from Client to Service
- Label:
GET /api/post/1 - Data passed: HTTP request with endpoint and parameters
-
Service processes (0.3s)
- Service box glows briefly
- Service receives request and prepares database query
-
Service → Database (0.7s)
- Arrow animates from Service to Database
- Label:
SELECT * FROM posts WHERE id = 1 - Data passed: SQL query with parameters
-
Database processes (0.4s)
- Database cylinder pulses/glows (showing it's active)
- Query executes against real database
-
Database → Service (0.7s)
- Arrow animates back from Database to Service
- Label:
{ id: "1", title: "Post title" } - Data passed: Database result set with post data
-
SDK captures (0.4s)
- Purple dashed border pulses (passive observation)
- SDK intercepts and records the DB call and response
- Note: SDK doesn't block - it observes the real call happening
-
Service → Client (0.7s)
- Arrow animates from Service to Client
- Label:
{ id: "1", title: "Post title" } - Data passed: HTTP response with post data
-
SDK → Tusk Cloud (0.9s)
- Arrow animates from SDK border to Tusk Cloud
- Label:
Export spans - Data passed: Recorded span data (request + response + timing)
-
Tusk Cloud stores (0.5s)
- Cloud scales/glows to indicate receipt
- Indicator:
Spans recorded ✓ - Span data saved for future replay
-
Pause (1.5s): Before transitioning to Replay mode
Key Visual: Database is active and full color, SDK observes passively without blocking
Shows how tests run without hitting real dependencies - database is bypassed entirely:
Relevant Shapes:
- Tusk CLI (left side, purple box - test orchestrator)
- Service Under Test (center, gray box with black border)
- Tusk SDK (dashed purple border around Service)
- Database (right side, grayed out cylinder with ✕ mark - NOT called)
- Tusk Cloud (bottom center, purple cloud icon)
Order of Events:
-
Tusk Cloud → Tusk CLI (0.8s)
- Arrow animates from Cloud to CLI
- Label:
Fetch test spans - Data passed: Previously recorded span data downloaded before test runs
- Note: This happens first - CLI pre-loads the test data
-
Tusk CLI receives (0.3s)
- CLI box pulses briefly
- Test runner now has all recorded spans in memory
-
Tusk CLI → Service (0.8s)
- Arrow animates from CLI to Service
- Label:
GET /api/post/1 - Data passed: Same HTTP request as Record mode
-
Service processes (0.3s)
- Service box glows briefly
- Service receives request and attempts to query database
-
Service attempts Database call (0.4s)
- Faded/dotted arrow starts animating toward Database
- Label:
SELECT * FROM posts WHERE id = 1 - Arrow stops mid-way (intercepted before reaching DB)
- Data passed: Query intent, but never reaches database
-
SDK intercepts (0.5s)
- Purple dashed border strongly pulses/glows (active blocking)
- Arrow stops completely before reaching Database
- Label:
Found spanappears between Service and Database - Note: SDK actively blocks the call - this is the key difference from Record mode
-
Database bypassed (0.3s)
- Database remains grayed out (30% opacity)
- Large "✕ BYPASSED" or "Not called" label appears
- No arrow reaches the Database
- Visual emphasis: Make it very clear the DB is NOT touched
-
SDK replays span from cache (0.5s)
- Glow/pulse inside SDK border area
- Label:
Span replayedorUsing cached response - Data passed: SDK serves the cached response from memory (loaded in step 1)
- Response comes from SDK's memory, not from Database or Cloud
-
Service → Tusk CLI (0.7s)
- Arrow animates from Service to CLI
- Label:
{ id: "1", title: "Post title" } - Data passed: Same response as Record mode, but from cache
-
Test success (0.4s)
- Checkmark appears near CLI
- Label:
✓ Test passedor✓ API test complete - Test validates response matches expectations
-
Pause (1.5s): Before looping back to Record mode
Key Visual: Database is grayed out with X mark - SDK actively intercepts and serves cached data
REPLAY Mode Emphasis:
- Tusk CLI is the test orchestrator (not just a passive client)
- Database is clearly bypassed: Grayed out, ✕ mark, no arrows complete to it
- SDK actively blocks: Shows intercepting action with strong pulse
- Cached data comes from SDK's memory: Already fetched from Cloud in step 1
- Faster execution: Whole sequence is quicker than RECORD (no real DB latency)
- Record → Replay: 0.5s crossfade, database transitions to grayscale, "Client" becomes "Tusk CLI"
- Replay → Record: 0.5s crossfade, database transitions to color, "Tusk CLI" becomes "Client"
- React (via Vite): Fast development with HMR
- Motion (
motion/react): Declarative animation library (formerly Framer Motion) - Tailwind CSS: Utility-first styling with custom Tusk purple (
#9900ff) - SVG: Vector graphics for all visual elements
- Puppeteer: Headless browser automation for GIF generation
- ffmpeg: GIF compilation and optimization
- Motion over CSS animations: Declarative, AI-friendly, easier to orchestrate complex sequences
- SVG over Canvas: Scalable, crisp at any resolution, easier to position and style
- Tailwind: Inline utility classes keep styling colocated with components
- Puppeteer + ffmpeg: Automated, reproducible GIF generation with compression
src/
├── App.jsx # Mode switcher (Record ↔ Replay)
├── components/
│ ├── RecordMode.jsx # Record animation sequence
│ ├── ReplayMode.jsx # Replay animation sequence
│ ├── ServiceBox.jsx # Service + SDK border
│ ├── Database.jsx # Cylinder with active/bypassed states
│ ├── TuskCloud.jsx # Cloud icon with pulse effects
│ ├── ClientBox.jsx # Client/CLI box (changes color by mode)
│ ├── ExternalService.jsx # External service box (optional)
│ └── AnimatedArrow.jsx # Reusable arrow with labels
├── index.css # Tailwind imports + base styles
└── main.jsx # React entry point
generate-gif.js # Puppeteer script to create GIF
output/
└── tusk-animation.gif # Generated animation (gitignored)
- Node.js - Version specified in
.nvmrcfile- If you have nvm installed, simply run:
nvm use - Otherwise, ensure you have Node.js v18+ installed
- If you have nvm installed, simply run:
- npm (v9+)
- ffmpeg: Required for GIF generation
# macOS brew install ffmpeg # Ubuntu/Debian sudo apt install ffmpeg # Windows (via Chocolatey) choco install ffmpeg
npm installInstalls:
react,react-dom: UI frameworkmotion: Animation librarytailwindcss,@tailwindcss/postcss: Stylingpuppeteer: Browser automation (downloads Chromium automatically)vite: Build tool and dev server
Start the live development server with hot reloading:
npm run devThen open http://localhost:5173 in your browser. The animation will loop continuously, switching between Record and Replay modes every 7 and 6 seconds respectively.
To create the animated GIF:
node generate-gif.jsWhat it does:
- Launches headless Chrome via Puppeteer
- Opens
http://localhost:5173 - Captures 140 frames at 10fps over 14 seconds (one complete loop)
- Compiles frames into a GIF using ffmpeg with optimized palette
- Outputs to
./output/tusk-animation.gif
Default settings:
- Frame rate: 10fps (every 100ms)
- Duration: 14 seconds (Record 7s + Replay 6s + transitions 1s)
- Resolution: 1200×500px
- File size: ~0.2-0.3 MB (well under 5MB limit)
Edit generate-gif.js to change settings:
const FRAME_RATE = 15; // 15fps instead of 10fps
const DURATION = 14000; // Keep duration sameThis increases smoothness but file size (~0.4-0.5 MB).
await page.setViewport({ width: 1600, height: 600 }); // Larger canvasUpdate SVG viewBox in components accordingly.
const DURATION = 28000; // 2 full loops (28 seconds)The ffmpeg command in generate-gif.js already uses optimized settings:
scale=1200:-1:flags=lanczos: High-quality scalingpalettegen=max_colors=128: Optimized color palettepaletteuse=dither=bayer: Smooth dithering
To reduce file size further:
'palettegen=max_colors=64' // Fewer colors (was 128)To increase quality:
'palettegen=max_colors=256' // More colors (was 128)Each visual element is a self-contained React component with its own animation variants:
// Example: ServiceBox with pulse animation
const serviceBoxVariants = {
idle: { opacity: 1 },
glow: {
filter: [
'drop-shadow(0 0 0px rgba(153, 0, 255, 0))',
'drop-shadow(0 0 12px rgba(153, 0, 255, 0.6))',
'drop-shadow(0 0 0px rgba(153, 0, 255, 0))'
],
transition: { duration: 0.4 }
}
};Using Motion's staggerChildren and delayChildren for automatic timing:
const recordSequence = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.5 // Each step starts 0.5s after previous
}
}
}Arrows use SVG pathLength animation for smooth drawing effect:
const pathVariants = {
hidden: { pathLength: 0, opacity: 0 },
visible: {
pathLength: 1,
opacity: 1,
transition: { duration: 0.8, ease: "easeInOut" }
}
};AnimatePresence provides smooth crossfade between modes:
<AnimatePresence mode="wait">
<motion.div
key={mode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
{mode === 'record' ? <RecordMode /> : <ReplayMode />}
</motion.div>
</AnimatePresence>All coordinates calculated from box dimensions for pixel-perfect alignment:
const clientRightX = clientX + clientWidth;
const serviceCenterX = serviceX + serviceWidth / 2;
// Arrows connect at exact box edges| Aspect | RECORD Mode | REPLAY Mode |
|---|---|---|
| Database | Full color (blue), active, pulses | Grayed (30% opacity), ✕ mark, static |
| SDK Border | Subtle pulse (passive observation) | Strong pulse (active interception) |
| Arrows to DB | Solid arrows that complete | Arrows show attempt but are intercepted |
| Requester | "Client" (dark gray box) | "Tusk CLI" (purple box) |
| Cloud Timing | Receives at END (step 8-9) | Sends at BEGINNING (step 1) |
| Overall Feel | Real API call (slower, hits DB) | Fast test (DB bypassed, cached) |
Edit tailwind.config.js:
theme: {
extend: {
colors: {
'tusk-purple': '#9900ff', // Change brand color
}
}
}Or update component fill colors directly in SVG:
fill="#9900ff" // Tusk purple
fill="#3b82f6" // Database blue
fill="#9ca3af" // Service grayEdit the mode durations in src/App.jsx:
const duration = mode === 'record' ? 7000 : 6000; // millisecondsEdit individual step durations in RecordMode.jsx and ReplayMode.jsx:
delay={1.1} // When to start
duration={0.8} // How long it takes- Create new component in
src/components/ - Add to
RecordMode.jsxand/orReplayMode.jsx - Define position and animation variants
- Add arrows connecting to other elements
"ffmpeg: command not found"
- Install ffmpeg (see Prerequisites)
"Error: Failed to launch the browser process" (Puppeteer)
- Run:
npm install puppeteer(reinstalls Chromium) - Or set
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=false
GIF file size too large
- Reduce
FRAME_RATEingenerate-gif.js - Reduce
max_colorsin ffmpeg palette generation - Capture fewer frames or shorter duration
Arrows not aligned
- Check box dimensions match position calculations
- Verify edge coordinates:
boxX + widthfor right edge,boxXfor left edge
Timing feels off
- Adjust
delayvalues in AnimatedArrow components - Modify mode duration in
App.jsx - Check
staggerChildrenin animation variants
Components overlapping
- Update x/y positions in mode components
- Adjust SVG viewBox size if needed
- Development: Animations run at 60fps in browser
- GIF Export: Captured at 10fps to keep file size small
- File size: ~0.2MB at 10fps, 1200×500px, 128 colors
- Generation time: ~18-20 seconds for 14-second animation
- Fix spacing of arrows
- Fix sizes and locations of objects
- Add external service interactions (currently simplified)
- Implement more detailed span visualization