HTTP Instrumentation
Trace LLM calls per HTTP request in Flask, FastAPI, Django, and Starlette, plus the bundler setup Next.js needs.
HTTP Instrumentation
HTTP instrumentation automatically creates a trace for each incoming HTTP request and nests all LLM calls made within that request under it. Session IDs are extracted from standard HTTP headers automatically.
Per-request HTTP instrumentation — one span per incoming request — is currently available for the Python web frameworks below. In Java, Spring Boot and Servlet HTTP tracing is handled automatically by the auto-tracing layer. In JavaScript/TypeScript, LLM calls are auto-traced per request automatically — but Next.js needs a one-time bundler configuration, covered in Next.js below.
Trace scope in HTTP apps
When the SDK is initialized in an HTTP server, the HTTP request is the boundary of a trace. Each incoming request opens its own trace, and every LLM call, generation, span, and event produced while handling that request is automatically nested under it — when the response is sent, the trace is complete.
You don't create traces yourself in an HTTP app: the request (route) defines the unit of work. One request maps to one trace in the dashboard, and concurrent requests each get their own isolated trace.
That request scope is also the boundary for trace-level attributes: anywhere inside the request handler you can call Observe.setAttribute / Observe.set_attribute (session ID, user ID, or custom keys) and it applies to that request's trace. Outside a request there is no active trace, so those calls are ignored — see setting trace attributes.
How It Works
When Observe.init() runs, the SDK:
- Patches the web framework's request lifecycle with OpenTelemetry hooks.
- Creates an HTTP span for each incoming request.
- Extracts session and request IDs from headers (
X-Session-ID,X-Request-ID, etc.). - Nests any LLM calls made during the request under the HTTP span.
Flask
pip install https://static.testops.ai/sdk/python/browserstack_ai_sdk-latest.tar.gzOption 1 — Global instrumentation via Observe.init() (recommended):
import os
from browserstack_ai_sdk import Observe
from flask import Flask, request, jsonify
import openai
Observe.init(
public_key=os.environ["AISDK_PUBLIC_KEY"],
secret_key=os.environ["AISDK_SECRET_KEY"],
)
app = Flask(__name__)
openai_client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])
@app.route("/chat", methods=["POST"])
def chat():
user_message = request.json.get("message", "")
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
)
return jsonify({"reply": response.choices[0].message.content})
if __name__ == "__main__":
app.run(debug=True)Option 2 — Instrument a specific Flask app instance:
from browserstack_ai_sdk.http_instrumentation import instrument_flask_app
instrument_flask_app(app)What is captured per request: HTTP method, path, status code, session ID, all nested LLM calls.
FastAPI
pip install https://static.testops.ai/sdk/python/browserstack_ai_sdk-latest.tar.gzCall Observe.init() before creating the FastAPI() instance:
import os
from browserstack_ai_sdk import Observe
from fastapi import FastAPI
from pydantic import BaseModel
import openai
Observe.init(
public_key=os.environ["AISDK_PUBLIC_KEY"],
secret_key=os.environ["AISDK_SECRET_KEY"],
)
app = FastAPI()
openai_client = openai.AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
class ChatRequest(BaseModel):
message: str
@app.post("/chat")
async def chat(body: ChatRequest):
response = await openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": body.message}],
)
return {"reply": response.choices[0].message.content}To instrument an existing app instance manually:
from browserstack_ai_sdk.http_instrumentation import instrument_fastapi_app
instrument_fastapi_app(app)What is captured per request: HTTP method, path, status code, all nested LLM calls. send/receive ASGI sub-spans are excluded to reduce noise.
Django
pip install https://static.testops.ai/sdk/python/browserstack_ai_sdk-latest.tar.gzInitialize the SDK in your wsgi.py or asgi.py before the app starts:
# myproject/wsgi.py (or asgi.py)
import os
from browserstack_ai_sdk import Observe
Observe.init(
public_key=os.environ["AISDK_PUBLIC_KEY"],
secret_key=os.environ["AISDK_SECRET_KEY"],
)
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
application = get_wsgi_application()To instrument Django programmatically:
from browserstack_ai_sdk.http_instrumentation import instrument_django_app
instrument_django_app()Then use LLM clients normally inside your views:
# myapp/views.py
import openai
from django.http import JsonResponse
openai_client = openai.OpenAI()
def chat_view(request):
message = request.GET.get("message", "")
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": message}],
)
return JsonResponse({"reply": response.choices[0].message.content})Starlette
pip install https://static.testops.ai/sdk/python/browserstack_ai_sdk-latest.tar.gzimport os
from browserstack_ai_sdk import Observe
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
import openai
Observe.init(
public_key=os.environ["AISDK_PUBLIC_KEY"],
secret_key=os.environ["AISDK_SECRET_KEY"],
)
openai_client = openai.AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
async def chat(request):
body = await request.json()
response = await openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": body.get("message", "")}],
)
return JSONResponse({"reply": response.choices[0].message.content})
app = Starlette(routes=[Route("/chat", chat, methods=["POST"])])To instrument an existing Starlette app instance manually:
from browserstack_ai_sdk.http_instrumentation import instrument_starlette_app
instrument_starlette_app(app)Session ID Extraction
The SDK automatically reads session IDs from these request headers (in order of priority):
| Header | Description |
|---|---|
X-Session-ID | Primary session identifier |
Session-ID | Alternate session header |
X-Request-ID | Used as session ID if no session header is found |
X-Correlation-ID | Used as session ID fallback |
If no session ID header is present, a UUID is generated automatically for the request.
Next.js
Next.js inlines packages into the server bundle by default, which silently disables auto-tracing. Add the SDK (and the provider packages you import) to serverExternalPackages, then initialize the SDK in instrumentation.ts.
npm install @browserstack/ai-sdkStep 1 — Externalize the packages in next.config.ts:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
serverExternalPackages: ['@browserstack/ai-sdk', 'openai', '@anthropic-ai/sdk'],
};
export default nextConfig;Step 2 — Initialize the SDK in instrumentation.ts, which Next.js runs once at server boot:
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { Observe } = await import('@browserstack/ai-sdk');
await Observe.init({
publicKey: process.env.AISDK_PUBLIC_KEY,
secretKey: process.env.AISDK_SECRET_KEY,
});
}
}List every auto-instrumented package you import in serverExternalPackages — missing one drops that integration's spans. The top-level key requires Next.js 15+ (on 13–14 use experimental.serverComponentsExternalPackages), and LLM routes must stay on the Node.js runtime, not Edge.