# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

Flowlines SDK for Python — an observability SDK that instruments LLM provider APIs using OpenTelemetry. It captures requests, responses, timing, and errors, filtering to only LLM-related spans and exporting them via OTLP/HTTP to the Flowlines backend.

## Commands

```bash
# Install dependencies
uv sync

# Run unit tests
uv run pytest tests/

# Run a single test
uv run pytest tests/test_init.py::TestClassName::test_method_name

# Install integration test dependencies
uv sync --group integration

# Run integration tests (requires API key env vars: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.)
uv run pytest integration_tests/

# Lint
uv run ruff check flowlines/ tests/ integration_tests/

# Format
uv run ruff format flowlines/ tests/ integration_tests/

# Type checking
uv run mypy flowlines/
uv run pyright
```

## Architecture

All source is in `flowlines/`. The public API exports `Flowlines` and `FlowlinesExporter` from `__init__.py`.

### Core modules

- **`_init.py`** — `Flowlines` singleton with three initialization modes:
  - **Mode A** (default): Creates its own `TracerProvider`, registers all available instrumentors automatically
  - **Mode B1** (`has_external_otel=True`): User manages their own `TracerProvider`; Flowlines provides `create_span_processor()` and `get_instrumentors()` for manual integration
  - **Mode B2** (`has_traceloop=True`): Traceloop already initialized; Flowlines adds its span processor to the existing provider
- **`_context.py`** — `FlowlinesSpanProcessor` (decorator over `BatchSpanProcessor`) that injects `user_id`/`session_id`/`agent_id` from `ContextVar` into spans at start time. Context is set via `flowlines.context()` context manager or `Flowlines.set_context()`/`clear_context()` imperative API.
- **`_exporter.py`** — `FlowlinesExporter` wraps `OTLPSpanExporter`, filtering exported spans to only those with `gen_ai.*` or `ai.*` attributes (via `_filter.py`)
- **`_filter.py`** — `is_llm_span()` function that identifies LLM-related spans by attribute prefixes
- **`_patches.py`** — Monkey-patches for upstream instrumentor bugs (e.g., OpenAI tools serialization, missing Gemini span attributes). Applied automatically on `init()`, version-gated so they no-op once upstream fixes land

### Key patterns

- **Singleton**: `Flowlines` enforces single instance; `_reset()` classmethod exists for test teardown
- **Decorator pattern**: Both `FlowlinesSpanProcessor` and `FlowlinesExporter` wrap inner OpenTelemetry objects
- **Dynamic instrumentor discovery**: Registry of `(library_name, module_path, class_name)` tuples; each instrumentor is loaded only if its library is importable

## Code Conventions

- Private modules prefixed with underscore (`_init.py`, `_context.py`, etc.)
- `from __future__ import annotations` in all modules
- Strict type checking enabled (mypy strict + pyright strict)
- All function signatures have type annotations including return types
- Google-style docstrings with Args/Returns/Raises sections
- Ruff lint rules: `E`, `F`, `W`, `I`, `UP`, `B`, `SIM`, `TCH`

## Test Conventions

- Unit tests in `tests/` use `unittest.mock.patch` and `MagicMock` extensively
- `autouse` fixtures reset the `Flowlines` singleton before each test
- Tests organized in classes by feature/mode
- All test functions have type annotations (`-> None`) and docstrings
- Integration tests in `integration_tests/` use an in-process OTLP capture server
- `pytest-asyncio` with `asyncio_mode = "auto"`
