Tools: I Got Tired of "It Works on My Machine" So I Built a Time-Travel Debugger

Tools: I Got Tired of "It Works on My Machine" So I Built a Time-Travel Debugger

Source: Dev.to

The Debugging Problem Nobody Talks About ## What If You Could Record Everything? ## How It Actually Works ## What's In a Cassette? ## Replaying ## The Plugins ## HTTP Clients ## Databases ## Caching ## The Dashboard ## Dealing With Sensitive Data ## Real Use Cases ## Debugging Production Bugs ## Regression Testing ## Offline Development ## Performance Analysis ## What About VCR.py? ## Getting Started ## What's Next Last year, I spent three hours debugging a checkout bug that only happened in production. The external payment API was returning a slightly different response format than what we saw in staging. By the time I figured it out, I'd burned half my day and my patience. That was the last straw. I started building Timetracer that weekend. Now, when something breaks in production, I can capture the exact request, complete with every external API call, database query, and cache operation, and replay it on my laptop. No more guessing. No more "can you add some logging and redeploy?" This post is about why I built it, how it works, and why you might want to use it too. Here's a scenario you've probably lived through: A customer reports that their order failed. You check the logs. You see the error. You try to reproduce it locally. But your local setup hits a different database, a sandbox payment API, and your Redis is empty. The bug doesn't happen. You add more logging. Redeploy. Wait for it to happen again. Check the new logs. Still can't reproduce it. Repeat. Or maybe you're working on a feature that integrates with a third-party API, but that API is rate-limited, or requires special credentials, or just goes down at random times. Every time you run your code, you're making real API calls. Testing becomes painful. Or you're trying to demo something to your team, but the staging environment is down. Or slow. Or someone else is testing migrations on it. These problems have something in common: your code depends on things outside your control, and that makes debugging and development unpredictable. The idea behind Timetracer is simple: when your API handles a request, record everything that happens. Not just the request and response, but every external call your code makes. All of it goes into a JSON file we call a "cassette" (borrowing the term from VCR.py, which does something similar but only for HTTP). Later, when you want to debug, you load that cassette and replay the request. Timetracer intercepts all the external calls and returns the recorded responses instead of making real ones. Same input. Same external responses. Same bug. But now it's on your laptop, where you can step through the code, add breakpoints, and actually figure out what went wrong. Let's get into the technical stuff. If you're using FastAPI, adding Timetracer is two lines: That's it. The auto_setup function adds middleware that handles recording and replaying. For Flask, it's similar: You control the behavior with environment variables: Now make some requests to your app. Each request creates a cassette file. Here's a simplified version of what gets saved: Everything is there. The incoming request, the outgoing response, and every dependency call in between. To replay, point Timetracer at the cassette: Now when you hit /checkout with the same request, Timetracer intercepts the Stripe call and the database query. Instead of making real calls, it returns the recorded responses. Your code runs exactly as it did in production, but locally, without needing Stripe credentials or a production database. Recording HTTP calls is useful, but real applications do more than HTTP. That's why Timetracer has plugins for common dependencies. We support both httpx (async and sync) and the requests library: SQLAlchemy queries get captured with their SQL and parameters: Redis commands are recorded too: When you replay, all of these return the recorded values. No real database connections. No real Redis. No real HTTP calls. Just the data from the cassette. After using Timetracer for a few weeks, I got tired of opening JSON files to find the cassette I needed. So I built a dashboard. This starts a local web server with a table of all your cassettes. You can: Error responses show up with red highlighting, and slow requests (>1 second) get a warning indicator. If a request threw an exception, you see the full Python stack trace. It's nothing fancy, just a single HTML file with some JavaScript, but it makes browsing through cassettes way faster than grepping through JSON. Early on, I realized that cassettes could contain passwords, API keys, and other stuff I definitely didn't want in my Git repo. So Timetracer automatically redacts sensitive data: Headers that get stripped: Body fields that get masked: In the latest release (v1.2.0), I added pattern detection for PII, emails, phone numbers, Social Security numbers, credit card numbers. The credit card detection even validates the Luhn checksum to avoid false positives. A value like [email protected] becomes [REDACTED:EMAIL]. A credit card number becomes [REDACTED:CREDIT_CARD]. The goal is that cassettes should be safe to commit without thinking too hard about what's in them. This is the original reason I built it. Something breaks in production, you grab the cassette, replay locally, and debug with full context. Once you fix a bug, that cassette becomes a test case. You know exactly what input caused the problem and what the correct behavior should be. Run it against future code changes to make sure the bug doesn't come back. Working on a flight? At a coffee shop with bad WiFi? If you have cassettes from previous sessions, you can keep developing without hitting real APIs. Recording demo scenarios means your demos work even when external services are flaky. No more "let me refresh that" during a presentation. The timeline command generates a waterfall chart showing how long each dependency took: You can see exactly where time is being spent. Is it the database query? The third-party API? The Redis lookup? It's all right there. VCR.py is great. I've used it for years. But it only records HTTP calls. In a typical web application, a single API request might: VCR.py captures step 2 and 5. The rest? You're on your own. Timetracer captures all of it. One cassette has everything. Also, VCR.py is designed for test suites, you decorate individual test functions. Timetracer is designed as middleware. Every request through your app can be recorded. That makes it useful for production debugging, not just testing. Add the middleware, set the environment variables, and make some requests. Your first cassettes will be in the directory you specified. The documentation has more details on configuration, the CLI tools, S3 storage, and all the other features I didn't cover here. GitHub: https://github.com/usv240/timetracer I'm using Timetracer on my own projects, and I keep finding things to improve. Some ideas on the roadmap: If you try it out and have feedback, bugs, feature requests, or just thoughts, I'd love to hear it. Open an issue on GitHub or reach out on LinkedIn. Thanks for reading. I hope Timetracer saves you some debugging time. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: from fastapi import FastAPI from timetracer.integrations.fastapi import auto_setup app = auto_setup(FastAPI()) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: from fastapi import FastAPI from timetracer.integrations.fastapi import auto_setup app = auto_setup(FastAPI()) CODE_BLOCK: from fastapi import FastAPI from timetracer.integrations.fastapi import auto_setup app = auto_setup(FastAPI()) CODE_BLOCK: from flask import Flask from timetracer.integrations.flask import auto_setup app = auto_setup(Flask(__name__)) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: from flask import Flask from timetracer.integrations.flask import auto_setup app = auto_setup(Flask(__name__)) CODE_BLOCK: from flask import Flask from timetracer.integrations.flask import auto_setup app = auto_setup(Flask(__name__)) COMMAND_BLOCK: # Record mode - captures everything export TIMETRACER_MODE=record export TIMETRACER_DIR=./cassettes python app.py Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # Record mode - captures everything export TIMETRACER_MODE=record export TIMETRACER_DIR=./cassettes python app.py COMMAND_BLOCK: # Record mode - captures everything export TIMETRACER_MODE=record export TIMETRACER_DIR=./cassettes python app.py CODE_BLOCK: { "request": { "method": "POST", "path": "/checkout", "body": {"cart_id": "abc123", "user_id": "user_456"} }, "response": { "status": 200, "body": {"order_id": "order_789"}, "duration_ms": 342 }, "events": [ { "type": "http.client", "method": "POST", "url": "https://api.stripe.com/v1/charges", "request_body": {"amount": 2999, "currency": "usd"}, "response_body": {"id": "ch_xxx", "status": "succeeded"}, "duration_ms": 187 }, { "type": "db.query", "query": "INSERT INTO orders (user_id, total) VALUES (?, ?)", "params": ["user_456", 29.99], "duration_ms": 12 } ] } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "request": { "method": "POST", "path": "/checkout", "body": {"cart_id": "abc123", "user_id": "user_456"} }, "response": { "status": 200, "body": {"order_id": "order_789"}, "duration_ms": 342 }, "events": [ { "type": "http.client", "method": "POST", "url": "https://api.stripe.com/v1/charges", "request_body": {"amount": 2999, "currency": "usd"}, "response_body": {"id": "ch_xxx", "status": "succeeded"}, "duration_ms": 187 }, { "type": "db.query", "query": "INSERT INTO orders (user_id, total) VALUES (?, ?)", "params": ["user_456", 29.99], "duration_ms": 12 } ] } CODE_BLOCK: { "request": { "method": "POST", "path": "/checkout", "body": {"cart_id": "abc123", "user_id": "user_456"} }, "response": { "status": 200, "body": {"order_id": "order_789"}, "duration_ms": 342 }, "events": [ { "type": "http.client", "method": "POST", "url": "https://api.stripe.com/v1/charges", "request_body": {"amount": 2999, "currency": "usd"}, "response_body": {"id": "ch_xxx", "status": "succeeded"}, "duration_ms": 187 }, { "type": "db.query", "query": "INSERT INTO orders (user_id, total) VALUES (?, ?)", "params": ["user_456", 29.99], "duration_ms": 12 } ] } CODE_BLOCK: export TIMETRACER_MODE=replay export TIMETRACER_CASSETTE=./cassettes/checkout_abc123.json python app.py Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: export TIMETRACER_MODE=replay export TIMETRACER_CASSETTE=./cassettes/checkout_abc123.json python app.py CODE_BLOCK: export TIMETRACER_MODE=replay export TIMETRACER_CASSETTE=./cassettes/checkout_abc123.json python app.py COMMAND_BLOCK: from timetracer.plugins import enable_httpx, enable_requests enable_httpx() # For httpx calls enable_requests() # For requests library calls Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: from timetracer.plugins import enable_httpx, enable_requests enable_httpx() # For httpx calls enable_requests() # For requests library calls COMMAND_BLOCK: from timetracer.plugins import enable_httpx, enable_requests enable_httpx() # For httpx calls enable_requests() # For requests library calls CODE_BLOCK: from timetracer.plugins import enable_sqlalchemy engine = create_engine("postgresql://...") enable_sqlalchemy(engine) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: from timetracer.plugins import enable_sqlalchemy engine = create_engine("postgresql://...") enable_sqlalchemy(engine) CODE_BLOCK: from timetracer.plugins import enable_sqlalchemy engine = create_engine("postgresql://...") enable_sqlalchemy(engine) CODE_BLOCK: from timetracer.plugins import enable_redis enable_redis() Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: from timetracer.plugins import enable_redis enable_redis() CODE_BLOCK: from timetracer.plugins import enable_redis enable_redis() CODE_BLOCK: timetracer serve --dir ./cassettes --open Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: timetracer serve --dir ./cassettes --open CODE_BLOCK: timetracer serve --dir ./cassettes --open CODE_BLOCK: timetracer timeline ./cassette.json --open Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: timetracer timeline ./cassette.json --open CODE_BLOCK: timetracer timeline ./cassette.json --open COMMAND_BLOCK: pip install timetracer[fastapi,httpx] Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: pip install timetracer[fastapi,httpx] COMMAND_BLOCK: pip install timetracer[fastapi,httpx] COMMAND_BLOCK: pip install timetracer[flask,requests] Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: pip install timetracer[flask,requests] COMMAND_BLOCK: pip install timetracer[flask,requests] - That HTTP call to Stripe? Recorded. - That SELECT query to Postgres? Recorded. - That Redis GET? Recorded. - Sort by time, method, status, or duration - Filter by endpoint, HTTP method, or status code - Click a row to see full details - View the dependency timeline - See the raw JSON - Replay directly from the browser - Authorization - And about 20 others - password, secret, token - credit_card, cvv, ssn - api_key, private_key - And about 100 more patterns - Query the database for user info - Call an external API for pricing - Check a cache for recent activity - Write to another database - Make another API call - Better support for async database drivers - GraphQL request parsing - Request diffing between cassettes - Maybe a VS Code extension? - GitHub: https://github.com/usv240/timetracer - PyPI: https://pypi.org/project/timetracer/