diff --git a/drift/instrumentation/django/middleware.py b/drift/instrumentation/django/middleware.py index d052429..cc62f80 100644 --- a/drift/instrumentation/django/middleware.py +++ b/drift/instrumentation/django/middleware.py @@ -105,6 +105,17 @@ def _handle_replay_request(self, request: HttpRequest, sdk) -> HttpResponse: logger.warning("[DJANGO_MIDDLEWARE] No replay_trace_id found in headers, proceeding without span") return self.get_response(request) + # Start time travel as early as possible (inbound request start) to ensure deterministic + # behavior for time-dependent code paths, especially cache key generation (e.g. django-cachalot). + root_timestamp = headers_lower.get("http_x_td_root_timestamp") + if root_timestamp: + try: + from drift.instrumentation.datetime.instrumentation import start_time_travel + + start_time_travel(root_timestamp, trace_id=replay_trace_id) + except Exception as e: + logger.debug(f"[DJANGO_MIDDLEWARE] Failed to start time travel from x-td-root-timestamp: {e}") + # Set replay trace context replay_token = replay_trace_id_context.set(replay_trace_id) diff --git a/drift/instrumentation/fastapi/instrumentation.py b/drift/instrumentation/fastapi/instrumentation.py index b8dcef2..2d46b90 100644 --- a/drift/instrumentation/fastapi/instrumentation.py +++ b/drift/instrumentation/fastapi/instrumentation.py @@ -131,6 +131,17 @@ async def _handle_replay_request( logger.debug(f"[FastAPIInstrumentation] Setting replay trace ID: {replay_trace_id}") + # Start time travel at inbound request start when provided by CLI. + # This prevents time drift before the first outbound span is matched (critical for cache keys). + root_timestamp = headers_lower.get("x-td-root-timestamp") + if root_timestamp: + try: + from drift.instrumentation.datetime.instrumentation import start_time_travel + + start_time_travel(root_timestamp, trace_id=replay_trace_id) + except Exception as e: + logger.debug(f"[FastAPIInstrumentation] Failed to start time travel from x-td-root-timestamp: {e}") + # Remove accept-encoding header to prevent compression during replay # (responses are stored decompressed, compression would double-compress) if "accept-encoding" in headers_lower: diff --git a/drift/instrumentation/wsgi/handler.py b/drift/instrumentation/wsgi/handler.py index 23a682a..c30be5b 100644 --- a/drift/instrumentation/wsgi/handler.py +++ b/drift/instrumentation/wsgi/handler.py @@ -142,6 +142,17 @@ def _handle_replay_request( # No trace context in REPLAY mode; proceed without span return original_wsgi_app(app, environ, start_response) + # Start time travel at inbound request start when provided by CLI. + # This prevents time drift before the first outbound span is matched (critical for cache keys). + root_timestamp = headers_lower.get("x-td-root-timestamp") + if root_timestamp: + try: + from drift.instrumentation.datetime.instrumentation import start_time_travel + + start_time_travel(root_timestamp, trace_id=replay_trace_id) + except Exception as e: + logger.debug(f"[WSGI] Failed to start time travel from x-td-root-timestamp: {e}") + # Set replay trace context replay_token = replay_trace_id_context.set(replay_trace_id)