# Python SDK API

> Classes, methods, parameters, return values, configuration, and integrations for the Anectico Python SDK.

Canonical page: https://anectico.com/docs/reference/python-sdk/


This is the application-developer API for `anectico` 0.1.x. The SDK supports Python 3.10+, synchronous
and asynchronous frameworks, OpenTelemetry signals, identity, diagnostic events, feature flags, and
LLM wrappers.

Use `ingest:write` for traces, metrics, logs, and errors. Add `analytics:write` for identity, groups,
events, and flag exposure events; add `flags:read` for flag decisions.

## Create and run `AnecticoClient`

```python
import os
import anectico

client = anectico.AnecticoClient(
    api_key=os.environ['ANECTICO_API_KEY'],
    service_name='orders-api',
)
client.start()
try:
    run_application()
finally:
    shutdown = client.stop()
    if shutdown.success is False:
        report_telemetry_delivery_failure(shutdown.errors)
```

| API | Parameters | Returns | Behavior |
| --- | --- | --- | --- |
| `AnecticoClient(...)` | core options below or `config=AnecticoConfig(...)` | `AnecticoClient` | Builds an unstarted client. A supplied config takes precedence over every other constructor argument. |
| `start()` | — | same `AnecticoClient` | Validates configuration. Managed mode starts owned transports and the optional standard-library logging bridge; existing mode only enables helpers against application globals. Repeated calls are safe. |
| `stop()` | — | `ShutdownResult` | Managed mode removes the log bridge and concurrently flushes/stops owned providers. Existing mode never touches application providers and returns an unattempted result. Repeated calls are safe. |
| `is_running` | property | `bool` | Whether the client currently accepts telemetry. |
| context manager | `with AnecticoClient(...) as client` | client | Starts on entry, captures an exception leaving the block, then stops. |
| `flush(timeout_ms=None)` | optional milliseconds | `bool` | Flushes client-owned managed providers and the pending identity-event queue. `True` only when nothing was left unaccepted. Returns `False` in existing mode; the application must flush its providers. |
| `config` | property | `AnecticoConfig` | Active configuration. Contains the API key; do not log or serialize it. |
| `stats` | property | `dict[str,int]` | Counters for spans, metrics, logs, captured errors, and `identity_events_dropped`. |

`flush()` combines two halves and never turns an unknown into a success. The identity-event queue is
owned by the SDK end to end, so it reports real acceptance: a permanently rejected `identify` batch
makes `flush()` return `False` and increments `stats['identity_events_dropped']`, and a rejected
final batch makes `stop()` report a failed `ShutdownResult` rather than a silent success. The
telemetry half reports its provider flush outcome — buffered signals drained and exported — which is
not a per-request acceptance, and is passed through rather than reinterpreted.

Keep one client per process. Framework middleware supplies request-scoped identity and spans; do not
construct a client per request.

`ShutdownResult` has `attempted`, `flush_succeeded`, `shutdown_succeeded`,
`success`, and a tuple of sanitized `errors`. `success` is `True` only when an
attempted shutdown fully delivered and stopped every enabled provider. It is
`False` for a provider `False` return, exception, or timeout, and `None` when
the client was already stopped. Boolean conversion is true only for full
success. A Python context manager cannot return its exit outcome; use explicit
`start()`/`stop()` when the process must record delivery evidence.

## Constructor and `AnecticoConfig`

The constructor directly accepts the most common fields. `AnecticoConfig` exposes the complete set.

| Field | Type | Default/environment | Purpose |
| --- | --- | --- | --- |
| `api_key` | `str` | `ANECTICO_API_KEY`; required | Project-scoped Anectico API key. |
| `service_name` | `str` | `OTEL_SERVICE_NAME`; required | Logical service name. |
| `service_version` | `str` | `OTEL_SERVICE_VERSION`; `0.0.0` | Deployed version. |
| `environment` | `str` | `ANECTICO_ENVIRONMENT`; `development` | Deployment environment. |
| `endpoint` | `str` | `ANECTICO_ENDPOINT`; `https://api.anectico.com` | HTTP requires an absolute `http(s)` URL. gRPC accepts an `http(s)` authority URL without a path or `host:port`. |
| `open_telemetry_mode` | `managed \| existing` | `ANECTICO_OTEL_MODE`; `managed` | Provider ownership. Existing mode uses application-registered globals and does not configure or own their lifecycle. |
| `protocol` | `http \| grpc` | `OTEL_EXPORTER_OTLP_PROTOCOL`; `http` | OTLP transport. |
| `insecure` | `bool` | `OTEL_EXPORTER_OTLP_INSECURE`; `False` | Disable TLS; local development only. |
| `enable_traces`, `enable_metrics`, `enable_logs` | `bool` | corresponding `ANECTICO_ENABLE_*`; `True` | Enable each signal pipeline. |
| `trace_sample_rate` | `float` | `OTEL_TRACES_SAMPLER_ARG`; `0.1` | Trace sampling in the inclusive range 0–1. |
| `error_sample_rate` | `float` | `ANECTICO_ERROR_SAMPLE_RATE`; `1` | Non-fatal captured-error sampling. |
| `batch_timeout_ms` | `int` | `OTEL_BSP_SCHEDULE_DELAY`; `5000` | Maximum batching delay. |
| `batch_size` | `int` | `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`; `512` | Maximum export batch size. |
| `max_queue_size` | `int` | `OTEL_BSP_MAX_QUEUE_SIZE`; `2048` | Queue capacity; must be at least `batch_size`. |
| `export_timeout_ms` | `int` | `OTEL_BSP_EXPORT_TIMEOUT`; `30000` | Per-export deadline; also the total HTTP same-batch retry window. |
| `shutdown_timeout_ms` | `int` | `OTEL_BSP_SHUTDOWN_TIMEOUT`; `5000` | One aggregate graceful flush/shutdown deadline across enabled providers. |
| `metric_export_interval_ms` | `int` | `OTEL_METRIC_EXPORT_INTERVAL`; `60000` | Metric push interval. |
| `bridge_stdlib_logging` | `bool` | `ANECTICO_BRIDGE_STDLIB_LOGGING`; `True` | Export normal `logging` records through this client. |
| `log_level` | `str` | `ANECTICO_LOG_LEVEL`; `info` | Minimum bridged log level. |
| `log_redaction_fields` | `tuple[str,...]` | `ANECTICO_LOG_REDACTION_FIELDS`; empty | Additional comma-separated structured-log field names to redact. Extends, never replaces, built-in credential and financial-account protection. |
| `attach_stack_trace` | `bool` | `ANECTICO_ATTACH_STACK_TRACE`; `True` | Attach exception frames. |
| `max_stack_trace_frames` | `int` | `ANECTICO_MAX_STACK_TRACE_FRAMES`; `50` | Maximum exception frames. |
| `instrument_http_clients` | `bool` | `ANECTICO_INSTRUMENT_HTTP_CLIENTS`; `False` | Globally instrument `requests`/`httpx`. Opt in only for trusted traffic. |
| `propagate_trace_header_urls` | `tuple[str,...]` | `ANECTICO_PROPAGATE_TRACE_HEADER_URLS`; empty | Absolute trusted prefixes allowed to receive trace/identity headers. |
| `trust_incoming_identity` | `bool` | `ANECTICO_TRUST_INCOMING_IDENTITY`; `False` | Adopt inbound Anectico identity baggage. Enable only behind a sanitizing authenticated gateway. |
| `resource_attributes` | `dict[str,str]` | `OTEL_RESOURCE_ATTRIBUTES`; empty | Extra resource attributes. |
| `headers` | `dict[str,str]` | `OTEL_EXPORTER_OTLP_HEADERS`; empty | Extra export headers. |
| `debug` | `bool` | `ANECTICO_DEBUG`; `False` | SDK diagnostic logging. |

`AnecticoConfig.from_env(**overrides)` resolves environment variables and applies explicit overrides.
Unknown override names raise `ValueError`. Queue sizes and stack-frame limits must be integers;
timeouts must be finite and positive. An explicitly empty service name is invalid even when the
environment contains a name. Authentication headers override custom headers case-insensitively.
`validate()` raises on missing/invalid configuration. `get_endpoint_for_signal(signal)`,
`get_headers()`, and `get_log_level()` support custom integrations.

`open_telemetry_mode` accepts exactly `managed` or `existing` (case-sensitive).
In existing mode the application must register providers before
`AnecticoClient.start()`. Anectico creates no provider, exporter, export processor,
metric reader, propagator, HTTP instrumentation, or standard-library logging
bridge. Managed resource, sampling, endpoint, batching, exporter, propagation,
and logging settings do not reconfigure application providers; signal enable
flags still gate the corresponding Anectico helpers.

The enabled signal endpoints are resolved before validation, so
`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,
`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, and
`OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` can replace the base endpoint for their
signals. Endpoint errors never echo the configured URL, which may contain
sensitive deployment information.

HTTP exporters retain the same serialized batch for connection loss and
retryable `408`/`5xx` responses throughout the configured
`export_timeout_ms` window. Jittered exponential backoff is bounded by the
remaining deadline, interrupted by shutdown, and exhausted once without a
nested retry loop. Certificate verification, TLS configuration, and
client-certificate failures are classified as permanent, attempted once, and
logged with a sanitized diagnostic. Certificate verification is never disabled
implicitly.

## Traces, metrics, logs, and AI operations

| Method/property | Parameters | Returns | Behavior |
| --- | --- | --- | --- |
| `tracer` | property | OTel `Tracer` | Underlying tracer for advanced use. |
| `meter` | property | OTel `Meter` | Underlying meter for advanced use. |
| `start_span(name, **kwargs)` | name; arguments forwarded to `Tracer.start_as_current_span` | context manager yielding OTel `Span` | Makes the span current for the `with` block, so nested spans, captured errors, and correlated logs inherit its trace context; yields a non-recording span before startup. For manual lifetime control, use `client.tracer.start_span(...)`. |
| `record_metric(name, value, labels=None)` | name; float; string labels | `None` | Creates a gauge and records one value. |
| `log_event(level, message, attrs=None)` | `debug/info/warn/warning/error/fatal`; body; attributes | `None` | Emits a structured OTLP log; no-op when logs are disabled. |
| `record_llm_call(model, **options)` | required model; options below | `None` | Emits one completed `gen_ai` client span. |
| `record_tool_call(name, **options)` | required tool name; options below | `None` | Emits one completed internal tool span. |
| `start_agent_run(agent, **options)` | agent; optional IDs | context manager yielding `AgentRun` | Makes the agent span current so nested calls become children. Call `run.end(status, reason_code)`; normal exit defaults to completed and exceptions safely record failed/exception. |

`record_llm_call` accepts `response_model`, `provider`, `operation`, token counts, `cost_usd`,
`finish_reason`, `is_error`, `start_time`, and `end_time`. `messages` and `output` opt into sensitive
content. `record_tool_call` accepts `tool_type`, `call_id`, `conversation_id`, `agent_name`,
`is_error`, `arguments`, and `result`; arguments/results are also opt-in content.
Agent-run status is `completed`, `failed`, `timed_out`, `cancelled`, or `max_steps`. Reason codes are
lowercase `[a-z][a-z0-9_]{0,63}` values, not exception/provider text. The first valid `end` call wins;
later calls are no-ops. Calling `run.span.end()` directly bypasses the terminal contract and leaves
Anectico to use its legacy inferred status.

`start_agent_run(..., agent_version="checkout-agent@2026.08.24")` optionally declares the release
that produced the run. It is recorded as the standard `gen_ai.agent.version` span attribute. Leave
it unset when the release is unknown; Anectico reports that as undeclared rather than treating it as
an error or silently filtering the run away.

### Context assembly events

The run handle exposes `record_context_assembly(value)`, `record_context_compaction(value)`, and
`record_context_cache(value)`. Call them while a recording model span is current and carries
`gen_ai.operation.name = chat | text_completion | generate_content | embeddings | fetch_response`.
The SDK raises `ValueError` for the run span, a tool span, an ended span, or no active span. It
validates every field before adding the one event, so a rejected value records nothing.

```python
with client.start_agent_run("support-planner") as run:
    with client.tracer.start_as_current_span(
        "chat gpt-4o",
        kind=SpanKind.CLIENT,
        attributes={"gen_ai.operation.name": "chat"},
    ):
        run.record_context_assembly(
            ContextAssembly(
                budget_tokens=8192,
                reserved_output_tokens=1024,
                assembled_tokens=6144,
                overflow="none",
                assembly_sha256=assembly_sha256,
                sources=[ContextSource(
                    kind="memory", id="memory:item-7", sensitivity="confidential",
                    tokens=512, position=3, visibility="included",
                )],
            )
        )
```

See [Context events](/docs/reference/agent-attributes#context-events) for exact bounds. Never send compaction
ratio; the server derives it. OpenAI and Anthropic wrappers add cache hit/miss events when cache
fields are present. A reported zero is a miss, while an absent field produces no event.

Managed mode bridges standard-library `logging` by default. Those records
inherit active OTel trace context; prefer ordinary `logger.info(...)` inside
application code. Existing mode installs no bridge: ordinary logs use the
application's logging setup, while `log_event()` uses the registered global
Logger Provider and safely no-ops without a real provider. When a framework
logs the same exception after `capture_error`, the managed bridge links that log
with the canonical `error.id`: the log remains searchable without creating a
second Issue. Independent error logs still create Issues.

Application values supplied through `logging(..., extra={...})` are exported as structured
attributes. Safe top-level strings, booleans, integers, floats, and homogeneous primitive sequences
retain their OpenTelemetry types. Nested mappings or mixed/nested sequences are recursively
sanitized, then stored as bounded canonical JSON because OpenTelemetry attributes do not support
nested objects. The bridge excludes Python's reserved `LogRecord` fields and private keys.

Before export, field names such as password, authorization/Bearer, token/API key, cookie, secret,
bank/routing/account number, IBAN, card, and PIN are replaced with `[REDACTED]`, including inside
nested mappings and sequences. Credential forms in the message and exception stack text are also
redacted. Add domain-specific names without weakening the built-ins:

```python
client = AnecticoClient(
    api_key=os.environ["ANECTICO_API_KEY"],
    service_name="payroll",
    log_redaction_fields=["payroll_reference"],
)
```

The equivalent environment value is
`ANECTICO_LOG_REDACTION_FIELDS=payroll_reference,employee_private_code`.

## Errors, messages, users, and breadcrumbs

Breadcrumbs snapshot nested JSON data when added. If optional data cannot be serialized, the
breadcrumb is retained without that data so error reporting can still include the trail.

| Method | Parameters | Returns | Behavior |
| --- | --- | --- | --- |
| `capture_error(error, *, user=None, tags=None, extra=None, fingerprint=None, level='error')` | exception and optional context | `str` | Captures the exception and returns its ID; `''` before startup or when sampled out. `fatal` bypasses sampling. |
| `capture_message(message, level='error', *, user=None, tags=None, extra=None)` | message, severity, optional context | `str` | Captures a message and returns its ID. |
| `add_breadcrumb(category, message, *, level='info', data=None)` | breadcrumb fields | `None` | Adds to the 100-entry trail attached atomically to the next error. |
| `set_user(user)` | `User` | `None` | Sets global error user context. |
| `get_user()` | — | `User \| None` | Returns request-local user first, then global user. |
| `clear_user()` | — | `None` | Clears global error user context. |

Capture `tags` keys are encoded as indexed `anectico.tag.*` telemetry attributes automatically.
Capture severity is also indexed as `error.level` for Issue filtering and context.

`User` fields are `id`, `email`, `username`, `ip_address`, `segment`, and `data`. Use
`set_context_user(user)`/`get_context_user()` for an async request-local error user. The cross-signal
person identity remains `distinct_id`, managed separately below.

## Identity and groups

| API | Parameters | Returns | Behavior |
| --- | --- | --- | --- |
| `client.identify(distinct_id, properties=None)` | stable ID; person properties | `None` | Switches shared identity and performs best-effort canonical-person sync. Raises `ValueError` for an empty ID. |
| `client.sync_person(distinct_id, properties=None)` | stable ID; person properties | `None` | Performs best-effort canonical-person sync without an anonymous alias or any process-wide identity mutation. Use at trusted server authentication/profile boundaries. |
| `client.group(group_type, group_key, properties=None)` | type/key; group properties | `None` | Associates the person with an account and sends a membership assertion. Empty type/key is ignored. |
| `client.reset()` | — | `None` | Logout: creates a new anonymous identity and clears groups, global error-user context, and uncaptured breadcrumbs. Already-captured telemetry remains queued with its original attribution. |
| `shared_identity()` | — | `IdentityManager` | Returns the process-wide identity holder shared by signals and analytics. |
| `set_context_distinct_id(id)` | stable ID | `DistinctIDScope` | Sets async-safe request identity; reset with the returned token. |
| `reset_context_distinct_id(token)` | scope token | `None` | Restores the prior request identity. |
| `resolve_distinct_id_for(context=None, identity=None)` | optional OTel context/manager | `str` | Resolves request scope, baggage, then shared identity. |
| `adopt_distinct_id_from_baggage(context=None)` | OTel context | `DistinctIDScope \| None` | Promotes trusted baggage into request scope. Never call directly on untrusted public input. |

`IdentityManager` exposes `get_distinct_id`, `get_anon_id`, `is_identified`, `identify`, `group`,
`get_groups`, and `reset`. Use the module-level helpers for request scope.

## Diagnostic events: `AnalyticsClient`

Capture snapshots event properties when enqueueing. Later mutations do not change queued events.
Properties must be JSON-serializable; invalid events are rejected before they enter the queue.
The built-in analytics and feature-flag HTTP clients do not follow redirects: configure the final
API endpoint directly so project credentials stay at that endpoint.

```python
events = anectico.AnalyticsClient(
    endpoint='https://api.anectico.com',
    api_key=os.environ['ANECTICO_API_KEY'],
    release=os.environ.get('MY_APP_RELEASE'),
    app_version=os.environ.get('MY_APP_VERSION'),
)
```

`release` and `app_version` are stamped as the reserved `$release`/`$app_version` string
properties on every `capture()`'d event when set; the SDK never auto-detects a server
deployment's version, so set them yourself from your own deploy pipeline (for example, reading
your own environment variable in application code — this module never reads the environment
itself). Leave them unset and the properties are omitted entirely, never sent as `""`. See
[Reserved event properties](/docs/investigate/event-schema#reserved-properties).

| API | Parameters | Returns | Behavior |
| --- | --- | --- | --- |
| `AnalyticsClient(endpoint, api_key, flush_at=20, flush_interval_s=5, sender=None, on_delivery_error=None, release=None, app_version=None)` | connection, batching, optional custom sender, optional dropped-batch handler, optional release identifiers | client | Starts an optional daemon flush worker. |
| `capture(event, properties=None, *, distinct_id=None, session_id=None, timestamp=None)` | event and optional overrides | `None` | Queues an event. Without an explicit ID it uses shared identity. |
| `identify(distinct_id, properties=None, *, anon_distinct_id=None)` | ID; properties; optional alias | `None` | Queues identify and switches shared identity after enqueue; normal batching controls delivery. |
| `group(group_type, group_key, properties=None)` | group and properties | `None` | Records shared membership and queues `$groupidentify`. |
| `flush()` | — | `bool` | Validates complete indexed acknowledgements. Returns `True` for all attempted events queue-acknowledged (or an empty queue), `False` for explicit refusals without pending retries, and raises `RuntimeError` while uncertain events remain. |
| `stop()` | — | `bool` | Stops the worker and flushes, returning what that final flush returned. Raises while retryable events remain queued. Further capture calls raise a closed-client error. |
| `disable()` | — | `None` | Immediately closes analytics collection and discards pending work without a final flush; active delivery settles separately. |
| `stats()` | — | `dict[str,int]` | `recorded`, `delivered`, `dropped`, `queued`. `delivered` counts validated queue acknowledgements; `dropped` counts explicit refusals/local overflow; `queued` includes in-flight and uncertain events. |
| `reset()` | — | `None` | Attempts to flush, then rotates shared anonymous identity even if delivery raises; retained events keep their original attribution. |

Capture uses the [versioned indexed acknowledgement contract](/docs/reference/rest-api#capture-acknowledgements).
HTTP status alone never proves delivery or refusal. The SDK validates the version, request UUID,
original indices, all outcomes and totals before changing counters. Queue acknowledgement does not
prove a unique stored row, query visibility or a completed identity mutation.

`flush()` raises while uncertain events remain, including network errors and malformed responses
at any HTTP status. A flush can report explicit drops and also retain an uncertain remainder;
inspect `stats()` and `on_delivery_error` when it raises. Only uncertain original positions and
unsent later chunks retry, preserving message IDs, identity, timestamps and payloads. Retries wait
at least one second and honor `Retry-After` up to 60 seconds. Accepted and refused siblings are
never automatically retried.

`stop()` performs a final flush. For collection withdrawal, call `disable()` instead:
collection closes immediately and pending events are discarded with `collection_disabled`.
Further capture/identify/group/sync_person calls raise the closed-client RuntimeError. Disable
is nonblocking and safe in a delivery callback; call `flush()` outside callbacks to wait for
an active request to settle, then inspect stats. A chunk already admitted to delivery may still
reach the server; its validated acceptance/refusal counts remain, but uncertain positions and
later chunks are never retried. A flush that discards work returns False. After settlement,
queued is zero. Repeated disable is safe; new consent requires a new AnalyticsClient. This does
not erase stored events or disable separate telemetry clients. Custom synchronous senders must
supply a finite I/O deadline; the SDK cannot cancel arbitrary Python code. Gate collection before
initialization where your application requires it.

Explicit refusals are reported once per affected chunk through
`on_delivery_error(DeliveryError(status, dropped, reason))`, `stats()["dropped"]`, and a warning.
Reasons include `invalid_event`, `quota_exceeded`, `mixed_rejection`, `bad_request`, `unauthorized`,
`forbidden`, `payload_too_large`, `queue_overflow` and `collection_disabled` (local discards use status 0). Uncertainty
alone does not fire a drop callback. A handler that raises is swallowed. Response bodies larger
than 64 KiB or inconsistent with HTTP framing remain uncertain, even with a valid JSON prefix.

```python
events = anectico.AnalyticsClient(
    endpoint='https://api.anectico.com',
    api_key=os.environ['ANECTICO_API_KEY'],
    on_delivery_error=lambda err: my_alerting.warn(
        f'anectico dropped {err.dropped} events: {err.status} {err.reason}'
    ),
)
```

Capture resolves its explicit ID, request-local identity, W3C baggage, then shared identity. Shared
`$groups` are inherited only for that shared person; explicit event `$groups` take precedence.
Request-scoped group assertions do not change another person's shared memberships. Identity/group
changes follow successful enqueue even when immediate delivery fails. `reset()` clears identity
even during a delivery outage, while queued events keep their original attribution.

Custom senders must return `anectico.analytics.SendResult(status, body, retry_after_s=None)`.
`body` contains the exact response bytes, including typed failures. An integer status or an
assumed accepted count is insufficient evidence and leaves the batch queued. Custom transports
must bound their network operations and preserve complete response framing; the default urllib
sender uses a 10-second socket timeout, a bounded body read, and checks declared content length.
Both use the same acknowledgement validator.
Analytics and feature-flag endpoints must be absolute HTTP(S) base URLs without credentials,
query strings, or fragments.

## Feature flags: `anectico.feature_flags.FeatureFlags`

Bootstrap data and returned payload containers are copied. Exposure deduplication distinguishes
identity, flag name, and the response's type and value. Remote decision responses must contain one
JSON document of at most 16 MiB, with boolean or string flag values. Invalid responses preserve the
last valid cache; overlapping reloads allow only the latest invocation to replace it.

Construct with `endpoint`, `api_key`, a `capture(event, properties, distinct_id)` callback, and
optional bootstrap response. The key needs `flags:read`; the callback normally sends exposure events
using `analytics:write`.

| Method | Parameters | Returns | Behavior |
| --- | --- | --- | --- |
| `reload(distinct_id, person_properties=None, groups=None)` | person and targeting context | `None` | Calls `/api/v1/decide` and replaces cached decisions. Network/API failures raise. |
| `get_feature_flag(key, distinct_id)` | key and ID | decision or `None` | Returns a decision and emits one deduplicated exposure. |
| `is_feature_enabled(key, distinct_id)` | key and ID | `bool` | True for `True` or a non-empty string variant. |
| `get_feature_flag_payload(key)` | key | any | Returns a deep copy of the cached payload. |
| `get_all_flags()` | — | `dict` | Returns a copy of cached flags. |
| `had_evaluation_errors()` | — | `bool` | Whether the latest evaluation used a fallback. |
| `reset()` | — | `None` | Clears exposure dedup after identity changes. |
| `refresh_local_evaluation()` | constructor `project_id` supplies scope | `"updated" \| "not_modified"` | Fetches or conditionally revalidates the strict project snapshot; requires `flags:read`. |
| `evaluate_local(key, distinct_id, person_properties=None)` | flag key, exact identity, person properties | `LocalEvaluationResult` | Uses only a fresh snapshot; successful reads use the constructor capture callback. |

Pass the exact canonical `project_id` to the same `FeatureFlags` constructor to enable server-side
local evaluation. `refresh_local_evaluation()` conditionally validates the versioned snapshot;
`evaluate_local(key, distinct_id, person_properties=None)` returns `LocalEvaluationResult` with a
value/payload or an explicit unavailable, stale, unsupported-target, or malformed-rule error. The
last-known-good snapshot is usable only through its advertised max-age. Successful local
`matched`/`default` reads use the existing capture callback to emit `$feature_flag_called`, deduped
per identity/key/value; `reset()` reopens both remote and local deduplication. That capture
path needs `analytics:write` when it sends to Anectico.

## Framework and provider integrations

| Integration | Constructor/function | Behavior |
| --- | --- | --- |
| FastAPI/Starlette | `AnecticoFastAPIMiddleware(app, client=None, skip_paths=None, trust_incoming_identity=False, request_identity_resolver=None)` | Privacy-safe ASGI server spans, status/duration, fail-open errors, explicit cancellation outcomes, and optional sync/async server-authenticated identity. Matched operations/URL targets use the bounded route template; concrete path/query values are excluded. `skip_paths=set()` disables default exclusions. |
| Flask | `AnecticoFlaskMiddleware(app, client, skip_paths=None, trust_incoming_identity=False)` | Registers request hooks without changing responses/error handlers. Matched operations and URL/target attributes use the bounded Werkzeug route; concrete path identifiers, query/fragment values, and unmatched paths are not exported. Unmatched operations use the HTTP method only. Unhandled exceptions are captured before Flask's framework log and the server span ends once during teardown, keeping one linked Issue/log/trace. `skip_paths=set()` explicitly disables the default health/static exclusions. |
| Django | `AnecticoDjangoMiddleware` after Django authentication; `ANECTICO` settings mapping | Creates route-normalized server spans, request-scoped authenticated identity, privacy-safe ORM child spans, status/duration, and deduplicated real-view exception capture. Full query strings, SQL/parameter values, usernames, and email addresses are not exported on spans. `USER_ID_RESOLVER` accepts a callable or dotted path and defaults to authenticated `user.pk`; put recognizable profile fields in `sync_person`. |
| Django lifecycle | `get_django_client()` / `shutdown_django_client()` | Returns the process-local auto-created client / returns its `ShutdownResult` and detaches it. A repeated hook returns `attempted=False`, `success=None`. Use the shutdown helper from Gunicorn `worker_exit`. |

FastAPI, Flask, and Django strip URL userinfo, query strings, and fragments.
HTTP attributes are bounded independently and receive a `.truncated=true` sibling
when shortened, so one oversized request does not invalidate an exporter batch.
Managed child spans also preserve the sampled or unsampled flag of a valid incoming
trace parent; local ratios apply only to new roots.
| Celery | `AnecticoCeleryIntegration(app, client)` / `close()` | Strongly registers publish/task lifecycle receivers; restores Anectico log export after Celery's default logger setup; injects and extracts W3C trace plus identity baggage through private broker headers; isolates each consumer attempt in its execution context, including overlapping deliveries with the same task ID, for child/log/error correlation; records type-only retry diagnostics; and never copies task arguments, results, or retry messages. `close()` disconnects receivers but does not stop the application-owned client. |
| `grpc.aio` unary client | `AnecticoAioUnaryUnaryClientInterceptor(client)` | Starts a client span and injects W3C trace/identity metadata only on channels where the interceptor is explicitly installed. Application messages, propagation values, and arbitrary metadata are not recorded. Non-OK transport spans remain visible but do not create generic Issues; capture one typed error at the domain handling boundary when needed. |
| `grpc.aio` server | `AnecticoAioServerInterceptor(client, trust_incoming_identity=False)` | Continues an inbound unary-unary trace, records method/duration/status, and rejects caller-supplied Anectico identity by default. Enable identity trust only on a private sanitizing boundary. Non-OK transport spans remain visible but do not create generic Issues. |
| OpenAI | `wrap_openai(client, anectico_client, capture_content=False)` | Idempotently instruments sync/async Chat Completions, including streams, in place and returns the same client. A stream records once on exhaustion; use `stream_options={"include_usage": True}` for token/cost fields. |
| Anthropic | `wrap_anthropic(client, anectico_client, capture_content=False)` | Instruments sync/async messages, including streams, in place and returns the same client. Token and cache counts need no extra request parameter. |

A stream is recorded once, when it is exhausted, fails, or is abandoned. Closing a
stream before exhaustion — or leaving its `with` block early — is not an error:
the span keeps its normal status and carries
`anectico.gen_ai.stream.abandoned = true`, so early exits stay out of your error
rate while remaining distinguishable. Its token counts are the provider's last
reported values and are partial by construction. A provider or iteration
exception records an error-status call and is preserved unchanged.

Stream telemetry retains the identity and trace context present when the provider call began,
even if a different request or task later consumes the stream.

Neither wrapper changes your request; `stream_options={"include_usage": True}` is
never added for you, because it inserts an extra empty-`choices` chunk into your
loop and is rejected outright by some OpenAI-compatible gateways. A count the
provider never reported is recorded as absent, never as zero. Anthropic's
`messages.stream()` helper is not covered — it issues its own request rather than
going through `messages.create` — so use `messages.create(..., stream=True)` for
an instrumented stream. Telemetry errors never replace a provider exception.
Content capture is disabled by default.

## Authentication and lower-level utilities

These exports support custom middleware and integrations. Application code should generally use
`AnecticoClient` and `AnalyticsClient`.

| API/type | Parameters or fields | Returns/behavior |
| --- | --- | --- |
| `validate_api_key(key)` | string | `None`; raises `InvalidAPIKeyError` for an unsupported key shape. |
| `anectico.auth.is_api_key_format(value)` | string | Boolean shape check without throwing. |
| `mask_api_key(key)` | string | Log-safe prefix plus last four characters; never usable as a credential. |
| `User(...)` | `id`, `email`, `username`, `ip_address`, `segment`, `data` | Error-user data class; `to_otel_attributes()` returns `enduser.*`/`user.data.*` attributes. |
| `set_context_user(user)` | `User` or `None` | Sets async request-local error-user context; call with `None` when the scope ends. |
| `get_context_user()` | — | Request-local `User` or `None`. |
| `Breadcrumb(timestamp, category, message, level, data=None)` | canonical breadcrumb fields | `to_dict()` returns the JSON-compatible wire shape, omitting empty data. |
| `BreadcrumbBuffer()` | fixed capacity 100 | Thread-safe `add`, `snapshot`, `serialize`, `drain`, and `clear`; `drain` serializes and clears atomically. |
| `ErrorLevel` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `FATAL` | String severity constants. |
| `ErrorOptions(...)` | `user`, `tags`, `extra`, `level`, `fingerprint` | Data class for custom error-recording integrations. Normal capture calls accept these fields directly. |
| `IdentityManager()` | — | Thread-safe holder with `get_distinct_id`, `get_anon_id`, `is_identified`, `observability_distinct_id`, `identify`, `group`, `get_groups`, and `reset`. Use the module-level functions for request scope. |
| `shared_identity()` | — | Process-wide manager shared by signals and diagnostic events. |
| `BaggageIdentitySpanProcessor(identity=None)` | optional `IdentityManager` | Identity-only span processor for application-owned providers. Defaults to `shared_identity()`; does not export/register/own lifecycle. Add before the export processor. |
| `BaggageIdentityLogRecordProcessor(identity=None)` | optional `IdentityManager` | Identity-only log processor for application-owned providers. Defaults to `shared_identity()`; does not export/register/own lifecycle. Add before the export processor. |
| `BAGGAGE_DISTINCT_ID_KEY` | constant | The W3C baggage member name `anectico.distinct_id`. |
| `DistinctIDScope` | opaque token | Returned by `set_context_distinct_id`; pass it to `reset_context_distinct_id`. |

`anectico.__version__` reports the installed package version. Lower-level stack/error serializer and
transport modules are implementation APIs and may change; use the public client methods above.

- [Install and use Python](/docs/instrument/python)
- [Identify customers safely](/docs/instrument/identity)
- [Record LLM calls](/docs/instrument/llm-calls)
