<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ujwal Vanjare</title>
    <description>The latest articles on DEV Community by Ujwal Vanjare (@ujwal240).</description>
    <link>https://dev.to/ujwal240</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3717448%2Fc1944409-c9df-4fa3-a96a-9b4df814231e.png</url>
      <title>DEV Community: Ujwal Vanjare</title>
      <link>https://dev.to/ujwal240</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ujwal240"/>
    <language>en</language>
    <item>
      <title>Making product recalls executable with Aurora DSQL and Vercel</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Thu, 25 Jun 2026 03:53:41 +0000</pubDate>
      <link>https://dev.to/ujwal240/making-product-recalls-executable-with-aurora-dsql-and-vercel-2nja</link>
      <guid>https://dev.to/ujwal240/making-product-recalls-executable-with-aurora-dsql-and-vercel-2nja</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Live demo: &lt;a href="https://safestate.vercel.app" rel="noopener noreferrer"&gt;https://safestate.vercel.app&lt;/a&gt; , code: &lt;a href="https://github.com/usv240/safestate" rel="noopener noreferrer"&gt;https://github.com/usv240/safestate&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A product recall today is basically a notice. It lives on a webpage, or a PDF, or an email that somebody is supposed to read. Say the problem out loud and it gets uncomfortable fast. A recalled crib can be listed and sold to another family, and nobody in that sale ever sees the recall. Reselling recalled goods is actually illegal, and recalled infant products have killed kids.&lt;/p&gt;

&lt;p&gt;I spent this hackathon building something to close that gap. I called it SafeState, and the idea is small: make the recall do something. When a second-hand item is listed or sold, the marketplace checks SafeState first, and recalled units get blocked right at checkout. It is precise down to the serial number, so safe units still sell.&lt;/p&gt;

&lt;p&gt;It runs on the stack this hackathon is about. A Next.js front end on Vercel, with Amazon Aurora DSQL behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DSQL is the whole point here
&lt;/h2&gt;

&lt;p&gt;The promise SafeState has to keep is this: the moment a recall lands in any region, no marketplace anywhere should ever read that product as "safe" again.&lt;/p&gt;

&lt;p&gt;That is a strong consistency problem, not a nice-to-have. If there is any window where a recalled product still looks safe, that is exactly when it gets sold. An eventually consistent store or a nightly sync leaves that window open. DSQL's active-active, multi-region setup with strong consistency is what closes it.&lt;/p&gt;

&lt;p&gt;I set up a real peered cluster across us-east-1 and us-east-2, with us-west-2 as the witness. Write a recall through one region's endpoint and you can read it back from the other region right away. There is a page in the app that lets you run that yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one trick that makes it work
&lt;/h2&gt;

&lt;p&gt;DSQL runs on snapshot isolation (PostgreSQL REPEATABLE READ) with optimistic concurrency. It catches write-write conflicts at commit time. Snapshot isolation will not protect you from write skew, so I had to design around that.&lt;/p&gt;

&lt;p&gt;To guarantee that a recall and a sale of the same product actually collide, I make both of them write the same row. Every model has one &lt;code&gt;safety_guard&lt;/code&gt; row that holds its status and an epoch number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// authorize-transfer, simplified. The AUTHORIZED path touches the SAME guard&lt;/span&gt;
&lt;span class="c1"&gt;// row a concurrent recall writes, so DSQL is forced to detect the conflict.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT epoch FROM safety_guard WHERE model_id = $1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// ...evaluate every active directive against THIS unit's serial...&lt;/span&gt;
&lt;span class="c1"&gt;// if it is covered, return BLOCKED. otherwise:&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INSERT INTO ownership_transfers (...) VALUES (...)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UPDATE product_instances SET current_owner_id = $1 WHERE id = $2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;buyer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UPDATE safety_guard SET updated_at = now() WHERE model_id = $1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// the conflict-forcing write&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the loser throws SQLSTATE 40001 / OC000 here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the recall commits first, the sale's COMMIT throws &lt;code&gt;SQLSTATE 40001&lt;/code&gt; (&lt;code&gt;OC000&lt;/code&gt;). A small wrapper catches it, backs off with some jitter, and runs the whole transaction again. The second time around it reads the recalled state and returns BLOCKED. So there is no version of events where a recalled product slips through as safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RETRYABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;40001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OC000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OC001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// retry the WHOLE transaction on conflict, backoff plus jitter, max 3 attempts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Proving it under load
&lt;/h2&gt;

&lt;p&gt;A guarantee you cannot see is just a claim, so I put a stress test right in the app: a hundred concurrent attempts to buy a recalled unit, fired at the live cluster at once. Every one comes back blocked. Zero recalled units sell, no matter the concurrency.&lt;/p&gt;

&lt;p&gt;Getting there taught me something. My first version put a &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; on the guard row in every check. That was overkill. Two blocked checks on the same model would each take a write intent on that one row and conflict with each other for no reason. The conflict I actually care about is between a recall and an authorized sale, and both of those already write the guard row. So I dropped the &lt;code&gt;FOR UPDATE&lt;/code&gt; from the read. The blocked path stopped fighting itself, the load test went clean, and the recall versus sale conflict still fires exactly as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two databases, on purpose
&lt;/h2&gt;

&lt;p&gt;Not everything belongs in the transactional store. Every public check, verify, and scan is an event worth counting, and every Safety Receipt is a small durable record. That stream is write-heavy and key-accessed, and it does not need a distributed transaction. So it lives in Amazon DynamoDB, while Aurora DSQL keeps the transactional core. Picking the right database per workload, instead of forcing one to do both, kept the hot path clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vercel side
&lt;/h2&gt;

&lt;p&gt;Route handlers talk to DSQL over the normal Postgres protocol, but auth is a short-lived IAM token minted per connection with &lt;code&gt;@aws-sdk/dsql-signer&lt;/code&gt;. There is no database password sitting in an env var anywhere.&lt;/p&gt;

&lt;p&gt;A Vercel Cron job pulls real recalls from the public CPSC API once a day. And Claude reads messy second-hand listings, the kind a person actually writes ("used baby sleeper, works fine"), and figures out which recall they match, with a confidence score. The uncertain ones go to a review queue instead of being auto-blocked.&lt;/p&gt;

&lt;p&gt;Two more things the same app does. When a recall is issued, it walks live ownership and emails the people who own one now, not the original buyers. And the public check fans out to CPSC, FDA, and NHTSA at once, so you can look up a product, a bag of spinach, or a car.&lt;/p&gt;

&lt;p&gt;One thing that cost me an hour. Vercel functions run on Lambda, and Lambda reserves &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; and &lt;code&gt;AWS_REGION&lt;/code&gt;. You cannot set those as env vars. So I pass the DSQL credentials under different names and hand them to the signer directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAFESTATE_AWS_ACCESS_KEY_ID&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accessKeyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAFESTATE_AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;secretAccessKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAFESTATE_AWS_SECRET_ACCESS_KEY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// local dev falls back to the default AWS provider chain&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DsqlSigner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;creds&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;creds&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A few things that helped
&lt;/h2&gt;

&lt;p&gt;If you build on DSQL, pick a problem where being correct under concurrency is the actual product, not a side detail. That is where it earns its keep. Make your conflicting operations write the same row so OCC has something to catch. And write the retry-on-40001 wrapper before anything else, because you will lean on it constantly.&lt;/p&gt;

&lt;p&gt;Recalls should stop being PDFs and start being decisions. Aurora DSQL and Vercel got me there over a weekend.&lt;/p&gt;

&lt;p&gt;Live: &lt;a href="https://safestate.vercel.app" rel="noopener noreferrer"&gt;https://safestate.vercel.app&lt;/a&gt; , code: &lt;a href="https://github.com/usv240/safestate" rel="noopener noreferrer"&gt;https://github.com/usv240/safestate&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I built this for the H0: Hack the Zero Stack hackathon. #H0Hackathon&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>webdev</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>I built a GitLab flow that tells you what a diff actually means</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:08:24 +0000</pubDate>
      <link>https://dev.to/ujwal240/i-built-a-gitlab-flow-that-tells-you-what-a-diff-actually-means-3bfp</link>
      <guid>https://dev.to/ujwal240/i-built-a-gitlab-flow-that-tells-you-what-a-diff-actually-means-3bfp</guid>
      <description>&lt;p&gt;Every code review starts the same way. You open a diff and try to figure out&lt;br&gt;
what it actually means - not just what changed, but what depends on it,&lt;br&gt;
whether it breaks something downstream, and whether someone else is about to&lt;br&gt;
step on the same code. That investigation usually falls on the reviewer,&lt;br&gt;
by hand, every single time.&lt;/p&gt;

&lt;p&gt;I spent the last two weeks building something to change that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Orbit Change Passport&lt;/strong&gt; is a GitLab Duo Agent Platform flow that fires&lt;br&gt;
automatically when a merge request is marked ready for review. It queries&lt;br&gt;
GitLab Orbit's Knowledge Graph and posts a structured comment before anyone&lt;br&gt;
reads a single line of the diff.&lt;/p&gt;

&lt;p&gt;Here is what the comment covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Changed Surface&lt;/strong&gt; - the exact functions and methods the diff touched,
identified by name, not line numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency graph&lt;/strong&gt; - a live Mermaid diagram of everything that imports
the changed modules, rendered inline in GitLab&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict Radar&lt;/strong&gt; - every other open MR right now that touches the same
code, caught before merge instead of at it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-project blast radius&lt;/strong&gt; - files in other projects in the group that
depend on what changed. This one is only possible because Orbit's graph
spans the whole group, not just one repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test gaps&lt;/strong&gt; - test files that import the changed module but were not
touched in this MR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suggested reviewers&lt;/strong&gt; - based on recent authorship of dependent files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewer Brief&lt;/strong&gt; - a second shorter comment posted immediately after:
one paragraph with the single most important thing before reading the diff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A companion Duo Chat agent lets reviewers keep asking questions against the&lt;br&gt;
same graph after the passport posts - "what else calls this?",&lt;br&gt;
"who imports this file?" - answered live, not restated from the comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;Here is what the impact line looks like on a real run:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Impact: HIGH - 2 definitions changed - 2 within-project dependents -&lt;br&gt;
cross-project blast radius (3 projects) - 1 conflict&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That single line tells me: this change has dependents outside this repo,&lt;br&gt;
and another open MR is already touching the same functions. Both of those&lt;br&gt;
things would normally take 10 minutes to find manually. They showed up&lt;br&gt;
automatically before I opened the diff.&lt;/p&gt;

&lt;p&gt;The cross-project result is the one that surprised me most. A docstring&lt;br&gt;
change to &lt;code&gt;engine/orbit_client.py&lt;/code&gt; surfaced 14 dependent files across three&lt;br&gt;
other projects in the group. Nobody catches that by scrolling through a diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The flow uses a three-tier strategy to identify which functions a diff&lt;br&gt;
actually touched:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DEFINES traversal in the Orbit graph (File to Definition edge)&lt;/li&gt;
&lt;li&gt;fqn-fragment query as a fallback for Rust crates&lt;/li&gt;
&lt;li&gt;Bounded page scan as a last resort&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each tier produces a different confidence level in the output. This redundancy&lt;br&gt;
matters because query availability on the live Orbit instance is not guaranteed.&lt;br&gt;
DEFINES, IMPORTS, CALLS, and Definition filtering have each independently been&lt;br&gt;
unavailable within a two-hour window during testing. A flow that only works&lt;br&gt;
when one specific query is live is not useful.&lt;/p&gt;

&lt;p&gt;For dependents, the flow runs ImportedSymbol lookups in both FQN and&lt;br&gt;
crate-relative forms for Rust, and by identifier_name stem for Python files.&lt;br&gt;
That last one took a while to figure out. Python relative imports&lt;br&gt;
(&lt;code&gt;from . import module&lt;/code&gt;) are stored in Orbit with &lt;code&gt;import_path&lt;/code&gt; set to the&lt;br&gt;
package name and &lt;code&gt;identifier_name&lt;/code&gt; set to the module stem, not as a dotted&lt;br&gt;
path. We found that by reading the actual graph data.&lt;/p&gt;

&lt;p&gt;The Reviewer Brief is a second call to &lt;code&gt;create_merge_request_note&lt;/code&gt; within the&lt;br&gt;
same agent turn. We arrived at this after two other approaches failed: a&lt;br&gt;
router-chained second AgentComponent that never started, and a second ambient&lt;br&gt;
flow that turned out to share the trigger silently with the first. Only the&lt;br&gt;
most-recently-enabled one fires - that was not documented anywhere and took a&lt;br&gt;
while to diagnose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is in the repo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flows/change-passport/change-passport.yaml   - the Duo Agent Platform flow
skills/ask-orbit-passport/SKILL.md           - companion Duo Chat agent
engine/passport_runner.py                    - CLI runner, posts to any MR
engine/orbit_client.py                       - Orbit query layer
research/findings.md                         - every query pattern confirmed
                                               against the live graph
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The flow is live in the AI Catalog. The repo is public and MIT licensed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://gitlab.com/gitlab-ai-hackathon/transcend/38491653" rel="noopener noreferrer"&gt;https://gitlab.com/gitlab-ai-hackathon/transcend/38491653&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Flow: &lt;a href="https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/automate/flows/1011552/" rel="noopener noreferrer"&gt;https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/automate/flows/1011552/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent: &lt;a href="https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/automate/agents/1011562" rel="noopener noreferrer"&gt;https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/automate/agents/1011562&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo MR: &lt;a href="https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/merge_requests/5" rel="noopener noreferrer"&gt;https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/merge_requests/5&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built for the GitLab Transcend Hackathon.&lt;/p&gt;

&lt;p&gt;A diff shows what changed. Orbit Change Passport shows what that change means.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>opensource</category>
      <category>ai</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Scaling Astrolord: Our Journey with Serverless on AWS</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Wed, 04 Feb 2026 03:23:53 +0000</pubDate>
      <link>https://dev.to/ujwal240/scaling-astrolord-our-journey-with-serverless-on-aws-2j78</link>
      <guid>https://dev.to/ujwal240/scaling-astrolord-our-journey-with-serverless-on-aws-2j78</guid>
      <description>&lt;p&gt;When we started building &lt;a href="https://astro-lord.com" rel="noopener noreferrer"&gt;Astrolord&lt;/a&gt;, we knew we had a tricky engineering problem on our hands. We were building an AI-powered astrology platform that needed to perform heavy astronomical calculations one second and stream personalized AI responses the next. We needed infrastructure that could handle a sudden spike in traffic, like during a major planetary transit, without burning a hole in our pocket during quiet hours.&lt;/p&gt;

&lt;p&gt;We decided early on to go all-in on AWS Serverless. It wasn't just about avoiding server management; it was about building an architecture that could scale to zero when no one was using it, and scale up infinitely when they were. Here is a look at how we pieced it all together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F690st14e065ha0u7eqmi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F690st14e065ha0u7eqmi.png" alt="High Level Architecture Diagram - Showing React/S3 and FastAPI/Lambda flows" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compute Layer: AWS Serverless
&lt;/h2&gt;

&lt;p&gt;The heart of our backend is Python. The challenge was deploying our application in a way that didn't require maintaining a fleet of servers or paying for idle time.&lt;/p&gt;

&lt;p&gt;We chose AWS Lambda for its efficiency. By adapting our web application to run in a serverless environment, we can write modern, clean code while letting AWS handle the underlying infrastructure.&lt;/p&gt;

&lt;p&gt;There were trade-offs, of course. We had to be careful with "cold starts", the initial delay when a function wakes up. We spent time optimizing our application startup to keep latency minimal. But the benefit of paying literally zero dollars when no traffic is hitting the API made it an easy choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Front Door: API Gateway
&lt;/h2&gt;

&lt;p&gt;Sitting in front of those Lambda functions is Amazon API Gateway. Connective tissue is often overlooked, but for us, this piece is critical. It acts as our secure entry point, routing requests to the correct backend services.&lt;/p&gt;

&lt;p&gt;Beyond just routing, we rely on it for stability. We configured usage plans and throttling rules directly at the gateway level. This means if a bad actor tries to hammer our API, AWS blocks them before they even wake up our compute layer, saving us money and processing power.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivering the UI: S3 and CloudFront
&lt;/h2&gt;

&lt;p&gt;For the frontend, we built a static React application. We didn't want to run a web server or manage containers just to serve HTML and JavaScript files.&lt;/p&gt;

&lt;p&gt;Instead, we use what I consider the "gold standard" for static hosting: Amazon S3 paired with CloudFront. We upload our build artifacts into an S3 bucket, which offers incredible durability. Then, CloudFront sits in front of that bucket, caching our application at edge locations all over the world.&lt;/p&gt;

&lt;p&gt;The result is that a user in Mumbai loads the app just as fast as a user in New York. We also configured strict security controls, ensuring that no one can bypass the CDN to hit our bucket directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmj1477mqexsmtjstpeur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmj1477mqexsmtjstpeur.png" alt="Astrolord Dashboard - The result of our React + Vite frontend" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability
&lt;/h2&gt;

&lt;p&gt;One thing they don't tell you about distributed systems is that debugging can be a nightmare if you aren't prepared. Since we don't have a single server to SSH into, we rely heavily on centralized logging and monitoring.&lt;/p&gt;

&lt;p&gt;We capture all standard output from our functions into CloudWatch Logs. We also set up specific metric filters to track errors and throttling events. If our error rate crosses a certain threshold, an alarm triggers and we get notified immediately. It gives us the confidence to deploy on a Friday (well, almost).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjiywon5fouw85rfq0nox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjiywon5fouw85rfq0nox.png" alt="AWS CloudWatch Metrics - Showing Lambda invocations or low latency" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Economics of Serverless
&lt;/h2&gt;

&lt;p&gt;One of the biggest wins for us wasn't just technical, it was financial.&lt;/p&gt;

&lt;p&gt;With a traditional EC2 or container-based architecture, you are paying for capacity 24/7. Even if no one visits your site at 3 AM, that server is still running, and you are still receiving a bill.&lt;/p&gt;

&lt;p&gt;With this serverless setup, our infrastructure cost aligns perfectly with our business growth.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Zero Idle Costs&lt;/strong&gt;: When our app is quiet, our compute bill is literally $0.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Generous Free Tier&lt;/strong&gt;: AWS offers a substantial free tier for Lambda and API Gateway, meaning we effectively run our development and testing environments for free.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;No Over-Provisioning&lt;/strong&gt;: We never have to guess how many servers we need. The system just scales to meet demand, whether that's 5 users or 5,000.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Architecture Works for Us
&lt;/h2&gt;

&lt;p&gt;Usage patterns in our app are unpredictable. Some days are quiet; other days, everyone wants to know what the full moon means for them.&lt;/p&gt;

&lt;p&gt;This serverless architecture on AWS absorbs that volatility perfectly. We don't have to provision for peak capacity and pay for idle time. We just pay for the milliseconds of compute we actually use. It has allowed us to focus less on "keeping the lights on" and more on improving the actual product.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>python</category>
      <category>react</category>
    </item>
    <item>
      <title>Time-Travel Debugging for Python: A Complete Tutorial</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Fri, 23 Jan 2026 05:10:45 +0000</pubDate>
      <link>https://dev.to/ujwal240/-time-travel-debugging-for-python-a-complete-tutorial-1dip</link>
      <guid>https://dev.to/ujwal240/-time-travel-debugging-for-python-a-complete-tutorial-1dip</guid>
      <description>&lt;h1&gt;
  
  
  Time-Travel Debugging for Python: A Complete Tutorial
&lt;/h1&gt;

&lt;p&gt;Building web applications means dealing with external APIs, databases, and the inevitable production bugs. I'm going to show you how to capture production issues and debug them locally without ever hitting those external services again.&lt;/p&gt;

&lt;p&gt;This is a complete walkthrough using Timetracer with a Starlette application. By the end, you'll have a working example and understand how to apply this to your own projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you're new to Timetracer, you might want to check out &lt;a href="https://dev.to/ujwal240/i-got-tired-of-it-works-on-my-machine-so-i-built-a-time-travel-debugger-235h"&gt;my initial post&lt;/a&gt; about why I built this tool, or &lt;a href="https://dev.to/ujwal240/timetracer-v14-native-django-support-and-easiest-pytest-integration-5e4f"&gt;the v1.4 release post&lt;/a&gt; covering Django and pytest integration.&lt;/p&gt;

&lt;p&gt;This tutorial focuses specifically on Starlette integration and shows the complete debugging workflow with the new v1.6.0 dashboard features.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You know that moment when a bug happens in production? You spend hours trying to reproduce it locally. You're making API calls to third-party services, dealing with rate limits, stale data, and that nagging feeling you're not testing the exact scenario that failed.&lt;/p&gt;

&lt;p&gt;Traditional debugging flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bug reported in production&lt;/li&gt;
&lt;li&gt;Try to reproduce locally (often fails)&lt;/li&gt;
&lt;li&gt;Add logging and redeploy (slow)&lt;/li&gt;
&lt;li&gt;Hope you captured enough context (usually didn't)&lt;/li&gt;
&lt;li&gt;Repeat until fixed (hours or days)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's a better way. With Timetracer, you capture the entire request context in production and replay it locally. Think of it as a flight recorder for your web application.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up the Project
&lt;/h2&gt;

&lt;p&gt;Let's build a simple API that proxies GitHub user data. First, install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;starlette uvicorn httpx timetracer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file called &lt;code&gt;app.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.applications&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Starlette&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.routing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JSONResponse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome to the Starlette + Timetracer example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endpoints&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/user/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/repos/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_repos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;user_resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;user_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;repos_resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/repos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;repos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repos_resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_repos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public_repos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_repos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stargazers_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt; 
                      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stargazers_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Starlette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/user/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/repos/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_repos&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us three endpoints: a homepage, a user lookup, and a repo list. The last two hit the GitHub API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj6qgndsut4d8bjqahmbx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj6qgndsut4d8bjqahmbx.png" alt="Code Example" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Starlette application with three endpoints&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Integrating Timetracer
&lt;/h2&gt;

&lt;p&gt;Now add Timetracer. Import the integration and call &lt;code&gt;auto_setup()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;timetracer.integrations.starlette&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_setup&lt;/span&gt;

&lt;span class="c1"&gt;# ... your routes ...
&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Starlette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/user/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/repos/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_repos&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# This is the only line you need for Timetracer
&lt;/span&gt;&lt;span class="nf"&gt;auto_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One line of code. This adds middleware that captures every request and tracks all httpx calls to external APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recording Requests
&lt;/h2&gt;

&lt;p&gt;Start the server in record mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;record
uvicorn app:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your terminal should show Timetracer capturing requests:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4drra4t2z14zdvhgdc6v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4drra4t2z14zdvhgdc6v.png" alt="Terminal Record Mode" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal output showing Timetracer recording requests with timing information&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now let's make some requests and see what gets captured.&lt;/p&gt;
&lt;h3&gt;
  
  
  Request 1: Homepage
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome to the Starlette + Timetracer example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"endpoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/user/{username}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/repos/{username}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9zncr9odxstots4qmowp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9zncr9odxstots4qmowp.png" alt="Homepage Response" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Browser showing the homepage JSON response&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Terminal output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timetracer [OK] recorded GET /  id=cddb  status=200  total=9ms  deps=none
  cassette: cassettes/2026-01-23/GET__root__cddb6be9.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;deps=none&lt;/code&gt; because this endpoint doesn't make any external calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request 2: User Lookup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/user/octocat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"octocat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The Octocat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"public_repos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"followers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21594&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvlrvx0mad9xyki0e570l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvlrvx0mad9xyki0e570l.png" alt="User Response" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;GitHub user data returned through our API&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Terminal output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timetracer [OK] recorded GET /user/octocat  id=88d7  status=200  total=472ms  deps=http.client:1
  cassette: cassettes/2026-01-23/GET__user_octocat__88d76871.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time &lt;code&gt;deps=http.client:1&lt;/code&gt; shows one external HTTP call was tracked. The duration is 472ms instead of 9ms because we're waiting for GitHub's API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request 3: Repository List
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/repos/octocat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8y58gt92u8fld7x2l9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8y58gt92u8fld7x2l9u.png" alt="Repos Response" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Top repositories for the octocat user&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This endpoint makes two GitHub API calls: one for the user data and one for the repository list.&lt;/p&gt;
&lt;h3&gt;
  
  
  What Got Saved?
&lt;/h3&gt;

&lt;p&gt;Each cassette is a JSON file containing your request, response, and all external dependencies with timing information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xtfavn8n9i89igk0r8g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xtfavn8n9i89igk0r8g.png" alt="Cassette JSON Structure" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Cassette file showing the captured request, response, and external API calls&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The cassette includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request details (method, path, headers, body)&lt;/li&gt;
&lt;li&gt;Response details (status, headers, body, duration)&lt;/li&gt;
&lt;li&gt;All external dependencies (each GitHub API call with its own timing)&lt;/li&gt;
&lt;li&gt;Metadata about the session (framework, timestamp, etc.)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Using the Dashboard
&lt;/h2&gt;

&lt;p&gt;Now for the interactive part. Timetracer includes a web dashboard to browse and analyze your captured requests.&lt;/p&gt;

&lt;p&gt;Start the dashboard server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timetracer serve &lt;span class="nt"&gt;--dir&lt;/span&gt; cassettes &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; in your browser:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfvfdwjihspdthiaq37l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfvfdwjihspdthiaq37l.png" alt="Dashboard Overview" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dashboard showing all captured requests with statistics&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The dashboard shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total requests, success count, error count&lt;/li&gt;
&lt;li&gt;Every captured request with method, path, status, duration, and dependencies&lt;/li&gt;
&lt;li&gt;Search and filter capabilities&lt;/li&gt;
&lt;li&gt;View details or replay any request&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Viewing Request Details
&lt;/h3&gt;

&lt;p&gt;Click "View" on the &lt;code&gt;/repos/octocat&lt;/code&gt; request:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3e0yws9uu0lq9bk1whky.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3e0yws9uu0lq9bk1whky.png" alt="Dashboard Detail View" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Detailed view showing request, response, and external API dependencies&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The detail view shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request metadata: Path, method, timestamp&lt;/li&gt;
&lt;li&gt;Response: Status 200, duration 524ms&lt;/li&gt;
&lt;li&gt;Dependency Events: Both GitHub API calls with individual timings

&lt;ul&gt;
&lt;li&gt;GET &lt;a href="https://api.github.com/users/torvalds" rel="noopener noreferrer"&gt;https://api.github.com/users/torvalds&lt;/a&gt; (104ms)&lt;/li&gt;
&lt;li&gt;GET &lt;a href="https://api.github.com/users/torvalds/repos" rel="noopener noreferrer"&gt;https://api.github.com/users/torvalds/repos&lt;/a&gt; (95ms)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ready-to-use replay command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This view tells you exactly what happened during the request, including all external services that were called.&lt;/p&gt;
&lt;h3&gt;
  
  
  Filtering Requests
&lt;/h3&gt;

&lt;p&gt;Type "repos" in the search box:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8mkoq456od1mniictws.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8mkoq456od1mniictws.png" alt="Filtered Dashboard" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dashboard filtered to show only repository-related requests&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The dashboard now shows "Showing 5 of 19 cassettes" with only the matching requests visible.&lt;/p&gt;

&lt;p&gt;You can also filter by HTTP method or status code to focus on specific types of requests.&lt;/p&gt;
&lt;h3&gt;
  
  
  Inspecting the Raw Data
&lt;/h3&gt;

&lt;p&gt;For technical inspection, the dashboard includes a Raw JSON viewer:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fddjccf2x0t9fhbf50d7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fddjccf2x0t9fhbf50d7d.png" alt="Raw JSON Tab" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Raw JSON view showing the complete cassette structure&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This gives you direct access to the underlying cassette data, making it easy to verify exactly what state is being captured and will be replayed.&lt;/p&gt;


&lt;h2&gt;
  
  
  Debugging a Real Bug
&lt;/h2&gt;

&lt;p&gt;Now let's use Timetracer for what it's really good at: debugging production issues without touching production.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Bug Appears
&lt;/h3&gt;

&lt;p&gt;Imagine a user reports that requesting a non-existent GitHub user crashes the server with a 500 error. The problematic code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# Crashes on 404
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When someone requests a user that doesn't exist, GitHub returns 404, but our code assumes success and tries to parse the error response.&lt;/p&gt;

&lt;p&gt;Even though the app crashes, Timetracer still captures the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timetracer [ERROR] recorded GET /user/nonexistent-user-12345  id=bad1  status=500  total=156ms  deps=http.client:1
  cassette: cassettes/2026-01-23/GET__user_nonexistent-user-12345__bad1234.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Inspecting the Error
&lt;/h3&gt;

&lt;p&gt;In the dashboard, click "View" on the failed request:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhitycmxkis4k6lqk02fl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhitycmxkis4k6lqk02fl.png" alt="Error Detail View" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dashboard detail view showing a 404 error from GitHub API&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The detail view clearly shows that GitHub returned a 404, which propagated to our endpoint as a 500 error. You can see exactly what happened: the external API call failed, and our code didn't handle it properly.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;Looking at the dashboard detail view, you can see GitHub returned 404. Fix the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Check status code before parsing
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting the Replay Command
&lt;/h3&gt;

&lt;p&gt;The dashboard provides a ready-to-copy replay command:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffed3or0hsicw7py160ti.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffed3or0hsicw7py160ti.png" alt="Replay Command" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Ready-to-use replay command for testing the fix&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Just copy this command to test your fix with the exact scenario that failed in production.&lt;/p&gt;


&lt;h2&gt;
  
  
  Testing the Fix Without Network
&lt;/h2&gt;

&lt;p&gt;This is where Timetracer shows its real value. You can test the fix using the captured cassette without making any real API calls to GitHub.&lt;/p&gt;

&lt;p&gt;Stop the server and restart in replay mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;replay
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_CASSETTE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cassettes/2026-01-23/GET__user_nonexistent-user-12345__bad1234.json
uvicorn app:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff047gpky6ldickd6xsor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff047gpky6ldickd6xsor.png" alt="Replay Mode" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Server running in replay mode with mocked external API responses&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Make the same request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/user/nonexistent-user-12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terminal shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timetracer replay GET /user/nonexistent-user-12345  mocked=1  matched=OK  runtime=5ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User not found"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Status: 404&lt;/p&gt;

&lt;p&gt;The fix works. Notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No network call - the response came from the cassette&lt;/li&gt;
&lt;li&gt;Fast: 5ms instead of the original 156ms&lt;/li&gt;
&lt;li&gt;Exact scenario: Same 404 from GitHub that caused the original crash&lt;/li&gt;
&lt;li&gt;Offline: This works with no internet connection&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You just debugged and fixed a production bug without touching production or making a single external API call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Comparison
&lt;/h2&gt;

&lt;p&gt;Let's compare the timing differences:&lt;/p&gt;

&lt;h3&gt;
  
  
  Record Mode vs Replay Mode
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Record Duration&lt;/th&gt;
&lt;th&gt;Replay Duration&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9ms&lt;/td&gt;
&lt;td&gt;8ms&lt;/td&gt;
&lt;td&gt;1.1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/user/octocat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;472ms&lt;/td&gt;
&lt;td&gt;8ms&lt;/td&gt;
&lt;td&gt;59x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/repos/octocat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;524ms&lt;/td&gt;
&lt;td&gt;10ms&lt;/td&gt;
&lt;td&gt;52x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftj67pf1soolio3ra5u2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftj67pf1soolio3ra5u2w.png" alt="Performance Comparison" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Comparison of request durations in record mode versus replay mode&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For endpoints without external calls, the times are similar. But anything that touches an external API or database becomes dramatically faster in replay mode.&lt;/p&gt;

&lt;p&gt;This isn't just about speed. It's about reliability. Tests that depend on external APIs can be flaky due to network issues, rate limiting, or changing data. Replay mode eliminates all those problems.&lt;/p&gt;


&lt;h2&gt;
  
  
  When to Use This
&lt;/h2&gt;

&lt;p&gt;I've found Timetracer most useful in these scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Debugging Production Bugs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user reports an issue, capture the failing request in production. Download the cassette and debug locally with the exact same conditions. No need to reproduce complex scenarios or guess at what data caused the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Integration Testing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tests that hit real APIs are slow and unreliable. Record your test scenarios once, then replay them. Tests run in milliseconds instead of seconds, and they never fail due to network issues or rate limiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Offline Development&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Working on a plane or anywhere without internet? Load up cassettes with the API responses you need. Everything works normally without network access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Performance Analysis&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The dashboard shows you exactly how long each external dependency takes. If your endpoint is slow, you can see whether it's your code or a slow external API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Preventing Regressions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you fix a bug, keep the cassette and add it to your test suite. That specific scenario is now covered forever.&lt;/p&gt;


&lt;h2&gt;
  
  
  Framework Support
&lt;/h2&gt;

&lt;p&gt;Timetracer works with:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1kl89ercgs5vvdvb1trd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1kl89ercgs5vvdvb1trd.png" alt="Framework Support" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Supported web frameworks and external service integrations&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web Frameworks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI&lt;/li&gt;
&lt;li&gt;Starlette (new in v1.6.0)&lt;/li&gt;
&lt;li&gt;Flask&lt;/li&gt;
&lt;li&gt;Django&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;External Services:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;httpx and requests (HTTP clients)&lt;/li&gt;
&lt;li&gt;Motor and PyMongo (MongoDB)&lt;/li&gt;
&lt;li&gt;SQLAlchemy (SQL databases)&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration is similar across all frameworks. Usually just &lt;code&gt;auto_setup(app)&lt;/code&gt; or adding middleware.&lt;/p&gt;


&lt;h2&gt;
  
  
  Trade-offs to Consider
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Storage&lt;/strong&gt;: Each cassette is a JSON file. If you have many unique requests, you'll accumulate files. Clean up old cassettes periodically or store them in S3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sensitive data&lt;/strong&gt;: Cassettes contain your actual request and response data. Review what's being captured, especially in production. Timetracer has built-in redaction for common sensitive fields like passwords and tokens, but verify this for your use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cassette maintenance&lt;/strong&gt;: API responses change over time. You'll need to re-record cassettes when your external dependencies change their response format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not a replacement&lt;/strong&gt;: This isn't trying to replace your testing framework or mocking library. It's a debugging tool that captures production context and lets you work with it locally.&lt;/p&gt;


&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Install Timetracer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For Starlette&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;timetracer[starlette]

&lt;span class="c"&gt;# For FastAPI&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;timetracer[fastapi]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integrate into your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;timetracer.integrations.starlette&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_setup&lt;/span&gt;
&lt;span class="nf"&gt;auto_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run in record mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;record
uvicorn app:app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View the dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timetracer serve &lt;span class="nt"&gt;--dir&lt;/span&gt; cassettes &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test in replay mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;replay
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_CASSETTE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;path/to/cassette.json
uvicorn app:app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The workflow I showed here - capturing a failing production request, viewing it in the dashboard, fixing the bug, and testing the fix in replay mode - saves hours compared to traditional debugging.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Trying to reproduce the bug&lt;/li&gt;
&lt;li&gt;Adding logging&lt;/li&gt;
&lt;li&gt;Redeploying&lt;/li&gt;
&lt;li&gt;Hoping you captured enough context&lt;/li&gt;
&lt;li&gt;Repeating until fixed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download the cassette&lt;/li&gt;
&lt;li&gt;View it in the dashboard&lt;/li&gt;
&lt;li&gt;Fix the code&lt;/li&gt;
&lt;li&gt;Verify the fix in replay mode&lt;/li&gt;
&lt;li&gt;Deploy with confidence&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The complete example code is on GitHub at &lt;a href="https://github.com/usv240/timetracer" rel="noopener noreferrer"&gt;github.com/usv240/timetracer&lt;/a&gt;. All 174 tests are passing, and version 1.6.0 just added Starlette support and PyMongo integration.&lt;/p&gt;

&lt;p&gt;If you work with external APIs, spend time debugging production issues, or want faster integration tests, give it a try.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/usv240/timetracer" rel="noopener noreferrer"&gt;https://github.com/usv240/timetracer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;a href="https://pypi.org/project/timetracer" rel="noopener noreferrer"&gt;https://pypi.org/project/timetracer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://github.com/usv240/timetracer#readme" rel="noopener noreferrer"&gt;GitHub README&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Example code: See &lt;code&gt;examples/starlette_example/&lt;/code&gt; in the repo&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #python #starlette #fastapi #debugging #testing #devtools&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Timetracer v1.4: Native Django Support and Easiest pytest Integration</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Mon, 19 Jan 2026 18:32:01 +0000</pubDate>
      <link>https://dev.to/ujwal240/timetracer-v14-native-django-support-and-easiest-pytest-integration-5e4f</link>
      <guid>https://dev.to/ujwal240/timetracer-v14-native-django-support-and-easiest-pytest-integration-5e4f</guid>
      <description>&lt;p&gt;A few weeks ago, I introduced &lt;strong&gt;Timetracer&lt;/strong&gt; in my previous post: &lt;a href="https://dev.to/ujwal240/i-got-tired-of-it-works-on-my-machine-so-i-built-a-time-travel-debugger-235h"&gt;"I Got Tired of 'It Works on My Machine' So I Built a Time-Travel Debugger"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The tool allows you to record API interactions (including dependencies like databases and external APIs) and replay them locally to debug production issues.&lt;/p&gt;

&lt;p&gt;The initial version focused on FastAPI and Flask. However, the most frequent feedback I received was the need for &lt;strong&gt;Django support&lt;/strong&gt; and better integration with &lt;strong&gt;pytest suites&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I just released v1.4.0, which adds both.&lt;/p&gt;

&lt;p&gt;Here is why this matters and how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. No More "Enterprise Setup" Pain
&lt;/h3&gt;

&lt;p&gt;Django is often used for large, mature applications. These apps tend to have complex dependencies, specific database states, VPN-locked APIs, or legacy SOAP services that are a pain to run locally.&lt;/p&gt;

&lt;p&gt;With Timetracer's new middleware, you can record a session from a staging environment (or a colleague's machine that actually works) and replay it on your machine. You get the full app behavior without needing the full enterprise infrastructure running on &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Tests That Don't Lie
&lt;/h3&gt;

&lt;p&gt;We've all written tests with &lt;code&gt;unittest.mock&lt;/code&gt; where we guess what the API returns. Then the API changes, our mock stays the same, the test passes, but production breaks.&lt;/p&gt;

&lt;p&gt;The new pytest integration replaces brittle mocks with real, recorded interactions. Your tests run against actual data snapshots, making them far more reliable than manual mocks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Django Support
&lt;/h2&gt;

&lt;p&gt;Timetracer now includes middleware that works with Django 3.2 LTS and newer. It supports both standard synchronous views and the newer async views in Django 4.1+.&lt;/p&gt;

&lt;p&gt;The integration captures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incoming HTTP requests&lt;/li&gt;
&lt;li&gt;Outbound API calls (requests, httpx, aiohttp)&lt;/li&gt;
&lt;li&gt;Database queries (via SQLAlchemy for now, native ORM soon)&lt;/li&gt;
&lt;li&gt;Redis operations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Setting it up
&lt;/h3&gt;

&lt;p&gt;First, install the package with Django dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;timetracer[django,requests]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add the middleware in your &lt;code&gt;settings.py&lt;/code&gt;. It usually works best near the top of the list so it can capture everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;
&lt;span class="n"&gt;MIDDLEWARE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timetracer.integrations.django.TimeTracerMiddleware&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# ... other middleware
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Optional configuration
&lt;/span&gt;&lt;span class="n"&gt;TIMETRACER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MODE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;record&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 'record', 'replay', or 'off'
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CASSETTE_DIR&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./cassettes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your app makes external API calls, you should enable the plugins in your &lt;code&gt;AppConfig&lt;/code&gt; or &lt;code&gt;settings.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# myapp/apps.py or settings.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;timetracer.integrations.django&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_setup&lt;/span&gt;

&lt;span class="c1"&gt;# Enable recording for the requests library
&lt;/span&gt;&lt;span class="nf"&gt;auto_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Run your server: &lt;code&gt;python manage.py runserver&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Browse your site. Timetracer saves JSON "cassettes" for each request.&lt;/li&gt;
&lt;li&gt; To debug a specific request later, restart with &lt;code&gt;TIMETRACER_MODE=replay&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; The server will now mock all external calls using the recorded data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  pytest Integration
&lt;/h2&gt;

&lt;p&gt;Previously, using recorded cassettes in tests required manual setup. v1.4.0 adds a pytest plugin that automatically registers fixtures when you install the package.&lt;/p&gt;

&lt;p&gt;This allows you to write integration tests that don't need live external APIs but still exercise your full stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the Fixtures
&lt;/h3&gt;

&lt;p&gt;The plugin provides three main fixtures:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. timetracer_replay&lt;/strong&gt;&lt;br&gt;
Use this to run a test against a pre-recorded session. It guarantees the test runs exactly the same way every time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_user_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timetracer_replay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 'user_profile.json' contains the recorded interaction
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;timetracer_replay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cassettes/user_profile.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/users/123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;testuser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. timetracer_record&lt;/strong&gt;&lt;br&gt;
Use this when writing a new test case. It captures the interaction so you can save it as a cassette.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_new_feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timetracer_record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# This will execute real network calls and save the result
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;timetracer_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cassettes/new_feature.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/new-feature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. timetracer_auto&lt;/strong&gt;&lt;br&gt;
This is useful for TDD. If the cassette doesn't exist, it records. If it does exist, it replays.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_checkout_flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timetracer_auto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Records first time, replays subsequently
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;timetracer_auto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cassettes/checkout.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/checkout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cart_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;abc&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Async Support (aiohttp)
&lt;/h2&gt;

&lt;p&gt;We also added a plugin for &lt;code&gt;aiohttp&lt;/code&gt;. Building high-concurrency apps often requires async HTTP clients, and &lt;code&gt;aiohttp&lt;/code&gt; is a popular choice alongside &lt;code&gt;httpx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Timetracer now correctly intercepts and records &lt;code&gt;aiohttp.ClientSession&lt;/code&gt; requests, capturing the full async flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The goal remains effective local debugging. If you are using Django or pytest, I'd appreciate you trying this out and letting me know if it helps simplify your debugging workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repositories and Docs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  GitHub: &lt;a href="https://github.com/usv240/timetracer" rel="noopener noreferrer"&gt;https://github.com/usv240/timetracer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  PyPI: pip install timetracer&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>pytest</category>
      <category>testing</category>
    </item>
    <item>
      <title>I Got Tired of "It Works on My Machine" So I Built a Time-Travel Debugger</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Sun, 18 Jan 2026 06:17:35 +0000</pubDate>
      <link>https://dev.to/ujwal240/i-got-tired-of-it-works-on-my-machine-so-i-built-a-time-travel-debugger-235h</link>
      <guid>https://dev.to/ujwal240/i-got-tired-of-it-works-on-my-machine-so-i-built-a-time-travel-debugger-235h</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That was the last straw.&lt;/p&gt;

&lt;p&gt;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?"&lt;/p&gt;

&lt;p&gt;This post is about why I built it, how it works, and why you might want to use it too.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Debugging Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Here's a scenario you've probably lived through:&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;You add more logging. Redeploy. Wait for it to happen again. Check the new logs. Still can't reproduce it. Repeat.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;These problems have something in common: your code depends on things outside your control, and that makes debugging and development unpredictable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What If You Could Record Everything?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That HTTP call to Stripe? Recorded.&lt;/li&gt;
&lt;li&gt;That SELECT query to Postgres? Recorded.&lt;/li&gt;
&lt;li&gt;That Redis GET? Recorded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Actually Works
&lt;/h2&gt;

&lt;p&gt;Let's get into the technical stuff.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;If you're using FastAPI, adding Timetracer is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;timetracer.integrations.fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_setup&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auto_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The &lt;code&gt;auto_setup&lt;/code&gt; function adds middleware that handles recording and replaying.&lt;/p&gt;

&lt;p&gt;For Flask, it's similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;timetracer.integrations.flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_setup&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auto_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You control the behavior with environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Record mode - captures everything&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;record
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./cassettes

python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now make some requests to your app. Each request creates a cassette file.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's In a Cassette?
&lt;/h3&gt;

&lt;p&gt;Here's a simplified version of what gets saved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/checkout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"cart_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_456"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order_789"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;342&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http.client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.stripe.com/v1/charges"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"request_body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usd"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"response_body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ch_xxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"succeeded"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;187&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db.query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INSERT INTO orders (user_id, total) VALUES (?, ?)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user_456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;29.99&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is there. The incoming request, the outgoing response, and every dependency call in between.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replaying
&lt;/h3&gt;

&lt;p&gt;To replay, point Timetracer at the cassette:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;replay
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_CASSETTE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./cassettes/checkout_abc123.json

python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you hit &lt;code&gt;/checkout&lt;/code&gt; with the same request, Timetracer intercepts the Stripe call and the database query. Instead of making real calls, it returns the recorded responses.&lt;/p&gt;

&lt;p&gt;Your code runs exactly as it did in production, but locally, without needing Stripe credentials or a production database.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Plugins
&lt;/h2&gt;

&lt;p&gt;Recording HTTP calls is useful, but real applications do more than HTTP. That's why Timetracer has plugins for common dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP Clients
&lt;/h3&gt;

&lt;p&gt;We support both httpx (async and sync) and the requests library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;timetracer.plugins&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;enable_httpx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enable_requests&lt;/span&gt;

&lt;span class="nf"&gt;enable_httpx&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;# For httpx calls
&lt;/span&gt;&lt;span class="nf"&gt;enable_requests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# For requests library calls
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Databases
&lt;/h3&gt;

&lt;p&gt;SQLAlchemy queries get captured with their SQL and parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;timetracer.plugins&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;enable_sqlalchemy&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql://...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;enable_sqlalchemy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Caching
&lt;/h3&gt;

&lt;p&gt;Redis commands are recorded too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;timetracer.plugins&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;enable_redis&lt;/span&gt;

&lt;span class="nf"&gt;enable_redis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dashboard
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timetracer serve &lt;span class="nt"&gt;--dir&lt;/span&gt; ./cassettes &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts a local web server with a table of all your cassettes. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sort by time, method, status, or duration&lt;/li&gt;
&lt;li&gt;Filter by endpoint, HTTP method, or status code&lt;/li&gt;
&lt;li&gt;Click a row to see full details&lt;/li&gt;
&lt;li&gt;View the dependency timeline&lt;/li&gt;
&lt;li&gt;See the raw JSON&lt;/li&gt;
&lt;li&gt;Replay directly from the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Error responses show up with red highlighting, and slow requests (&amp;gt;1 second) get a warning indicator. If a request threw an exception, you see the full Python stack trace.&lt;/p&gt;

&lt;p&gt;It's nothing fancy, just a single HTML file with some JavaScript, but it makes browsing through cassettes way faster than grepping through JSON.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dealing With Sensitive Data
&lt;/h2&gt;

&lt;p&gt;Early on, I realized that cassettes could contain passwords, API keys, and other stuff I definitely didn't want in my Git repo.&lt;/p&gt;

&lt;p&gt;So Timetracer automatically redacts sensitive data:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headers that get stripped:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;Cookie&lt;/li&gt;
&lt;li&gt;Set-Cookie&lt;/li&gt;
&lt;li&gt;X-API-Key&lt;/li&gt;
&lt;li&gt;And about 20 others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Body fields that get masked:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;password, secret, token&lt;/li&gt;
&lt;li&gt;credit_card, cvv, ssn&lt;/li&gt;
&lt;li&gt;api_key, private_key&lt;/li&gt;
&lt;li&gt;And about 100 more patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;A value like &lt;code&gt;user@example.com&lt;/code&gt; becomes &lt;code&gt;[REDACTED:EMAIL]&lt;/code&gt;. A credit card number becomes &lt;code&gt;[REDACTED:CREDIT_CARD]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The goal is that cassettes should be safe to commit without thinking too hard about what's in them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Debugging Production Bugs
&lt;/h3&gt;

&lt;p&gt;This is the original reason I built it. Something breaks in production, you grab the cassette, replay locally, and debug with full context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regression Testing
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Offline Development
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demos
&lt;/h3&gt;

&lt;p&gt;Recording demo scenarios means your demos work even when external services are flaky. No more "let me refresh that" during a presentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Analysis
&lt;/h3&gt;

&lt;p&gt;The timeline command generates a waterfall chart showing how long each dependency took:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timetracer timeline ./cassette.json &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What About VCR.py?
&lt;/h2&gt;

&lt;p&gt;VCR.py is great. I've used it for years. But it only records HTTP calls.&lt;/p&gt;

&lt;p&gt;In a typical web application, a single API request might:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query the database for user info&lt;/li&gt;
&lt;li&gt;Call an external API for pricing&lt;/li&gt;
&lt;li&gt;Check a cache for recent activity&lt;/li&gt;
&lt;li&gt;Write to another database&lt;/li&gt;
&lt;li&gt;Make another API call&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;VCR.py captures step 2 and 5. The rest? You're on your own.&lt;/p&gt;

&lt;p&gt;Timetracer captures all of it. One cassette has everything.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Install with pip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;timetracer[fastapi,httpx]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for Flask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;timetracer[flask,requests]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the middleware, set the environment variables, and make some requests. Your first cassettes will be in the directory you specified.&lt;/p&gt;

&lt;p&gt;The documentation has more details on configuration, the CLI tools, S3 storage, and all the other features I didn't cover here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/usv240/timetracer" rel="noopener noreferrer"&gt;https://github.com/usv240/timetracer&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I'm using Timetracer on my own projects, and I keep finding things to improve. Some ideas on the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better support for async database drivers&lt;/li&gt;
&lt;li&gt;GraphQL request parsing&lt;/li&gt;
&lt;li&gt;Request diffing between cassettes&lt;/li&gt;
&lt;li&gt;Maybe a VS Code extension?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Thanks for reading. I hope Timetracer saves you some debugging time.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/usv240/timetracer" rel="noopener noreferrer"&gt;https://github.com/usv240/timetracer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;a href="https://pypi.org/project/timetracer/" rel="noopener noreferrer"&gt;https://pypi.org/project/timetracer/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>flask</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
