Skip to content

feat: add captureAi on the capture V1 AI endpoint - #939

Open
turnipdabeets wants to merge 9 commits into
feat/v4-capture-v1from
feat/v4-capture-ai
Open

turnipdabeets wants to merge 9 commits into
feat/v4-capture-v1from
feat/v4-capture-ai

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Adds captureAi(...), which sends AI observability events ($ai_generation, $ai_span, $ai_trace, $ai_embedding, ...) to the capture V1 AI endpoint (POST /i/v1/ai/events). That endpoint accepts events up to 8 MiB. It follows the capture_ai lanes in posthog-rs (PostHog/posthog-rs#250), posthog-go (PostHog/posthog-go#323) and posthog-python (PostHog/posthog-python#1017, #1022), and Ingestion's goal of shipping captureAi with every V1 release. Stacked on #938.

  • captureAi takes the same arguments as capture and goes through the same enrichment and beforeSend. Only the delivery route differs. capture never reroutes $ai_* events.
  • It uses the capture V1 transport from feat!: send analytics events through capture v1 #938: Bearer auth, V1 headers, per-event results, retry identity, same-origin redirects and the Retry-After cap. Retry identity is now tracked per endpoint, so the two lanes don't reset each other's attempt numbers. A 404 is terminal for AI: there is no /batch fallback.
  • AI events go to their own disk queue (posthog.aiFolder). It is flushed by flush() and close(), and created with the other queues so events left on disk by a previous launch still send.
  • captureAi returns the queued event's UUID (@discardableResult), or nil when the event isn't queued: opted out, disabled, dropped by beforeSend, or over the size limit.
  • The queue batches by bytes as well as count, with a 5 MiB soft limit (new internal QueueEndpoint.maxBatchBytes). It drops an event over 8 MiB + 64 KiB when it is queued, logging only the event name and size. The queue also flushes as soon as it holds 5 MiB, so large events are sent before the disk cap evicts them, and a flush reads about one batch from disk at a time.
  • The queue keeps at most 100 events and 50 MiB on disk, dropping the oldest first (new internal QueueEndpoint.maxQueueBytes, enforced in PostHogFileBackedQueue on add and on load). Without the byte cap, 100 events at the size limit could take ~800 MiB.
  • The compliance adapter adds POST /capture_ai and advertises capture_ai_v1.

Differences from the spec and the other SDKs

The capture-ai spec only applies to server SDKs. Where iOS differs:

  • No uuid argument: iOS capture doesn't take one either. captureAi does return the UUID, like the spec.
  • Queue created at setup, not lazily: an offline mobile queue has to send events persisted by an earlier launch.
  • Queue limit is 100 events and 50 MiB, not 1000 events: keeps the disk use of multi-MB events down on devices.
  • Gzip follows config.compression: the server SDKs send AI requests uncompressed by default.

💚 How did you test it?

  • New Swift Testing suite "Capture V1 AI lane", using MockPostHogServer's new /i/v1/ai/events stub:
    • routing by method, including beforeSend and options
    • a 404 is terminal, with no /batch request
    • an oversized event is dropped when queued
    • requests split at the byte limit
    • captureAi returns the sent event's UUID, and nil for an event that isn't queued
    • the queue flushes once a batch's worth of bytes is queued, before flushAt
  • PostHogFileBackedQueue tests for the byte cap (evicts the oldest, keeps an oversized newest record, trims on load) and for a byte-bounded read.
  • Removing the 404 path guard or the byte limit makes the matching test fail.
  • CI passes after rebasing onto main, including test-ios-simulator, test-no-traits and Public API snapshot.
  • Locally, make lint passes, and make test on the top of the stack (feat!: remove the /batch fallback from capture V1 #940) ran 1185 tests. The only failures are the 13 known group-container issues, which also fail on the base branch locally.
  • SDK compliance harness 1.14.0 in CI, capture_ai_v1 suite: 97/101. The 4 failures:
    • capture_ai_keeps_supplied_uuid: captureAi doesn't take a UUID, like capture. The test aborts before flushing, so its request lands in the next test and fails event_has_required_root_fields_batch too.
    • max_retries_respected and unset_options_omitted: the same mobile failures as in the analytics suite.
  • Not done: docs for captureAi on posthog.com / LLM analytics.

📝 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 pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Built with Claude Code. The design follows the sdk-specs capture-ai spec, the V1 AI route in rust/capture/src/v1, and the posthog-rs, posthog-go and posthog-python AI lanes. Maintainer decisions: a separate AI lane with a small disk queue and no /batch fallback, PR'd separately from #938.

🤖 Generated with Claude Code

@turnipdabeets
turnipdabeets added this pull request to stack #941 October 10, 2026 15:23
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-10-23T22:51:30.378361+00:00
Duration: 940219ms

⚠️ Some Tests Failed

213/219 tests passed, 6 failed


Capture_V1 Tests

⚠️ 99/101 tests passed, 2 failed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 3118ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2873ms
Required Headers.Has Authorization Bearer Header ✅ 2796ms
Required Headers.Has Content Type Json ✅ 2924ms
Required Headers.Has Posthog Sdk Info Format ✅ 2754ms
Required Headers.Has Posthog Attempt Header ✅ 2900ms
Required Headers.Has Posthog Request Id ✅ 404ms
Required Headers.Has Posthog Request Timestamp ✅ 435ms
Required Headers.Has User Agent ✅ 417ms
Body Format.Body Has Created At And Batch ✅ 2875ms
Body Format.No Api Key In Body ✅ 2879ms
Body Format.No Sent At In Body ✅ 2895ms
Event Format.Event Has Required Root Fields ✅ 2846ms
Event Format.Event Uuid Is Valid ✅ 2752ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2725ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2791ms
Event Format.Distinct Id Is String ✅ 2797ms
Event Format.Distinct Id At Root Not Properties ✅ 342ms
Event Format.Custom Properties Preserved ✅ 3050ms
Event Format.Set Properties Preserved ✅ 2820ms
Event Format.Set Once Properties Preserved ✅ 2776ms
Event Format.Groups Properties Preserved ✅ 2855ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 474ms
Event Format.Event Has Required Root Fields Batch ✅ 2906ms
Event Format.Event Uuid Is Valid Batch ✅ 3037ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2964ms
Event Format.Distinct Id Is String Batch ✅ 2834ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2796ms
Event Format.Custom Properties Preserved Batch ✅ 3023ms
Event Format.Set Properties Preserved Batch ✅ 2935ms
Event Format.Set Once Properties Preserved Batch ✅ 2990ms
Event Format.Groups Properties Preserved Batch ✅ 3220ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2908ms
Batch Behavior.Multiple Events In Single Batch ✅ 3050ms
Batch Behavior.Batch Envelope Smoke ✅ 3017ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 313ms
Batch Behavior.Flush At Triggers Batch ✅ 1244ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2805ms
Deduplication.Generates Unique Uuids ✅ 3229ms
Deduplication.Different Events Same Content Different Uuids ✅ 2872ms
Deduplication.Preserves Uuid On Retry ✅ 9307ms
Deduplication.Preserves Timestamp On Retry ✅ 9260ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7915ms
Deduplication.No Duplicate Events In Batch ✅ 2989ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2763ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16710ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9285ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7584ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 9290ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2875ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2847ms
Response Format Validation.Success No Retry After When All Ok ✅ 2931ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2844ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2852ms
Response Format Validation.Response Echoes Request Id ✅ 551ms
Retry Behavior.Retries On 408 ✅ 9202ms
Retry Behavior.Retries On 500 ✅ 9190ms
Retry Behavior.Retries On 503 ✅ 11843ms
Retry Behavior.Retries On 504 ✅ 9153ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6308ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10794ms
Retry Behavior.Does Not Retry On 400 ✅ 2378ms
Retry Behavior.Does Not Retry On 401 ✅ 4799ms
Retry Behavior.Does Not Retry On 402 ✅ 5086ms
Retry Behavior.Does Not Retry On 413 ✅ 2412ms
Retry Behavior.Does Not Retry On 415 ✅ 2356ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4737ms
Retry Behavior.Implements Backoff ✅ 21813ms
Retry Behavior.Max Retries Respected ❌ 21867ms
Partial Batch Handling.Handles 200 Full Success ✅ 2383ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5908ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5890ms
Partial Batch Handling.Does Not Retry Limited Events ✅ 5834ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7955ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7923ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7871ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7853ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7707ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7928ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7736ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5899ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry ✅ 5861ms
Compression.Sends Gzip Content Encoding ✅ 2764ms
Compression.No Content Encoding When Disabled ✅ 2665ms
Compression.Compressed Body Is Decompressible ✅ 2735ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4770ms
Event Options.Cookieless Mode Override ✅ 2762ms
Event Options.Disable Skew Correction Override ✅ 2784ms
Event Options.Process Person Profile Override ✅ 443ms
Event Options.Product Tour Id Override ✅ 2875ms
Event Options.Unset Options Omitted ✅ 2830ms
Event Options.Options Override In Batch ✅ 2853ms
Event Options.Unknown Option Passes Through ✅ 2781ms
Event Options.Option Value Is Not Converted ✅ 2968ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2875ms
Event Options.Option Wins Over Legacy Property ✅ 2770ms
Event Options.Legacy Property Fills Unset Option ✅ 2767ms
Event Options.Null Option Falls Back To Legacy Property ✅ 543ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2849ms
Geoip And Historical Migration.Historical Migration Set In Body ❌ 2903ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2834ms

Failures

retry_behavior.max_retries_respected

Expected 4 requests, got 5

geoip_and_historical_migration.historical_migration_set_in_body

Body missing 'historical_migration' field. Body keys: ['created_at', 'batch']

Capture_Ai_V1 Tests

⚠️ 97/101 tests passed, 4 failed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 461ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2897ms
Endpoint And Method.Capture Does Not Reroute Ai Named Events ✅ 2849ms
Required Headers.Has Authorization Bearer Header ✅ 2818ms
Required Headers.Has Content Type Json ✅ 399ms
Required Headers.Has Posthog Sdk Info Format ✅ 2763ms
Required Headers.Has Posthog Attempt Header ✅ 2901ms
Required Headers.Has Posthog Request Id ✅ 2808ms
Required Headers.Has Posthog Request Timestamp ✅ 2909ms
Required Headers.Has User Agent ✅ 2883ms
Body Format.Body Has Created At And Batch ✅ 2767ms
Body Format.No Api Key In Body ✅ 514ms
Body Format.No Sent At In Body ✅ 316ms
Event Format.Event Has Required Root Fields ✅ 2803ms
Event Format.Event Uuid Is Valid ✅ 2844ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2882ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2888ms
Event Format.Distinct Id Is String ✅ 2796ms
Event Format.Distinct Id At Root Not Properties ✅ 2705ms
Event Format.Custom Properties Preserved ✅ 2818ms
Event Format.Set Properties Preserved ✅ 2781ms
Event Format.Set Once Properties Preserved ✅ 415ms
Event Format.Groups Properties Preserved ✅ 2792ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2769ms
Event Format.Capture Ai Keeps Supplied Uuid ❌ 91ms
Event Format.Event Has Required Root Fields Batch ❌ 2835ms
Event Format.Event Uuid Is Valid Batch ✅ 2871ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2876ms
Event Format.Distinct Id Is String Batch ✅ 2722ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 409ms
Event Format.Custom Properties Preserved Batch ✅ 2770ms
Event Format.Set Properties Preserved Batch ✅ 2817ms
Event Format.Set Once Properties Preserved Batch ✅ 550ms
Event Format.Groups Properties Preserved Batch ✅ 2906ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2884ms
Batch Behavior.Multiple Events In Single Batch ✅ 2976ms
Batch Behavior.Batch Envelope Smoke ✅ 2967ms
Batch Behavior.Flush At Triggers Batch ✅ 1175ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2763ms
Deduplication.Generates Unique Uuids ✅ 3096ms
Deduplication.Different Events Same Content Different Uuids ✅ 2970ms
Deduplication.Preserves Uuid On Retry ✅ 5540ms
Deduplication.Preserves Timestamp On Retry ✅ 9304ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 8099ms
Deduplication.No Duplicate Events In Batch ✅ 2987ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2765ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 15844ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9325ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7947ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 5473ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2953ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2993ms
Response Format Validation.Success No Retry After When All Ok ✅ 3004ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2864ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2901ms
Response Format Validation.Response Echoes Request Id ✅ 401ms
Retry Behavior.Retries On 408 ✅ 9247ms
Retry Behavior.Retries On 500 ✅ 8864ms
Retry Behavior.Retries On 503 ✅ 11867ms
Retry Behavior.Retries On 504 ✅ 9296ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6264ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10920ms
Retry Behavior.Does Not Retry On 400 ✅ 4925ms
Retry Behavior.Does Not Retry On 401 ✅ 4789ms
Retry Behavior.Does Not Retry On 402 ✅ 4833ms
Retry Behavior.Does Not Retry On 413 ✅ 4901ms
Retry Behavior.Does Not Retry On 415 ✅ 4680ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4852ms
Retry Behavior.Implements Backoff ✅ 19275ms
Retry Behavior.Max Retries Respected ❌ 21232ms
Partial Batch Handling.Handles 200 Full Success ✅ 4910ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5885ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5851ms
Partial Batch Handling.Does Not Retry Limited Events ✅ 5879ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7868ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7838ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7875ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7943ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7898ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7879ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7859ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5804ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry ✅ 5948ms
Compression.Sends Gzip Content Encoding ✅ 2833ms
Compression.No Content Encoding When Disabled ❌ 2869ms
Compression.Compressed Body Is Decompressible ✅ 2829ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4826ms
Event Options.Cookieless Mode Override ✅ 2790ms
Event Options.Disable Skew Correction Override ✅ 2888ms
Event Options.Process Person Profile Override ✅ 2811ms
Event Options.Product Tour Id Override ✅ 2809ms
Event Options.Unset Options Omitted ✅ 2685ms
Event Options.Options Override In Batch ✅ 2814ms
Event Options.Unknown Option Passes Through ✅ 3002ms
Event Options.Option Value Is Not Converted ✅ 2747ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2817ms
Event Options.Option Wins Over Legacy Property ✅ 2653ms
Event Options.Legacy Property Fills Unset Option ✅ 336ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2663ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 366ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2654ms

Failures

event_format.capture_ai_keeps_supplied_uuid

Expected last action result '0198c0de-0000-7000-8000-000000000abc', got '01a16aef-b9a1-7ea9-b9fc-a5bc8d0a1874'

event_format.event_has_required_root_fields_batch

Expected 1 requests, got 2

retry_behavior.max_retries_respected

Expected 4 requests, got 5

compression.no_content_encoding_when_disabled

Header 'Content-Encoding' should be absent but has value 'gzip'

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2762ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2712ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2759ms
Request Payload.Flags Request Omits Authorization Header ✅ 2847ms
Request Payload.Token In Flags Body Matches Init ✅ 2818ms
Request Payload.Groups Round Trip ✅ 2815ms
Request Payload.Groups Default To Empty Object ✅ 2801ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2876ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2822ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2734ms
Request Lifecycle.No Flags Request On Init Alone ✅ 156ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2828ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5413ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2704ms
Retry Behavior.Retries Flags On 502 ✅ 2849ms
Retry Behavior.Retries Flags On 504 ✅ 2855ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 2942ms

@github-actions

Copy link
Copy Markdown
Contributor

This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the stale label – otherwise this will be closed in another week.

@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 86a175b: 5 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

Comment thread PostHog/PostHogSDK.swift Outdated
Comment thread PostHog/PostHogSDK.swift
Comment thread sdk_compliance_adapter/Sources/main.swift
Comment thread PostHog/PostHogQueue.swift
Comment thread sdk_compliance_adapter/Sources/main.swift Outdated
@turnipdabeets
turnipdabeets marked this pull request as ready for review October 20, 2026 17:37
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 20, 2026 17:37
Comment thread PostHog/QueueEndpoint+Factories.swift Outdated
/// Soft request size for the AI endpoint, matching the other PostHog SDKs.
let aiMaxBatchBytes = 5 * 1024 * 1024
/// AI events can be megabytes, so keep fewer of them on disk than analytics events.
let aiMaxQueueSize = 100

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Other v1 SDKs give the AI lane its own settings, separate from analytics: compression, queue size and (in Python and Go) a request timeout of at least 30 s, since AI events can be several MB and shouldn't share analytics' limits. Here the AI lane inherits compression, flushAt, maxBatchSize and the 10 s timeout, and hardcodes 100 events / 50 MiB. Could we add AI-specific settings, or document these as fixed mobile limits if that's the better fit for iOS?

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.

I didn't find AI-specific settings in the other SDKs: Node only has enableFullAiCapture, Python's AI lane uses the client's flush, batch, timeout and gzip settings, Go doesn't have AI capture on main yet, and the capture-ai spec only defines enable_full_ai_capture. Is that changing with the v1 work? If the other SDKs are adding AI settings, I'll add matching ones here.

For now, 3791af1 documents these as fixed mobile limits on captureAi. AI requests now time out after 60 s instead of 10 s. flushAt, maxBatchSize, flushIntervalSeconds and compression still come from the shared config. The 5 MiB request size, the send at 5 MiB queued, and the 100 events / 50 MiB disk caps are fixed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sorry for the confusion - these are present on Rust/Go/Python (soon node) v1 branches only as that's the only place we've added capture_ai explicit interfaces and pipelines yet. The idea is to be able to tune those submit pipelines explicitly for AI events.

Check those out and then let me know what you think. since they are all backend SDKs the needs of this one may differ

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.

Thanks, added in 4dd9dc5: captureAiCompression, captureAiMaxQueueSize, captureAiTimeout and captureAiMaxEventBytes on PostHogConfig, named after the backend ones. Defaults stay mobile-friendly: gzip, 100 events, 60 s, 8 MiB. captureAiMaxEventBytes can only lower the limit, and the 50 MiB disk cap stays fixed so a big queue can't fill the phone.

Comment thread PostHog/PostHogSDK.swift
Comment thread PostHog/PostHogQueue.swift
'posthog-ios': minor
---

Add `captureAi(...)` to send AI events such as `$ai_generation` to PostHog's AI endpoint (`/i/v1/ai/events`), which accepts events up to 8 MiB. It returns the event UUID.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The 4.0 guide has no section for captureAi yet. Can this PR add one covering the AI endpoint, the 8 MiB event limit, the 60-second timeout and the new 5 MiB analytics batch cap? How wrapper SDKs such as Flutter expose AI capture is still being worked out for all of them at once, so for now the guide can describe only the native iOS API.

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.

added in 5880463

@turnipdabeets
turnipdabeets force-pushed the feat/v4-capture-ai branch 5 times, most recently from cf4e222 to 1e4bdfd Compare October 23, 2026 18:36
turnipdabeets and others added 9 commits October 23, 2026 18:23
captureAi(...) takes capture's arguments, runs the same enrichment and
beforeSend, and queues the event on its own disk queue for
/i/v1/ai/events. It reuses the capture V1 transport (headers, per-event
results, retry identity per endpoint, redirects, Retry-After), with no
/batch fallback. The AI queue batches by bytes (5 MiB) as well as count,
keeps at most 100 events, and drops events over 8 MiB + 64 KiB logging
only the name and size. capture() never reroutes $ai_ events.

The compliance adapter gains /capture_ai and the capture_ai_v1 capability.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The AI queue now drops its oldest events once they take more than 50 MiB
on disk, on top of the 100-event limit. captureAi returns the queued
event's UUID, or nil when the event isn't queued.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…lose()

- close() flushes the AI queue along with the others, and aiQueue is guarded
  by setupLock like the other queues.
- The AI queue flushes once a batch's worth of bytes (5 MiB) is queued, so
  multi-MB events are sent before the 50 MiB cap evicts them.
- A flush reads about one byte-sized batch from disk instead of the whole
  window, so large AI events aren't all held in memory at once.
- The compliance adapter's /capture_ai accepts bodies up to 9 MB and passes
  the harness timestamp.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The harness reads the action result's success field, as posthog-python's
adapter returns it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Capture V1 rejects requests over 10 MiB, and analytics batches were capped
by count only. Give the analytics endpoint the same 5 MiB soft byte limit
as the AI lane (renamed to captureV1MaxBatchBytes). An event over the limit
is still sent alone, and 413 halving is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Document the AI lane's fixed limits on captureAi.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Add captureAiCompression, captureAiMaxQueueSize, captureAiTimeout and
captureAiMaxEventBytes, matching the AI lane settings in the backend v1
SDKs. Defaults keep the current behavior: gzip, 100 events, 60 s, 8 MiB.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

@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 5ba4a9a: 1 Should fix.

encode: { event in
guard let data = toJSONData(event.toJSON()) else { return nil }
// Name and size only: AI properties can hold prompts and media.
if data.count > maxEventBytes {

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 · Enforce the configured properties limit separately from envelope overhead

With captureAiMaxEventBytes = 1024, an event containing 32 KiB of properties still queues and returns a UUID: maxEventBytes includes a 64 KiB envelope allowance that properties can consume. This violates the public option's documented properties cap and defeats callers' lower payload limits. Validate serialized properties against the configured cap separately.

This branch has not been deployed

No deployments
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.

3 participants