Skip to content

feat(ai): add per-request context to span processor - #1060

Merged
carlos-marchal-ph merged 3 commits into
mainfrom
feat(aio)/span-processor-request-context
Oct 14, 2026
Merged

carlos-marchal-ph merged 3 commits into
mainfrom
feat(aio)/span-processor-request-context

Conversation

@carlos-marchal-ph

@carlos-marchal-ph carlos-marchal-ph commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

  • A server that traces LLM calls for many users through PostHogSpanProcessor sends every AI event under one person, or none.
  • The only documented way to set the user was a resource attribute, which is fixed for the whole process.
  • PostHogSpanProcessor now adds the active PostHog context to the AI spans it exports:
    • identify_context → posthog.distinct_id
    • set_context_session → $session_id
    • each tag → a property under its own key
  • Usage: wrap the work in posthog.new_context(), as for any other capture.
with posthog.new_context():
    posthog.identify_context(user_id)
    posthog.tag("plan", "pro")
    handle_request()  # AI spans started here carry the user, session and tag

How it works

  • on_start stores the active context for the span, keyed by trace and span ID. It does nothing when no context is active.
  • on_end releases the entry for every span. For AI spans it forwards a copy of the span with the context attributes merged in.
  • If the span already carries a user (posthog.distinct_id, user.id or the ai.telemetry/ai.settings distinct ID keys), no posthog.distinct_id is added from the context or from a tag.
  • The context session maps to $session_id, the same as capture, so AI events link to replays. $ai_session_id stays free for the instrumentation's own conversation ID, or an explicit $ai_session_id tag.
  • Fork-safe: the store and its lock reset in the child after os.fork. A span that is never ended releases its entry when it is garbage-collected.
  • The live span is never changed, so other processors and exporters on the same provider never see the user, session or tags.
  • Precedence: the span's own attribute wins, then the tag, then the context distinct ID and session ID.
  • Value rules:
    • strings, numbers, bools and homogeneous lists pass through
    • integers outside the signed 64-bit range become strings
    • datetime, UUID and Decimal become strings, with ISO format for dates
    • NaN, ±inf, None, empty strings and values that can't be encoded are skipped
    • other values are sent as JSON strings
  • Stamping can never raise into the caller. Errors are caught per value and logged at debug level.
  • shutdown clears the store.
  • PostHogTraceExporter can't do this, because an exporter only sees finished spans. Its docstring now says so.

Note

Apps that already use PostHog contexts, for example through the Django, Flask or ASGI request middleware, will see their AI events attributed to the context's person after upgrading. The changeset says so.

Same change in posthog-js (@posthog/ai) and posthog-go (otel).

💚 How did you test it?

  • New tests in posthog/test/ai/otel/test_processor.py run a real TracerProvider. They cover:
    • two concurrent contexts on threads, each exported with its own values
    • attributes set by the span itself are kept
    • another processor on the same provider sees no context attributes
    • a non-AI span is dropped and leaves no stored entry
    • an unencodable tag doesn't break export, and the other tags survive
    • the value rules
  • End to end against a local PostHog with an OTel-instrumented LLM SDK: two parallel requests with different users each produced an $ai_generation event with the right distinct ID, $session_id and tag. The same run passed for Node and Go.
  • The ReadableSpan constructor used for the copy is identical on opentelemetry-sdk 1.20, 1.27 and 1.40.
  • Not run: the test suite on SDK 1.20. Its old protobuf doesn't import on Python 3.14 locally.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran sampo add to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Written with Claude Code (claude-opus-5-5) and subagents, directed by @carlos-marchal-ph.
  • Design chosen with Carlos: reuse the existing PostHog context API instead of adding a resolver callback option.
  • The first version wrote onto the live span. Independent reviews flagged that this leaks the user ID and tags to every other exporter on the provider. It now stamps only the copy sent to PostHog.
  • Stored by (trace_id, span_id) rather than a weak reference to the span, because the SDK passes on_end a fresh ReadableSpan object.
  • The copy reports dropped attribute, event and link counts as 0. PostHog ingestion doesn't read them.

🤖 Generated with Claude Code

@carlos-marchal-ph
carlos-marchal-ph requested a review from a team as a code owner October 14, 2026 10:14
@carlos-marchal-ph carlos-marchal-ph self-assigned this Oct 14, 2026
@carlos-marchal-ph
carlos-marchal-ph requested a review from a team October 14, 2026 10:14

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 8161d56: 1 Must fix, 3 Should fix.

Comment thread posthog/ai/otel/processor.py Outdated
)
self._processor = BatchSpanProcessor(exporter)
self._captured: Dict[Tuple[int, int], RequestContext] = {}
self._captured_lock = threading.Lock()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 · Reinitialize the capture lock after a fork

When a process forks while another thread holds _captured_lock, the child inherits a locked mutex without the thread that releases it. Every subsequent on_end blocks, including spans without PostHog context, hanging application work despite the underlying batch processor's fork handling. Reinitialize the lock and clear captured state after fork.

snapshot = RequestContext(
distinct_id=get_context_distinct_id() or None,
session_id=get_context_session_id() or None,
tags=dict(get_tags()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · Freeze mutable tag values when capturing the context

When a dictionary or list passed to contexts.tag changes before the span ends, dict(get_tags()) retains the same mutable value. with_request_context then exports the updated value instead of the context at span start; a dictionary tagged with tier='pro' exports tier='free' after mutation. Copy or normalize each value during capture.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving this one on purpose. values are read when the span ends so spans that turn out not to be AI spans (http, db...) cost almost nothing. snapshotting at start would bring that cost back to guard against code mutating a tagged list while an llm call is in flight, which we don't expect. noted in the docstring that tag values are read at span end

Comment thread posthog/ai/otel/processor.py Outdated
Comment on lines +94 to +96
if captured:
with self._captured_lock:
self._captured[_span_key(span)] = captured

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · Release captured contexts when abandoned spans are collected

When a manually started span is abandoned without end(), _captured retains its context even after the span is garbage collected. This applies to non-AI spans too, and retains arbitrary tag objects until processor shutdown, causing memory growth in long-lived services. Tie entries to span lifetime with weak-reference cleanup or enforce bounded retention.

Comment on lines +99 to +102
if isinstance(value, _PRIMITIVE_TYPES):
return value
if isinstance(value, (list, tuple)) and _is_homogeneous(value):
return list(value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · Validate integer tags against OTLP's signed 64-bit range

A context tag containing an integer outside the signed 64-bit range, such as 2**63, passes through to_attribute_value unchanged, including inside homogeneous lists. OTLP's AnyValue.int_value cannot represent it, so the exporter logs an encoding error and drops the entire property. Validate integer bounds and stringify oversized values before export.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

posthog-python Compliance Report

Date: 2026-10-14T11:19:35.903198+00:00
Duration: 260180ms

✅ All Tests Passed!

121/121 tests passed


Capture_V1 Tests

✅ 95/95 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 519ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 512ms
Required Headers.Has Authorization Bearer Header ✅ 512ms
Required Headers.Has Content Type Json ✅ 513ms
Required Headers.Has Posthog Sdk Info Format ✅ 511ms
Required Headers.Has Posthog Attempt Header ✅ 511ms
Required Headers.Has Posthog Request Id ✅ 512ms
Required Headers.Has Posthog Request Timestamp ✅ 512ms
Required Headers.Has User Agent ✅ 511ms
Body Format.Body Has Created At And Batch ✅ 511ms
Body Format.No Api Key In Body ✅ 512ms
Body Format.No Sent At In Body ✅ 512ms
Event Format.Event Has Required Root Fields ✅ 513ms
Event Format.Event Uuid Is Valid ✅ 510ms
Event Format.Event Timestamp Is Rfc3339 ✅ 512ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 518ms
Event Format.Distinct Id Is String ✅ 512ms
Event Format.Distinct Id At Root Not Properties ✅ 511ms
Event Format.Custom Properties Preserved ✅ 512ms
Event Format.Set Properties Preserved ✅ 513ms
Event Format.Set Once Properties Preserved ✅ 511ms
Event Format.Groups Properties Preserved ✅ 511ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 511ms
Event Format.Event Has Required Root Fields Batch ✅ 516ms
Event Format.Event Uuid Is Valid Batch ✅ 516ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 516ms
Event Format.Distinct Id Is String Batch ✅ 516ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 514ms
Event Format.Custom Properties Preserved Batch ✅ 517ms
Event Format.Set Properties Preserved Batch ✅ 516ms
Event Format.Set Once Properties Preserved Batch ✅ 516ms
Event Format.Groups Properties Preserved Batch ✅ 514ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 516ms
Batch Behavior.Multiple Events In Single Batch ✅ 518ms
Batch Behavior.Batch Envelope Smoke ✅ 519ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 507ms
Batch Behavior.Flush At Triggers Batch ✅ 1013ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 513ms
Deduplication.Generates Unique Uuids ✅ 519ms
Deduplication.Different Events Same Content Different Uuids ✅ 514ms
Deduplication.Preserves Uuid On Retry ✅ 6521ms
Deduplication.Preserves Timestamp On Retry ✅ 6522ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 6520ms
Deduplication.No Duplicate Events In Batch ✅ 521ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 512ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 13530ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 6518ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 3022ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 6523ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 511ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 515ms
Response Format Validation.Success No Retry After When All Ok ✅ 515ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 1518ms
Response Format Validation.Success No Retry After When Drop Only ✅ 516ms
Response Format Validation.Response Echoes Request Id ✅ 512ms
Retry Behavior.Retries On 408 ✅ 6523ms
Retry Behavior.Retries On 500 ✅ 6519ms
Retry Behavior.Retries On 503 ✅ 8525ms
Retry Behavior.Retries On 504 ✅ 6521ms
Retry Behavior.Retryable Errors Have Retry After ✅ 3520ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 11518ms
Retry Behavior.Does Not Retry On 400 ✅ 2517ms
Retry Behavior.Does Not Retry On 401 ✅ 2515ms
Retry Behavior.Does Not Retry On 402 ✅ 2515ms
Retry Behavior.Does Not Retry On 413 ✅ 2513ms
Retry Behavior.Does Not Retry On 415 ✅ 2513ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 2513ms
Retry Behavior.Implements Backoff ✅ 22538ms
Retry Behavior.Max Retries Respected ✅ 22539ms
Partial Batch Handling.Handles 200 Full Success ✅ 2512ms
Partial Batch Handling.Handles 200 With All Ok ✅ 3518ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 3516ms
Partial Batch Handling.Does Not Retry Limited Events ✅ 3514ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 6522ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 6521ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 6526ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 6524ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 6524ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 6518ms
Partial Batch Handling.Respects Retry After On Partial ✅ 8518ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 3517ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry ✅ 3519ms
Compression.Sends Gzip Content Encoding ✅ 512ms
Compression.No Content Encoding When Disabled ✅ 511ms
Compression.Compressed Body Is Decompressible ✅ 511ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 2513ms
Event Options.Cookieless Mode Override ✅ 512ms
Event Options.Disable Skew Correction Override ✅ 510ms
Event Options.Process Person Profile Override ✅ 511ms
Event Options.Product Tour Id Override ✅ 511ms
Event Options.Unset Options Omitted ✅ 511ms
Event Options.Options Override In Batch ✅ 517ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 513ms
Geoip And Historical Migration.Historical Migration Set In Body ✅ 511ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 513ms

Capture_Ai Tests

✅ 5/5 tests passed

View Details
Test Status Duration
Routing.Capture Ai Posts To Ai Endpoint ✅ 511ms
Routing.Capture Does Not Reroute Ai Named Events ✅ 512ms
Identity.Capture Ai Event Has Uuid ✅ 511ms
Identity.Capture Ai Keeps Supplied Uuid ✅ 512ms
Timestamp Format.Non Utc Event Timestamp Is Converted To Utc ✅ 512ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 14ms
Request Payload.Flags Request Uses V2 Query Param ✅ 14ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 12ms
Request Payload.Flags Request Omits Authorization Header ✅ 11ms
Request Payload.Token In Flags Body Matches Init ✅ 13ms
Request Payload.Groups Round Trip ✅ 12ms
Request Payload.Groups Default To Empty Object ✅ 12ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 13ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 11ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 13ms
Request Lifecycle.No Flags Request On Init Alone ✅ 4ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 513ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 19ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 14ms
Retry Behavior.Retries Flags On 502 ✅ 317ms
Retry Behavior.Retries Flags On 504 ✅ 317ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 515ms

Feature_Flags_Local_Evaluation Tests

✅ 4/4 tests passed

View Details
Test Status Duration
Versioned Boolean Matching.Matching Version Missing ✅ 94ms
Versioned Boolean Matching.Matching Version 1 ✅ 86ms
Versioned Boolean Matching.Matching Version 2 ✅ 88ms
Versioned Boolean Matching.Version Only Reload 1 2 1 2 Missing ✅ 47ms

@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[Medium impact] Adds per-request context capture to the AI span processor.

Fix the inherited lock before merging; forked workers can hang when spans end.

Reviews (1) · Last reviewed commit: "feat: span processor request context" · Reviewed by Greptile

Comment thread posthog/ai/otel/processor.py Outdated
)
self._processor = BatchSpanProcessor(exporter)
self._captured: Dict[Tuple[int, int], RequestContext] = {}
self._captured_lock = threading.Lock()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Forked workers can hang

If another parent thread holds _captured_lock when a worker forks, the child inherits the locked object but not the thread that can release it. Every subsequent on_end() waits forever, even for non-AI spans or spans without PostHog context, so requests can hang.

Reset the lock and clear _captured in a child-process fork hook.

Knowledge Base Used: Framework integrations

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/ai/otel/processor.py
Line: 82

Comment:
**Forked workers can hang**

If another parent thread holds `_captured_lock` when a worker forks, the child inherits the locked object but not the thread that can release it. Every subsequent `on_end()` waits forever, even for non-AI spans or spans without PostHog context, so requests can hang.

Reset the lock and clear `_captured` in a child-process fork hook.

**Knowledge Base Used:** [Framework integrations](https://app.greptile.com/posthog-org-19734/-/custom-context/knowledge-base/posthog/posthog-python/-/docs/framework-integrations.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

snapshot = RequestContext(
distinct_id=get_context_distinct_id() or None,
session_id=get_context_session_id() or None,
tags=dict(get_tags()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Saved tags change later

dict(get_tags()) copies the keys but keeps references to mutable tag values. If a caller tags a list, starts a span, then appends to that list before ending the span, the exported tag contains the appended value rather than the value captured at span start.

Convert each tag to an independent attribute value during on_start(), preserving the per-value error guards, and add a mutation test.

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/ai/otel/_request_context.py
Line: 36

Comment:
**Saved tags change later**

`dict(get_tags())` copies the keys but keeps references to mutable tag values. If a caller tags a list, starts a span, then appends to that list before ending the span, the exported tag contains the appended value rather than the value captured at span start.

Convert each tag to an independent attribute value during `on_start()`, preserving the per-value error guards, and add a mutation test.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving this one on purpose. values are read when the span ends so spans that turn out not to be AI spans (http, db...) cost almost nothing. snapshotting at start would bring that cost back to guard against code mutating a tagged list while an llm call is in flight, which we don't expect. noted in the docstring that tag values are read at span end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Reading tags at span end keeps the common non-AI span path cheap, and the documented end-time semantics make the mutable-value behavior an intentional tradeoff rather than an unnoticed snapshot bug. No change needed.

Comment thread posthog/ai/otel/_request_context.py Outdated
from ...contexts import get_context_distinct_id, get_context_session_id, get_tags

DISTINCT_ID_ATTRIBUTE = "posthog.distinct_id"
SESSION_ID_ATTRIBUTE = "$ai_session_id"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be $session_id? The rest of the SDK treats the context session as the web/replay session. add_context_tags writes it as $session_id (client.py:269), and the Django and ASGI middleware fill it from the posthog-js X-POSTHOG-SESSION-ID header. Stamping it as $ai_session_id has three effects:

  • AI events get no $session_id, so the trace view's replay link never shows (getSessionID reads only $session_id).
  • Ingestion fills $ai_session_id from the instrumentation's own session ID (e.g. gen_ai.conversation.id, anthropic.session.id) only while it's unset, so this replaces the real conversation grouping with the browser session.
  • The native wrappers in posthog.ai capture under the same context and send $session_id, so OTel and wrapper events disagree.

Would mapping it to $session_id, and leaving $ai_session_id to span attributes or an explicit tag, work? posthog-js#5299 and posthog-go#371 use the same mapping, so all three would change together.

Comment thread posthog/test/ai/otel/test_processor.py Outdated
def request(user):
with new_request_context():
contexts.identify_context(user)
both_inside.wait()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The barrier releases before either span starts, and each span starts and ends straight away, so the two spans can run one after the other. A single shared "last captured" slot would still pass, and so would moving the capture from on_start to on_end, since every test ends its span inside the context. Could this start both spans inside their contexts, wait on the barrier, and then end them after the contexts exit? That covers concurrent spans and the streaming case the on_start snapshot exists for.

pypi/posthog: minor
---

`PostHogSpanProcessor` now adds the active PostHog context to the AI spans it exports: the distinct ID from `identify_context`, the session ID from `set_context_session`, and context tags as properties. Spans started inside `posthog.new_context()` reach PostHog under the right person, so one process can trace many users. Attributes already on a span are kept, and other processors on the provider never see the context. Apps already using PostHog contexts (for example the Django, Flask or ASGI request middleware) will see their AI events attributed to the context's person after upgrading.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this also say that the middleware tags (email, $ip, $user_agent, $current_url, ...) now land on every AI event? The Go changeset mentions it, and it's the part of the upgrade a privacy-minded user would want to know about.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed a1dbaf0: 1 Must fix, 3 Should fix.

warn_if_posthog_ai_gateway_otel_attributes(span.attributes)
if captured:
try:
span = with_request_context(span, captured)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 · Provide a privacy filter before automatically exporting context tags

With Django middleware and a Client.before_send scrubber removing email and $current_url, this enrichment newly uploads those raw properties through OTLP. with_request_context copies middleware tags directly, and the batch processor bypasses the client's scrubber, exposing previously filtered request data. Provide a filtering hook or explicit opt-in before exporting tags.

extra = {
key: value
for key, value in request_context_attributes(context).items()
if key not in own and not (has_identity and key == DISTINCT_ID_ATTRIBUTE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · Preserve span identity across all context identity aliases

With posthog.distinct_id='bob' on a span and an ai.telemetry.metadata.posthog_distinct_id='alice' context tag, this guard adds the tag because it only excludes the canonical key. Ingestion prioritizes the metadata alias and attributes Bob's event to Alice, violating span precedence. Suppress all context identity aliases when the span already has an identity.

def with_request_context(span: ReadableSpan, context: RequestContext) -> ReadableSpan:
"""Return a copy of ``span`` with the context's values added, keeping attributes it already has."""
own = span.attributes or {}
has_identity = any(key in own for key in IDENTITY_ATTRIBUTES)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · Check that existing identity attributes contain usable IDs

When a span contains user.id='' or user.id=42, has_identity becomes true and blocks a valid ID from identify_context('alice'). Ingestion accepts only nonempty strings for identity, so it ignores the span value and assigns a resource or anonymous identity instead of Alice. Only treat identity attributes as set when ingestion can use their values.

context=span.get_span_context(),
parent=span.parent,
resource=span.resource,
attributes={**own, **extra},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · Apply configured span limits to context attributes

When a provider configures SpanLimits or OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, context tags bypass those safeguards because with_request_context supplies a plain dictionary to ReadableSpan. Large strings and arbitrary tag counts reach the batch queue and OTLP encoder unbounded, increasing memory and export costs. Apply the originating span's limits when merging context attributes.

@carlos-marchal-ph
carlos-marchal-ph merged commit 6916499 into main Oct 14, 2026
66 of 72 checks passed
@carlos-marchal-ph
carlos-marchal-ph deleted the feat(aio)/span-processor-request-context branch October 14, 2026 16:52
eli-r-ph added a commit that referenced this pull request Oct 23, 2026
* feat(metrics): attach trace exemplars to flushed metric series (#1057)

(cherry picked from commit 3cc2d64)

* chore: Release v7.69.0 [skip ci]

(cherry picked from commit 653ff87)

* Update generated references

(cherry picked from commit 6f65898)

* feat(ai): add per-request context to span processor (#1060)

* feat: span processor request context

* fix: span processor review feedback

* fix: span processor review follow-ups

(cherry picked from commit 6916499)

* chore: Release v7.70.0 [skip ci]

(cherry picked from commit a615eed)

* Update generated references

(cherry picked from commit a49648e)

* fix(prompts): share one request between concurrent fetches of the same prompt (#1059)

(cherry picked from commit 10d094e)

* chore: Release v7.70.1 [skip ci]

(cherry picked from commit ec71a24)

* Update generated references

(cherry picked from commit fc3c434)

* fix(ai): mark cancelled async LLM calls as errors (#1065)

* fix(ai): mark cancelled async LLM calls as errors

* test: fix Python 3.10 cancellation and FastMCP compatibility

(cherry picked from commit fec41f2)

* chore: Release v7.70.2 [skip ci]

(cherry picked from commit e381808)

* Update generated references

(cherry picked from commit f33d55d)

* chore(deps): bump the ai-providers group with 8 updates (#1054)

Bumps the ai-providers group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [langchain](https://github.com/langchain-ai/langchain) | `1.4.2` | `1.4.3` |
| [anthropic](https://github.com/anthropics/anthropic-sdk-python) | `1.8.0` | `1.11.0` |
| [langgraph](https://github.com/langchain-ai/langgraph) | `1.2.12` | `1.2.13` |
| [langchain-core](https://github.com/langchain-ai/langchain) | `1.6.5` | `1.6.6` |
| [langchain-openai](https://github.com/langchain-ai/langchain) | `1.6.6` | `1.6.7` |
| [langchain-anthropic](https://github.com/langchain-ai/langchain) | `1.7.4` | `1.7.5` |
| [google-genai](https://github.com/googleapis/python-genai) | `2.25.0` | `2.28.0` |
| [langchain-text-splitters](https://github.com/langchain-ai/langchain) | `1.1.2` | `1.1.3` |

Updates `langchain` from 1.4.2 to 1.4.3
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](langchain-ai/langchain@langchain==1.4.2...langchain==1.4.3)

Updates `anthropic` from 1.8.0 to 1.11.0
- [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md)
- [Commits](anthropics/anthropic-sdk-python@v1.8.0...v1.11.0)

Updates `langgraph` from 1.2.12 to 1.2.13
- [Release notes](https://github.com/langchain-ai/langgraph/releases)
- [Commits](langchain-ai/langgraph@1.2.12...1.2.13)

Updates `langchain-core` from 1.6.5 to 1.6.6
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](langchain-ai/langchain@langchain-core==1.6.5...langchain-core==1.6.6)

Updates `langchain-openai` from 1.6.6 to 1.6.7
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](langchain-ai/langchain@langchain-openai==1.6.6...langchain-openai==1.6.7)

Updates `langchain-anthropic` from 1.7.4 to 1.7.5
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](langchain-ai/langchain@langchain-anthropic==1.7.4...langchain-anthropic==1.7.5)

Updates `google-genai` from 2.25.0 to 2.28.0
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](googleapis/python-genai@v2.25.0...v2.28.0)

Updates `langchain-text-splitters` from 1.1.2 to 1.1.3
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](langchain-ai/langchain@langchain-text-splitters==1.1.2...langchain-text-splitters==1.1.3)

---
updated-dependencies:
- dependency-name: langchain
  dependency-version: 1.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ai-providers
- dependency-name: anthropic
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ai-providers
- dependency-name: langgraph
  dependency-version: 1.2.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ai-providers
- dependency-name: langchain-core
  dependency-version: 1.6.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ai-providers
- dependency-name: langchain-openai
  dependency-version: 1.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ai-providers
- dependency-name: langchain-anthropic
  dependency-version: 1.7.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ai-providers
- dependency-name: google-genai
  dependency-version: 2.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ai-providers
- dependency-name: langchain-text-splitters
  dependency-version: 1.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: ai-providers
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dustin Byrne <dustin@posthog.com>
(cherry picked from commit f95e1f4)

* chore(deps): bump the github-actions group with 2 updates (#1055)

Bumps the github-actions group with 2 updates: [dtolnay/rust-toolchain](https://github.com/dtolnay/rust-toolchain) and [PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml](https://github.com/posthog/posthog-sdk-test-harness).

Updates `dtolnay/rust-toolchain` from 7e38f4b43b4db5c8dd498af069a4f6196df1d067 to e2a55d2ffb04f378e9626c28d38b36d230d1e12f
- [Release notes](https://github.com/dtolnay/rust-toolchain/releases)
- [Commits](dtolnay/rust-toolchain@7e38f4b...e2a55d2)

Updates `PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml` from 1.11.0 to 1.13.1
- [Release notes](https://github.com/posthog/posthog-sdk-test-harness/releases)
- [Changelog](https://github.com/PostHog/posthog-sdk-test-harness/blob/main/CHANGELOG.md)
- [Commits](PostHog/posthog-sdk-test-harness@4593de8...e32f08b)

---
updated-dependencies:
- dependency-name: dtolnay/rust-toolchain
  dependency-version: e2a55d2ffb04f378e9626c28d38b36d230d1e12f
  dependency-type: direct:production
  dependency-group: github-actions
- dependency-name: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml
  dependency-version: 1.13.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dustin Byrne <dustin@posthog.com>
(cherry picked from commit 9b7e632)

* chore(deps): bump mcp from 1.28.1 to 1.30.0 (#1068)

Bumps [mcp](https://github.com/modelcontextprotocol/python-sdk) from 1.28.1 to 1.30.0.
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](modelcontextprotocol/python-sdk@v1.28.1...v1.30.0)

---
updated-dependencies:
- dependency-name: mcp
  dependency-version: 1.30.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 12e88a2)

* docs: clarify reusable flush and terminal shutdown guidance (#1043)

* docs: clarify reusable flush and terminal shutdown guidance

* docs: preserve published 7.67.0 reference snapshot

(cherry picked from commit d0b6f71)

* chore: Release v7.71.0 [skip ci]

(cherry picked from commit 20a574e)

* Update generated references

(cherry picked from commit 883eaca)

* chore(deps): bump the uv group across 4 directories with 2 updates (#1069)

Bumps the uv group with 1 update in the /examples/example-ai-llamaindex directory: [tinytag](https://github.com/tinytag/tinytag).
Bumps the uv group with 1 update in the /examples/example-ai-openai-agents directory: [mcp](https://github.com/modelcontextprotocol/python-sdk).
Bumps the uv group with 1 update in the /examples/example-ai-pydantic-ai directory: [mcp](https://github.com/modelcontextprotocol/python-sdk).
Bumps the uv group with 1 update in the /examples/example-ai-semantic-kernel directory: [mcp](https://github.com/modelcontextprotocol/python-sdk).

Updates `tinytag` from 2.2.1 to 2.3.0
- [Release notes](https://github.com/tinytag/tinytag/releases)
- [Commits](tinytag/tinytag@2.2.1...2.3.0)

Updates `mcp` from 1.28.1 to 1.30.0
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](modelcontextprotocol/python-sdk@v1.28.1...v1.30.0)

Updates `mcp` from 1.29.1 to 1.30.0
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](modelcontextprotocol/python-sdk@v1.28.1...v1.30.0)

Updates `mcp` from 2.1.1 to 2.2.0
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](modelcontextprotocol/python-sdk@v1.28.1...v1.30.0)

---
updated-dependencies:
- dependency-name: tinytag
  dependency-version: 2.3.0
  dependency-type: indirect
  dependency-group: uv
- dependency-name: mcp
  dependency-version: 1.30.0
  dependency-type: indirect
  dependency-group: uv
- dependency-name: mcp
  dependency-version: 1.30.0
  dependency-type: indirect
  dependency-group: uv
- dependency-name: mcp
  dependency-version: 2.2.0
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit b1363dc)

* chore(deps-dev): bump claude-agent-sdk from 0.1.50 to 0.2.121 (#1073)

Bumps [claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-python) from 0.1.50 to 0.2.121.
- [Release notes](https://github.com/anthropics/claude-agent-sdk-python/releases)
- [Changelog](https://github.com/anthropics/claude-agent-sdk-python/blob/main/CHANGELOG.md)
- [Commits](anthropics/claude-agent-sdk-python@v0.1.50...v0.2.121)

---
updated-dependencies:
- dependency-name: claude-agent-sdk
  dependency-version: 0.2.121
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit a89acf8)

* feat(mcp): stop sending standalone $identify events (#1067)

* feat(mcp): stop sending standalone $identify events

The identify callback still sets distinct_id, $set and $groups on every
event. The standalone event carried no $anon_distinct_id, so ingestion
merged no person, and stateless servers sent one per tool call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(mcp): assert no $identify over the wire and refresh docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit facd617)

* chore: Release v7.72.0 [skip ci]

(cherry picked from commit 876b955)

* Update generated references

(cherry picked from commit b8327de)

* chore(deps): bump the github-actions group with 6 updates (#1083)

Bumps the github-actions group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `10.2.0` | `10.3.0` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.38.2` | `4.38.3` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.38.2` | `4.38.3` |
| [actions/create-github-app-token](https://github.com/actions/create-github-app-token) | `3.2.0` | `3.3.0` |
| [dtolnay/rust-toolchain](https://github.com/dtolnay/rust-toolchain) | `e2a55d2ffb04f378e9626c28d38b36d230d1e12f` | `683b588c487a69d01f6c6ddf40f036ff682cc2c8` |
| [PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml](https://github.com/posthog/posthog-sdk-test-harness) | `1.13.1` | `1.15.0` |

Updates `astral-sh/setup-uv` from 10.2.0 to 10.3.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@c18668a...1c37ad0)

Updates `github/codeql-action/init` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@2892aa5...24c5418)

Updates `github/codeql-action/analyze` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@2892aa5...24c5418)

Updates `actions/create-github-app-token` from 3.2.0 to 3.3.0
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md)
- [Commits](actions/create-github-app-token@bcd2ba4...64ab729)

Updates `dtolnay/rust-toolchain` from e2a55d2ffb04f378e9626c28d38b36d230d1e12f to 683b588c487a69d01f6c6ddf40f036ff682cc2c8
- [Release notes](https://github.com/dtolnay/rust-toolchain/releases)
- [Commits](dtolnay/rust-toolchain@e2a55d2...683b588)

Updates `PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml` from 1.13.1 to 1.15.0
- [Release notes](https://github.com/posthog/posthog-sdk-test-harness/releases)
- [Changelog](https://github.com/PostHog/posthog-sdk-test-harness/blob/main/CHANGELOG.md)
- [Commits](PostHog/posthog-sdk-test-harness@e32f08b...64ffa6a)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/create-github-app-token
  dependency-version: 3.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: dtolnay/rust-toolchain
  dependency-version: 683b588c487a69d01f6c6ddf40f036ff682cc2c8
  dependency-type: direct:production
  dependency-group: github-actions
- dependency-name: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 3822271)

* chore(deps): bump the ai-providers group with 6 updates (#1082)

Bumps the ai-providers group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [langchain](https://github.com/langchain-ai/langchain) | `1.4.3` | `1.4.4` |
| [anthropic](https://github.com/anthropics/anthropic-sdk-python) | `1.11.0` | `1.13.0` |
| [langgraph](https://github.com/langchain-ai/langgraph) | `1.2.13` | `1.2.14` |
| [langchain-core](https://github.com/langchain-ai/langchain) | `1.6.6` | `1.6.9` |
| [langchain-openai](https://github.com/langchain-ai/langchain) | `1.6.7` | `1.7.0` |
| [google-genai](https://github.com/googleapis/python-genai) | `2.28.0` | `2.29.0` |

Updates `langchain` from 1.4.3 to 1.4.4
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](langchain-ai/langchain@langchain==1.4.3...langchain==1.4.4)

Updates `anthropic` from 1.11.0 to 1.13.0
- [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md)
- [Commits](anthropics/anthropic-sdk-python@v1.11.0...v1.13.0)

Updates `langgraph` from 1.2.13 to 1.2.14
- [Release notes](https://github.com/langchain-ai/langgraph/releases)
- [Commits](langchain-ai/langgraph@1.2.13...1.2.14)

Updates `langchain-core` from 1.6.6 to 1.6.9
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](langchain-ai/langchain@langchain-core==1.6.6...langchain-core==1.6.9)

Updates `langchain-openai` from 1.6.7 to 1.7.0
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](langchain-ai/langchain@langchain-openai==1.6.7...langchain-openai==1.7.0)

Updates `google-genai` from 2.28.0 to 2.29.0
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](googleapis/python-genai@v2.28.0...v2.29.0)

---
updated-dependencies:
- dependency-name: langchain
  dependency-version: 1.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ai-providers
- dependency-name: anthropic
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ai-providers
- dependency-name: langgraph
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ai-providers
- dependency-name: langchain-core
  dependency-version: 1.6.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ai-providers
- dependency-name: langchain-openai
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ai-providers
- dependency-name: google-genai
  dependency-version: 2.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ai-providers
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 9b70476)

* feat(llma): support scorer and experiment tags (#1085)

(cherry picked from commit b086f65)

* chore: Release v7.73.0 [skip ci]

(cherry picked from commit 81931fa)

* Update generated references

(cherry picked from commit 9e62e0f)

* fix(flags): accept rediss:// URLs for flag_fallback_cache_url (#1080)

Signed-off-by: Sebastian Turner <sebastiant3484@gmail.com>
(cherry picked from commit 20dabaa)

* chore: Release v7.73.1 [skip ci]

(cherry picked from commit 223b592)

* Update generated references

(cherry picked from commit e9e0830)

* chore(deps): bump aiohttp from 3.14.3 to 3.14.4 (#1086)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.14.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 305dee3)

* chore(deps): bump the uv group across 7 directories with 1 update (#1087)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.14.4
  dependency-type: indirect
  dependency-group: uv
- dependency-name: aiohttp
  dependency-version: 3.14.4
  dependency-type: indirect
  dependency-group: uv
- dependency-name: aiohttp
  dependency-version: 3.14.4
  dependency-type: indirect
  dependency-group: uv
- dependency-name: aiohttp
  dependency-version: 3.14.4
  dependency-type: indirect
  dependency-group: uv
- dependency-name: aiohttp
  dependency-version: 3.14.4
  dependency-type: indirect
  dependency-group: uv
- dependency-name: aiohttp
  dependency-version: 3.14.4
  dependency-type: indirect
  dependency-group: uv
- dependency-name: aiohttp
  dependency-version: 3.14.4
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 1b86549)

* fix(flags): compare numeric string properties numerically (#1088)

(cherry picked from commit 63b5dcc)

* chore: Release v7.73.2 [skip ci]

(cherry picked from commit cb7580e)

* Update generated references

(cherry picked from commit 1222ac7)

* fix(ai): keep the openai.Stream API on sync OpenAI streams (#1062)

Sync chat completions and Responses streams returned a bare generator, so
`with`, `close()` and attributes such as `.response` were lost. Wrap them
with _StreamWrapper, as #804 did for Anthropic and #645 did for async.

Co-authored-by: Dustin Byrne <dustin@posthog.com>
(cherry picked from commit f70b3cc)

* fix(ai): take cumulative Anthropic stream usage from message_delta (#1074)

Anthropic's message_delta usage is cumulative. With server tools (web
search, web fetch, code execution) its input and cache counts exceed
message_start's, but the stream accumulator only recorded message_start's
input and cache counts, so streamed generations under-reported input
tokens and cache writes. Keep the larger of the two for each count.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 57d1607)

* chore: Release v7.73.3 [skip ci]

(cherry picked from commit 24c6722)

* Update generated references

(cherry picked from commit bf87352)

* feat(llma): add datasets to offline evaluations (#1090)

* feat(llma): add datasets to offline evaluations

* chore(llma): share evaluation request and response helpers

* fix(llma): address dataset sdk review feedback

(cherry picked from commit b8879a7)

* chore: Release v7.74.0 [skip ci]

(cherry picked from commit ac7fa99)

* Update generated references

(cherry picked from commit b14dd9e)

* feat(django): add request context to OpenTelemetry spans (#1070)

* feat(django): add request context to OpenTelemetry spans

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96

* fix(tracing): preserve span attributes and active context values

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96

* chore: use current tracing header settings in span checks

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96

* fix(tracing): respect disabled context owners

Generated-By: PostHog Desktop
Task-Id: bfd18848-e498-4352-a58a-b9ba85de4198
(cherry picked from commit 5f01fe9)

* fix: enrich Flask, ASGI, and Celery spans with context (#1075)

* fix(flask): add request context to active OpenTelemetry spans

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96

* fix(asgi): enrich active request spans

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96

* fix(celery): enrich active task spans with job context

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96

* chore: use current tracing header settings in framework checks

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96

* fix(celery): preserve caller span identity during eager tasks

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96

* chore(tests): use enabled clients in span tests

Generated-By: PostHog Desktop
Task-Id: bfd18848-e498-4352-a58a-b9ba85de4198
(cherry picked from commit 5ddb446)

* chore: Release v7.75.0 [skip ci]

(cherry picked from commit 908c77b)

* Update generated references

(cherry picked from commit a5b93ee)

* fix(flags): match explicit null properties with string operators (#1092)

(cherry picked from commit e9056e3)

* chore: Release v7.75.1 [skip ci]

(cherry picked from commit 361ca83)

* Update generated references

(cherry picked from commit 5e296d8)

* fix(ai): capture OpenAI chat custom tool calls instead of raising (#1098)

A custom tool call has `custom` instead of `function`, so formatting the
response raised AttributeError from the wrapped create(), after the provider
call had succeeded. Read the name and input from `custom`, matching
PostHog/posthog-js#5251.

(cherry picked from commit 4697e6f)

* chore: Release v7.75.2 [skip ci]

(cherry picked from commit e63a898)

* Update generated references

(cherry picked from commit e9710ec)

* fix(ai): record mid-stream errors on streamed OpenAI, Anthropic and Gemini events (#1094)

* fix(ai): record mid-stream errors on streamed OpenAI, Anthropic and Gemini events

capture_streaming_event hard-coded $ai_http_status=200, and the stream
generators only had a finally block. When the provider stream raised after
the first chunk (overloaded, connection reset), the event was sent as a
success with no $ai_is_error/$ai_error, unlike the non-streaming path.

Catch the exception in each sync and async generator, pass it through
StreamingEventData, and re-raise unchanged. The event now carries
$ai_is_error, $ai_error and the error's status code (0 when it has none).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(ai): pass the stream error to StreamingEventData directly and update the API snapshot

mypy 1.16 rejected splatting {"error": error} into StreamingEventData(...)
(typeddict-item). The field accepts None and capture_streaming_event reads it
with .get(), so pass error=error. Regenerate the public API snapshot for the new
StreamingEventData.error field.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit dc2a2b4)

* fix(ai): keep the refusal of a non-streamed OpenAI chat completion in the captured output (#1095)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 42c8b7b)

* fix(ai): record an integer $ai_http_status in the LangChain callback handler (#1096)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit a6ddc2e)

* chore: Release v7.75.3 [skip ci]

(cherry picked from commit dc01266)

* Update generated references

(cherry picked from commit 9185524)

* fix(ai): keep latest cumulative output tokens across message_deltas (#1100)

* fix(ai): keep latest cumulative output tokens across message_deltas

Server tools such as web search can emit several message_delta events in
one Anthropic stream, and each delta's usage is cumulative. The stream
accumulator added every delta's output_tokens together, so a stream with
two deltas reporting 50 then 110 cumulative tokens captured 160. Keep
the latest cumulative value, matching how the input and cache counts are
already handled and how the Anthropic SDK accumulates usage.

* style(ai): format cumulative token async regression test

---------

Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
(cherry picked from commit 34ec4f8)

* chore: Release v7.75.4 [skip ci]

(cherry picked from commit 84664b4)

* Update generated references

(cherry picked from commit e17a8be)

* fix: serialize non-finite floats as null in JSON payloads (#1081)

* fix: serialize non-finite floats as null in JSON payloads

Added a helper function to replace non-finite floats with None in JSON serialization.

* test: cover non-finite float serialization in batch payloads

* chore: refresh public API snapshot for DatetimeSerializer.encode

(cherry picked from commit 18b39e8)

* fix: annotate client timeouts as float (#1106)

`timeout` and `feature_flags_request_timeout_seconds` were typed (or
inferred) as `int`, so type checkers rejected fractional values even
though they go straight to requests/httpx, which accept floats.

Fixes #1089

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b3fcec6)

* chore: Release v7.75.5 [skip ci]

(cherry picked from commit a774223)

* Update generated references

(cherry picked from commit 8c2d8f6)

* fix(flags): retain tracking metadata in local evaluation snapshots (#1041)

(cherry picked from commit a95331f)

* chore: Release v7.75.6 [skip ci]

(cherry picked from commit a5909dc)

* Update generated references

(cherry picked from commit 4b280fc)

* chore(deps): bump uv to 0.13.4 and certifi to 2026.10.19 (#1104)

Bump uv to 0.13.4 and certifi to 2026.10.19

Eight eMudhra intermediate CA private keys were broken, so anyone
holding them can impersonate TLS servers to clients that still trust
emSign ECC Root CA - G3. uv 0.13.4 and certifi 2026.10.19 are the first
releases whose bundled CA store drops that root, and the versions pinned
here still trust it. See the Python.org notice.
https://discuss.python.org/t/information-for-python-users-regarding-emudhra-emsign-ca-certificate-security-incident/109576

Claude-Session: https://claude.ai/code/session_01T4ck1tYoQv3mzBTXH17asD

Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit 7651b10)

* test: send NaN and Infinity as null on the v1 path, as #1081 does on main

#1081 keeps an event whose properties hold a non-finite float and writes
the value as null. The v1 before_send test expected that event to be
dropped; a capture v1 body test now pins the null on the wire, and the
migration guide says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Sebastian Turner <sebastiant3484@gmail.com>
Co-authored-by: Daniel Visca <daniel.v@posthog.com>
Co-authored-by: releaser-posthog-python[bot] <262414804+releaser-posthog-python[bot]@users.noreply.github.com>
Co-authored-by: Carlos Marchal <carlos@posthog.com>
Co-authored-by: Juraj Majerik <juro.majerik@gmail.com>
Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dustin Byrne <dustin@posthog.com>
Co-authored-by: Georgis Andonis <6430745+gesh@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Radu Raicea <radu@raicea.com>
Co-authored-by: Sebastian Turner <sebastiant3484@gmail.com>
Co-authored-by: mosafariuk <mosafariuk@gmail.com>
Co-authored-by: alexcastgonz <82875882+pcbeingused333@users.noreply.github.com>
Co-authored-by: Paul D'Ambra <paul.dambra@gmail.com>
Co-authored-by: Harsh Kashyap <55448981+Harsh23Kashyap@users.noreply.github.com>
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
Co-authored-by: Anna Garcia <11654201+turnipdabeets@users.noreply.github.com>
Co-authored-by: Georges-Antoine Assi <3247106+gantoine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants