Skip to content

chore: merge main into v1 (DO NOT SQUASH, lands by fast-forward) - #444

Merged
eli-r-ph merged 78 commits into
v1from
eli/v1-sync-main-20261111
Nov 12, 2026
Merged

eli-r-ph merged 78 commits into
v1from
eli/v1-sync-main-20261111

Conversation

@eli-r-ph

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

v1 is missing main's 1.40.0-1.42.4: transactional email (Client.Emails), posthogmcpgo, the MCP spec alignment, flags fixes and the Markdown reference. This repo only squash-merges, and squashed syncs leave the merge base with main at October, so the v2 release PR (#320) keeps conflicting (48 files).

Warning

Do not squash or merge this PR with the button. After approval it lands by fast-forwarding v1 to this exact head, which keeps the merge commit. Squashing drops it, and #320 keeps conflicting.

Changes:

  • 253b8bd is a real merge of main (cbf49d4). v1 already had main up to 1.39.0 from the squashed feat: sync main 1.39 into v1 #405, so every file is merged against main@1.39.0, not the old base. A plain merge brought back 1.x tests in capture_test.go and duplicate MCP declarations that v1 had removed. The commit message lists each resolution; git show --remerge-diff 253b8bd shows them.
  • 3b35af9 ports fix: keep events when a property is NaN or Inf #433 to v1's encoder, so v1 serializes like main. Properties are normalized and each event checked as JSON before it is queued.
    • Before this, a float32 NaN or Inf alone made the request invalid JSON, and capture refused the whole batch (post-release item 18).
    • Exception custom properties use encoding/json again, which honors omitzero.
    • Cost per event: typical 5.8 to 10.5 µs, large AI event 226 to 1,089 µs. main pays the same.
  • a8c5c51 adds a test that a failed final attempt reports at once on both lanes, which fix: bound legacy capture retries #427 fixed on main.
  • 4fd41d2 publishes posthogmcpgo as posthogmcpgo/v2, and 34b7bc4 adds posthogmcpgo.WithEventOptions, matching posthogmcpsdk.
  • 7456de6 regenerates the 2.x and migration references with --major 2.

💚 How did you test it?

  • Every file that differs from v1 was changed on main after 1.39.0.
  • api/public-api.txt adds 80 lines and removes none, all in main's snapshot.
  • Race tests, vet and gofmt pass in all four modules.
  • New tests: float32 NaN alone on all four message types, a NaN keeping the rest of the batch, invalid custom JSON failing only its own event, and the event options cases. Each fails when its fix is reverted.
  • Merging this head into current main has 0 conflicts.

📝 Checklist

  • All commits are signed
  • 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.

🤖 Generated with Claude Code

lucasheriques and others added 30 commits October 1, 2026 20:51
)

* fix(flags): quote semver values in local-evaluation errors with %q

CodeQL's go/unsafe-quoting flags four semver errors that wrap a
JSON-derived property value in hand-written single quotes, so a value
containing a quote could break out of them. Every open PR fails CodeQL
on these lines. All eight semver messages now use %q, so the message
shows the value escaped and the four sibling messages stay consistent.

Tested: go build ./... and go test ./... pass; no test asserts the
message text.

* test(flags): assert semver errors escape quoted values

Covers the property and flag value branches of each semver operator family
with a value holding both quote kinds. All eight cases fail against the old
hand-quoted messages.

Tested: go test ./...
…t-required events (#345)

* feat(mcp): send $lib posthog-go-mcp on MCP analytics events

Add an opt-in Library field to Capture and Exception. When set, it replaces
SDKName as the event's $lib (and the top-level library on legacy wire
shapes); $lib_version stays posthog-go's version. posthogmcp sets it to
posthog-go-mcp on $mcp_tool_call and $exception, so MCP events carry their
own library name without relabelling the host app's other events, per the
mcp-analytics spec's "Library identity" requirement. A message without the
field serializes exactly as before.

Tested:
- go build ./... && go vet ./... && go test -race -count=1 ./... (root): all ok, no reruns needed
- posthogmcpsdk: go test -race -count=1 ./...: ok
- bin/api-diff: up to date (two added fields)
- New table tests: Library set/unset x Capture/Exception x caller $lib; a
  mixed legacy batch (Capture, Exception, Identify) keeps each $lib and
  BeforeSend sees the field; v1 mode records header and body; posthogmcp
  sends posthog-go-mcp even when ToolCall.Properties carries $lib.
- BenchmarkAPIfy, 10 interleaved runs, origin/main vs head, median ns/op:
  low 3442 -> 3458 (+0.5%), medium 28738 -> 28728 (0.0%), high 138424 -> 138470 (0.0%)
- Spec probe suite (copy, replace -> this worktree) vs the same suite on
  origin/main: checker output identical except the 1208 "$lib does not end
  with -mcp" violations, now 0; probe cases C1 (every era) and M9 flip
  FAIL -> PASS, nothing regresses.

Reviewer notes:
- API shape: an explicit field, not "a caller-supplied $lib property wins".
  Letting the property win would change $lib for any existing user who
  passes $lib today; the field is opt-in and invisible when empty.
- CaptureModeAnalyticsV1 cannot honour Library: the server takes $lib from
  the per-request PostHog-Sdk-Info header and stamps it over any body $lib,
  and one request mixes events. MCP events in v1 mode still report
  posthog-go. Fixing that means splitting v1 batches per library in
  capture_v1_send.go, left out of this change.

* feat(mcp): capture $mcp_unknown_tool and $mcp_input_required from posthogmcp

Add CaptureUnknownTool and CaptureInputRequired to the manual API. A call that
names an unregistered tool, and an input_required round the client receives,
are not tool calls per the mcp-analytics spec, so they get their own events
built on the same prepare, sanitize, and size-bound path as CaptureToolCall,
with the shared identity properties factored into setIdentityProperties.

Tested:
- go build ./... && go vet ./... && go test -race -count=1 ./posthogmcp/ . : ok
- posthogmcpsdk (replace -> ../): go build and go test -race: ok
- bin/api-diff: up to date

Reviewer notes: the go-sdk adapter that sends these events follows in a PR
that bumps its posthog-go requirement once this is released.

* fix(mcp): send $lib posthog-go-mcp on the optional events

CaptureUnknownTool and CaptureInputRequired built their Capture without
Library, so they reported posthog-go while the spec's Library identity rule
covers every MCP event. The identity test now checks the serialized $lib.
posthogmcp's package doc also says a v1 capture mode client reports one
library name per request and needs a dedicated client to separate MCP events.

Tested: go test -race -count=1 ./posthogmcp/ (failed first on $lib, then ok).

* fix(mcp): drop an oversized $set before failing an optional event

CaptureUnknownTool and CaptureInputRequired failed when SetProperties alone
pushed the event past 102,400 bytes, while CaptureToolCall falls back to the
required properties. They now try custom properties, then $set, before
giving up. The package doc also says a v1 capture mode client reports
posthog-go on MCP events, since the library name comes from the SDK there.

Tested: go build, go vet, go test -race -count=1 ./... (root), posthogmcpsdk
tests, bin/api-diff: ok. The new $set cases failed before the change.

* chore(mcp): refresh the public API snapshot for the posthogmcp doc

* fix(mcp): redact credentials in $mcp_input_request_methods

CaptureInputRequired truncated each method string but did not sanitize it, so
a caller-provided method containing a recognizable credential reached the
event unchanged. Each method now goes through sanitizeString first.

Tested: go test -race -count=1 ./posthogmcp/ (the new case failed first), bin/api-diff.

* fix(mcp): give a sessionless call a session and drop an empty-object intent

CaptureToolCall sent no $session_id when the caller passed neither a
SessionID nor a conversation handle, and sent an intent of "{}" as a real
intent. The spec requires a $session_id on every call, with distinct_id
falling back to it, and treats an empty context as no intent. A call without
a session now gets a fresh ses_<UUIDv7>, and "{}" is omitted like a blank
intent. Found by the spec probe suite (cases M4, M7, X6).

Tested: go test -race -count=1 ./posthogmcp/ (the new and updated cases fail
without the change), posthogmcpsdk go vet and tests, bin/api-diff.
…sdk (#344)

* feat(mcp): capture the calling model and client headers in posthogmcpsdk

Requires posthog-go v1.29.0, whose ToolCall carries LLMModel,
LLMModelSource, ClientUserAgent, and VendorClient, and resolves the TODO
in observe.

- Model: request _meta io.modelcontextprotocol/aiInvocation.model, then
  x-codex-turn-metadata.model (client_metadata), else an injected required
  llm_model argument (self_reported). Blank or "unknown" names no model.
  WithCaptureModel(false) turns off both sources and the injection.
- llm_model is injected, stripped before dispatch, and kept out of
  $mcp_parameters exactly like context, through one analyticsArgument
  table, so a tool's own llm_model stays its data.
- User-Agent and X-Anthropic-Client from RequestExtra.Header on HTTP.

Tested: go test -race ./... in posthogmcpsdk (go-sdk v1.6.1), new
parameterized TestInstrumentCapturesModel, TestStreamableHTTPCapturesClientHeaders,
and TestInstrumentAdvertisesAnalyticsArguments.

* fix(mcp): report the error a typed mcp.AddTool handler returned

go-sdk turns a typed handler's error into an isError result and keeps the
original on the result. The adapter read only the result text, so a handler
that returned an UpstreamTimeout was recorded as error type Error, and the
$exception lost its type, while Server.AddTool handlers kept theirs. It now
uses CallToolResult.GetError() when set, so both kinds report the same way.
Found by the spec probe suite (case C3, typed handler).

Tested: posthogmcpsdk go vet and go test -race -count=1 ./... (the typed
handler case failed first); a plain error and an error result keep type
Error.
* feat(mcp): anchor conversations in posthogmcpsdk, on by default

A stateless HTTP request carries no session, so each call of one agent
conversation got its own $session_id. Following the cross-SDK spec, the
adapter now advertises an optional conversation_id argument and:

- keeps an echoed handle only if it is UUIDv7-shaped, lowercased; it wins
  over a transport session, sets ToolCall.ConversationID, and core derives
  $session_id from it (ses_ + two FNV-1a lanes, the spec vector).
- mints a UUIDv7 only when the call has neither a valid handle nor a
  transport session (an Mcp-Session-Id, or a one-client connection that
  go-sdk serves without RequestExtra), and appends it to the result once
  as a {"conversation_id":...} text block, isError results included.
- mirrors the handle into structuredContent._mcp_instructions on every
  result of tools whose plain-object output schema it declared that on.
- forgets a minted handle the result cannot carry (protocol error, or a
  go-sdk v1.8 input_required round, detected by field name so v1.6.1
  still builds), so no event names a conversation the agent never got.
- strips conversation_id before validation and $mcp_parameters; a tool's
  own conversation_id stays its data. WithConversationID(false) opts out.

Error text and $mcp_response are read from the tool's own result, before
the handle block is appended.

Tested in posthogmcpsdk against go-sdk v1.6.1 and v1.8.0 (go get + tidy,
then restored): go build, go vet, staticcheck, go test -race ./... all
pass; the input_required test skips on v1.6.1 and fails on v1.8.0 when
the guard is removed. Pinned posthog-go v1.29.0 (replace dropped) builds
and vets on both.

* fix(mcp): anchor stateless clients that drop Mcp-Session-Id in posthogmcpsdk

Review fixes on conversation anchoring:
- A stateless go-sdk server gives a request without Mcp-Session-Id a random
  session id per request. Only a one-client connection or a request that sends
  the header now counts as a transport session, so such clients get a
  conversation handle and a fresh ses_<UUIDv7> instead of a hash of a random id.
- A handler error sends no result, so a minted handle is neither delivered
  nor recorded.
- Only a type "object" output schema gets _mcp_instructions; array and untyped
  schemas (allowed by go-sdk v1.8) are left alone.
- Skip session resolution when a handle sets $session_id.
- README: the handle rides structured results only when one exists, and
  $mcp_response is the tool's own result without the handle.

Tested: new table rows failed first (dropped session id on v1.6.1, handler
error with result, array/untyped output schema on v1.8.0), then pass. On
go-sdk v1.6.1 and v1.8.0: go vet, go test -race -count=1 ./..., staticcheck
clean; go.mod/go.sum restored to v1.6.1.

Reviewer note: an input_required round still emits one $mcp_tool_call, which
the spec says it should not; the test now pins that count of 1.

* fix(mcp): keep untyped input schemas extensible and leave constrained output schemas alone

withArguments accepted an input schema with properties but no explicit type
before the conversation work required type object for every schema, so such
tools lost their context, llm_model, and conversation_id arguments. Input
schemas may omit the type again. An output schema still must declare type
object, and now also must not set maxProperties or propertyNames, because
adding _mcp_instructions to a delivered result would break the schema the
client validates it against.

Tested: posthogmcpsdk go vet and go test -race -count=1 ./... ; the new
table cases for untyped inputs and constrained outputs failed first.
…gmcpsdk (#347)

* feat(mcp): send $mcp_unknown_tool and $mcp_input_required from posthogmcpsdk

The go-sdk adapter no longer counts an unregistered-tool call or an
input_required round as $mcp_tool_call. Unknown tools are recognised by
go-sdk's own unknown tool error, an echoed conversation handle still joins
the $mcp_unknown_tool event, and a legacy call that exhausts go-sdk's shim is
a failed call with $mcp_error_type input_required.

Tested: posthogmcpsdk go vet and go test -race on go-sdk v1.6.1 and v1.8.0
(round tests skip on v1.6.1), root go vet and posthogmcp tests, bin/api-diff,
probe cases X1 and C15 across eras.

Reviewer notes: depends on the core PR (#345) being
released; the adapter's posthog-go requirement is bumped to that release
before this leaves draft.

* fix(mcp): tell a forwarded unknown tool error and a failed round from the real thing

A registered tool that forwards another tool's unknown tool error was
recorded as $mcp_unknown_tool, losing the failed call. The adapter now
requires go-sdk's message to name the requested tool. A result that asks for
input next to an error is a failed call, not a round the client receives.

Tested: posthogmcpsdk go vet and go test -race -count=1 ./... on go-sdk
v1.6.1 and v1.8.0 (the new cases failed first on v1.8.0); root posthogmcp
tests; bin/api-diff.

* chore(mcp): require posthog-go v1.30.0 in posthogmcpsdk

The adapter needs the manual events API from #345, released in v1.30.0.

Tested: CI's build, vet and go test -race against the release with the replace dropped; go vet and go test -race in posthogmcpsdk.
* feat(mcp): add CaptureMissingCapability to posthogmcp

Sends $mcp_missing_capability for an agent's report of a capability the server lacks, per the mcp-analytics spec: $mcp_resource_name is the virtual tool's name, the report is $mcp_intent (source context_parameter), and the event carries the model, conversation, session and identity properties but no $mcp_tool_name, $mcp_parameters or $mcp_duration_ms. buildOptionalEvent no longer sets $mcp_tool_name itself; $mcp_unknown_tool and $mcp_input_required pass it.

Tested: go vet ./..., go test -race ./posthogmcp/..., bin/api-diff.

* chore(mcp): drop comments that restate the code

Remove the setModelAndIntentProperties doc and trim two added doc comments.
* docs: Refresh and simplify SDK agent guidance

* docs: Match pinned-core validation flags to CI
…osthogmcpsdk (#351)

* feat(mcp): add the opt-in get_more_tools missing-capability tool to posthogmcpsdk

WithMissingCapabilityTool advertises a get_more_tools virtual tool on the last tools/list page unless the server registers that name. It takes the enabled llm_model and conversation_id arguments. A call to it is answered by the middleware with the canned text and sends $mcp_missing_capability (intent = context), never $mcp_tool_call or $mcp_unknown_tool, also when the catalog lookup failed. Off by default.

go.mod pins posthog-go to the head of #350, which adds CaptureMissingCapability. Replace it with the release that carries #350 before merging.

Tested: go vet and go test -race in posthogmcpsdk on go-sdk v1.8.0 and v1.6.1; CI's build and vet against the pinned posthog-go with the replace dropped.

* fix(mcp): send the missing-capability call down the middleware chain

The middleware answered the virtual tool before calling next, so authentication and rate limiting installed inside Instrument never saw it, and a server tool of that name could be shadowed when the catalog lookup failed. The call now goes to next, and the middleware replies itself only when go-sdk answers it as an unknown tool, which makes go-sdk's own registry the source of truth.

Tested: go vet and go test -race in posthogmcpsdk; the two new tests (inner middleware rejects the call; server tool of that name with a failing listing) fail without the change; CI's pinned build with the replace dropped.

* chore(mcp): drop comments that restate the code

Remove added comments that repeat names or code; keep the go-sdk unknown-tool note and the exported option doc.

* chore(mcp): require posthog-go v1.32.0 in posthogmcpsdk

v1.32.0 carries CaptureMissingCapability (#350), which the adapter calls.

Tested: go vet and go test -race in posthogmcpsdk; CI's build, vet and go test against v1.32.0 with the replace dropped.
…test-sdk-action.yml from 03d972e49be84402c491324320b0a0f38c2ddc53 to 27806196fa5c5ac74e9e7500bc46b74db1869f3a in the github-actions group (#364)

chore(deps): bump PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml

Bumps the github-actions group with 1 update: [PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml](https://github.com/posthog/posthog-sdk-test-harness).


Updates `PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml` from 03d972e49be84402c491324320b0a0f38c2ddc53 to 27806196fa5c5ac74e9e7500bc46b74db1869f3a
- [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@03d972e...2780619)

---
updated-dependencies:
- dependency-name: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml
  dependency-version: 27806196fa5c5ac74e9e7500bc46b74db1869f3a
  dependency-type: direct:production
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(mcp): add WithIntentFallback to posthogmcpsdk

The TypeScript, Python, and Ruby MCP SDKs let a server supply the intent of a call whose agent sent no `context`, captured as `$mcp_intent_source: "inferred"` (sdk-specs mcp-analytics, "Configuration"). The Go adapter had no way to, so such calls had no intent. posthogmcp already accepted IntentSourceInferred; only the adapter lacked the hook.

WithIntentFallback takes `func(context.Context, *mcpsdk.CallToolRequest) (string, error)`, like WithIdentity. It runs only when the agent sent no usable context (blank, absent, or the context parameter is off) on `$mcp_tool_call`. An error or panic goes to the ErrorHandler and leaves the call without an intent. Blank and "{}" results and redaction come from posthogmcp's existing intent handling.

Tested: new TestInstrumentIntentFallback (9 cases, red before the change); go build, go vet, go test -race in posthogmcpsdk; build and vet against the pinned posthog-go with the replace dropped.

* test(mcp): pin the intent fallback's call count, request, and event scope

Review of the fallback found tests that passed without it running, an undocumented request and concurrency contract, and no check that it stays off for $mcp_unknown_tool and $mcp_missing_capability. Each case now asserts how often the fallback ran, a non-string context falls back, the callback sees the request with injected arguments, and the README example depends on the call's arguments.

Tested: go vet and go test -race in posthogmcpsdk; build and vet against the pinned posthog-go with the replace dropped.

* fix(mcp): run the intent fallback when the agent's context is "{}"

An agent that sent `"context": "{}"` counted as having stated an intent, so WithIntentFallback was skipped, and posthogmcp then dropped the "{}" as empty, leaving the call with no intent. The spec treats "{}" like a blank context, so it now falls through to the fallback.

Tested: new "empty object" case in TestInstrumentIntentFallback fails without the change; go vet and go test -race in posthogmcpsdk.
* feat: span processor request context

* fix: span processor review feedback

* fix: span processor review follow-ups
* fix(flags): load shared definitions without a secret key

* fix(flags): make keyless cache consumers read-only
Bumps the github-actions group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


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)

---
updated-dependencies:
- 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
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…el (#377)

Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.1 to 1.83.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](grpc/grpc-go@v1.83.1...v1.83.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…7 in /posthogmcpsdk in the go_modules group across 1 directory (#379)

chore(deps): bump github.com/klauspost/compress

Bumps the go_modules group with 1 update in the /posthogmcpsdk directory: [github.com/klauspost/compress](https://github.com/klauspost/compress).


Updates `github.com/klauspost/compress` from 1.17.11 to 1.18.7
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](klauspost/compress@v1.17.11...v1.18.7)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.7
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…7 in /otel (#380)

chore(deps): bump github.com/klauspost/compress in /otel

Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.17.11 to 1.18.7.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](klauspost/compress@v1.17.11...v1.18.7)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* ci(release): synchronize nested module core minimums

* ci(release): publish core before nested module tags
…#357)

* fix(mcp): set $exception_source and correct mechanism.synthetic

The mcp-analytics spec requires the $exception emitted alongside a failed
tool call to carry $exception_source: "mcp.tool_call", and to mark
mechanism.synthetic only for an isError result that carried no thrown
error. buildException set neither correctly: the source property was
absent, and synthetic was hardcoded true on a path only reached for a
thrown error.

Also strip $exception_source from caller-supplied properties, matching
how $exception_level is already protected.

Generated-By: PostHog Desktop
Task-Id: b4b6b8cc-da38-4c24-a978-9e2e9b5ac5f0

* fix(mcp): strip reserved exception metadata

* fix(mcp): keep mechanism.synthetic true until the adapter can mark synthesized errors

---------

Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com>
Co-authored-by: Ioannis J <yiannis@posthog.com>
* feat(flags): honor experiment holdouts in local flag evaluation

Local evaluation ignored `filters.holdout`, so an identifier inside an experiment's holdout was bucketed into a regular variant and disagreed with the server, silently and with no fallback.

Holdout resolution now runs after the inactive-flag check and before release conditions, returning the synthetic `holdout-<id>` variant. Membership uses the flag-level bucketing identity (group key for group-aggregated flags, device id for device-bucketed flags, distinct id otherwise) and the server's hash of `holdout-<value>`: the "-" separator, not the "." used by rollout hashing, selects a different population. Percentages are clamped to 0-100 without truncating fractions, compared inclusively, and 100 includes everyone without hashing. A holdout missing `id` or `exclusion_percentage` is skipped, leaving ordinary evaluation unchanged.

Tested: go build, go vet, gofmt, make api-diff, and go test -race -count=1 ./... at the repository root.

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

Generated-By: PostHog Desktop
Task-Id: 2887d167-f4e8-446a-a056-202893f6ecd9

* fix(flags): keep device-bucketed partial holdouts inconclusive without a device id

---------

Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com>
Co-authored-by: Ioannis J <yiannis@posthog.com>
o-mid and others added 17 commits November 10, 2026 14:44
* fix: keep events when a property is NaN or Inf

Identify, group identify, and exception custom properties skipped the normalization capture already uses, so one float32 NaN failed the whole event.

* fix: ignore shadowed exception properties before normalizing

Typed fields such as $lib win on collision and are never sent. Normalizing those custom values first dropped the exception when the ignored value could not be serialized.

* fix: encode exception custom structs with encoding/json

go-json ignores omitzero, so normalization was sending zero fields that the exception encoder used to omit.
Bumps the github-actions group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.39.1 to 4.39.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@9b32540...1fc58d0)

Updates `github/codeql-action/analyze` from 4.39.1 to 4.39.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@9b32540...1fc58d0)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.39.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.39.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(flags): fall back for unsupported cohort group types

* fix(flags): preserve decisive cohort siblings

* revert(flags): restore backend cohort fallback semantics

Reverse the sibling-composition behavior introduced in 38e3d22. Preserve order-specific regression tests for the agreed Rust backend contract.
…439)

* ci: notify Slack when release jobs fail, are cancelled, or never run

* ci: drop in-job Slack failure replies now covered by the notifier

* ci: accept a lone thread artifact and gate the notifier on trusted runs
* fix(flags): decide cohort groups and ignore failed flag values

A cohort group stopped at the first server-required leaf, so a later sibling that already proved the group never ran. A /flags detail with failed set was stored as a successful disable.

A decisive sibling now settles the group, and a failed detail is left out of the value and reported as evaluation_failed.

* fix(flags): address failed-flag review feedback

---------

Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
* fix(flags): honor cohort not_in and condition negation

Local evaluation treated a cohort not_in filter as membership, and a
release condition with negation set still required the property to match.

* fix(flags): keep invalid regex from matching under negation

An invalid regex is a non-match for both regex and not_regex. Condition negation was complementing that failure and enabling the flag for every value.

* fix(flags): ignore negation on legacy release conditions

Cohort not_in still excludes members. A negation field on a legacy release condition is left unused, matching the backend.

* test(flags): preserve unresolved cohort not_in results

* docs(flags): clarify cohort not_in release note

---------

Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
* docs: add Markdown SDK reference generator

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

* docs: generate the reference with go/doc instead of gomarkdoc

go/doc reads each exported symbol and go/doc/comment renders its doc comment
as Markdown, so the generator needs no third-party tool and no regex. It only
groups symbols by their posthog-capability tag in sdk-specs order and writes
front matter. The release step checks out sdk-specs for the headings.

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

* chore: update the public API snapshot for the capability tags

The snapshot includes doc comments, and the tags add a line to some of them.
No signatures change.

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

* ci: stage generated reference pages before the release commit

ghcommit-action passes an untracked folder to ghcommit as one path, which fails
on the first release that writes references/.

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

* docs: read capability tags from docs/capabilities.json

The posthog-capability lines showed up in IDE hover help for every SDK
user. The generator now reads a label-to-slug map next to it, and fails
on stale labels, unknown slugs, and unmapped SDK utils members.

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

* docs: map EnqueueAI to capture-ai on 2.x

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

* ci: add a workflow that regenerates the reference by hand

The release's reference step is best-effort, so a failure never blocks a
release. generate-references.yml reruns it from main on demand and commits
references/ when anything changed. Same shape as posthog-js's
generate-references.yml.

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

* ci: don't let the sdk-specs checkout block a release

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

* ci: warn when the SDK reference doesn't regenerate

The reference step is best-effort, so a failure leaves the release green. A
::warning:: annotation now says so and points to generate-references.yml.

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

* docs: open the Go getting-started page with the steps to a first event

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

* fix(reference): fence code examples, list typed constants, create references/

- Doc-comment examples printed as indented code, which MDX doesn't
  support, so braces in them parsed as JSX. Print them as fenced Go blocks.
- go/doc groups typed constants and variables under their type, and the
  type entry left them out, so values like CompressionGzip and every
  UnresolvedFlagReason weren't documented. Render them under the type.
- Create references/ before writing, so generation works from a clean
  checkout.

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

* fix(reference): clean up the generated Go reference

- Print each declaration with only the comments of the fields and
  methods go/doc kept, so unexported fields' comments don't show, and
  drop the blank lines a filtered member leaves.
- Put package constants and variables under Constants and Variables
  headings in SDK utils.
- Keep the EnqueueClient type entry, which public signatures take.
- Align columns with spaces, the way gofmt does.
- Tag a code example go only when it looks like Go, and text otherwise.
- Point [Name] doc links at the symbol's heading.
- Drop empty parts, the brace escapes the site no longer needs, and the
  printer's escaped backticks.
- Read the module path from go.mod, so the next major's page installs and
  imports .../posthog-go/v2.
- Fix the Properties example, which put the dots at the start of lines.

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

* fix(reference): drop the blank line nest leaves after the dropped H1

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

* fix(reference): tag an example go only when it parses, and trim blanks inside parentheses

A ... placeholder is read as an argument, so examples such as
NewWithConfig(...) still parse as Go, and prose with a parenthesis no
longer passes as code. A const block no longer opens with a blank line.

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

* docs: map Client.Emails to the emails capability

Main added Client.Emails, and the generator stops on a member with no
capability, so the release step would skip the pages.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs: generate Go SDK 1.x Markdown reference

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

* docs: generate Go SDK 2.x (experimental) Markdown reference and migration guide

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

* docs: regenerate the Go references with go/doc

The 2.x page and migration guide come from v1 with the capability tags applied,
and one migration line rewrapped so MDX doesn't read <cause> as a tag. Both
changes still need to land on v1.

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

* docs: regenerate the Go 2.x reference with a Capture AI section

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

* docs: regenerate the Go references with the getting-started intro

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

* docs: regenerate the Go references with fenced examples and typed constants

The 1.x pages come from this branch. The 2.x page comes from the v1
branch with --major 2, so it also picks up that branch's current doc
comments. The migration page is unchanged.

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

* docs: regenerate the Go references with the cleaned-up generator

The 1.x pages come from this branch. The 2.x and migration previews come
from the v1 branch with --major 2, so the migration page now matches
v1's current guide and the 2.x getting started installs .../posthog-go/v2.

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

* docs: regenerate the Go references with stricter code tags

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

* docs: regenerate the Go references with main's new APIs

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
… fixes, Markdown reference)

Real merge, so v1's merge base with main moves and the release-day merge
of v1 into main only has to cover what lands after this.

v1 already carries main up to 1.39.0 through the squashed sync #405, so
every file is resolved against main at 1.39.0 (e13d11c), not the old merge
base: files main has not changed since 1.39.0 keep v1's version, and the
rest are a three-way merge of v1, main@1.39.0 and main. A plain merge
re-applied pre-1.39 main hunks that v1 adapted on purpose (capture_test.go
legacy Library/CaptureMode tests, duplicated MCP declarations in
posthogmcpsdk/tools.go).

Resolutions:
- Not carried, already covered on v1: #427 (legacy /batch/ retry clamp;
  v1 clamps Retry-After to MaxRetryBackoff and never waits after its last
  attempt), and the legacy halves of #433 in message.go and posthog.go
  (/batch/ sender, capture strategy switch). capture_v1*.go stay deleted.
- config.go, posthog.go: main's email settings, sender, close and drain
  are kept; CaptureMode and the GeoIP default block stay removed (v1 sets
  $geoip_disable through its layers).
- release.yml: v1's tagging and /v2 require, plus posthogmcpgo and main's
  release steps. codeql.yml: main's action pins.
- otel/go.mod, posthogmcpsdk/go.mod: keep the /v2 requirement.
- CHANGELOG.md, version.go: follow main (1.42.4).
- api/public-api.txt: regenerated; 80 lines added, none removed, all in
  main's snapshot.

Build-only edits to tests main added: example_emails_test.go imports /v2;
emails_test.go gets the disabled client from New's two results;
event_properties_json_test.go calls v1's prepareForSend(msg, logger);
posthog_test.go expects v1's capture path. The #433 tests that fail here
are fixed by the next commit.

posthogmcpgo is ported to /v2 in a later commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… main does (port #433)

go-json reports a non-finite float64 but writes a float32 NaN or Inf as a
bare token with no error. v1 normalized properties only after a go-json
error, so an event whose only non-finite numbers were float32 values was
queued as invalid JSON, and capture refused the whole request it was in
(post-release item 18). A custom MarshalJSON returning invalid JSON did
the same.

- marshalEventPayload normalizes properties before every encode and checks
  the result as JSON, like main's prepareForSendV1 and #433. Finite events
  are byte-identical on the wire (TestPrepareCaptureChangesOnlyNonFiniteNumbers).
- Exception custom properties are normalized with encoding/json, which
  honors omitzero, as 1.x encoded them. Keys the typed fields overwrite
  ($exception_list, and $exception_fingerprint / $debug_images when set)
  are skipped first, so a bad value there no longer drops the exception.
- Drop main's legacy-only normalizeLegacyMessage and
  exceptionCustomWithoutShadowedKeys; v1 has no /batch/ messages.
- Tests: float32 NaN/Inf alone on capture, identify, group identify and
  exception; a float32 NaN keeps the rest of the batch; invalid custom
  output fails only its own event with or without a non-finite number;
  the shadowed-key test uses v1's typed exception keys.
- Migration guide and changeset describe the new behavior.

Cost (BenchmarkPrepareForSendFinite / _Cardinality, mean of 6): typical
5.8 -> 10.5 us, low 14 -> 25 us, medium 149 -> 258 us, high 630 -> 1042 us,
large AI event 226 -> 1089 us per event; main pays the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's #427 stopped its senders sleeping after the last failed attempt.
v1's sender already returns before any wait on its last attempt; this
test pins that for the analytics and AI lanes, with a long Retry-After and
RetryAfter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthogmcpgo arrived from main (#409) on the 1.x module paths. With the
core at /v2, its tag (posthogmcpgo/v2.x.y) needs the /v2 module path.

- go.mod: module github.com/posthog/posthog-go/posthogmcpgo/v2, requiring
  and replacing posthog-go/v2 and posthogmcpsdk/v2.
- Imports, the example and the README snippet use the v2 paths and v2's
  New, which returns an error.
- Its events get v2's per-event library pair from posthogmcp
  ($lib posthog-go-mcp and the SDK version); the end-to-end test asserts it.
- CI builds it against the pinned core with bin/build-against-pinned-core,
  like otel and posthogmcpsdk, which skips with a warning until v2.0.0 is
  tagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthogmcpsdk gained WithEventOptions in v2 (#425); the mcp-go adapter
needs the same to set capture options on $mcp_tool_call and its
$exception. A call without a distinct ID still sends
process_person_profile false, and a resolver error or panic is reported
and drops only its options.

Tests cover options on the call and its exception, the personless
default, and the error and panic paths. README, migration guide (module
path) and a changeset describe it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run with --major 2, so the 1.x and latest pages stay as main generated
them. The 2.x page gains the Emails API this sync brings to v2; the
migration page matches docs/migration-v2.md (non-finite numbers,
posthogmcpgo/v2 path).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eli-r-ph eli-r-ph self-assigned this Nov 11, 2026
@eli-r-ph
eli-r-ph requested a review from a team November 11, 2026 20:37
@eli-r-ph
eli-r-ph marked this pull request as ready for review November 11, 2026 22:01
@veria-ai

veria-ai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

PR overview

No security issues were found in this pull request.

Reviews

@eli-r-ph eli-r-ph closed this Nov 11, 2026
@eli-r-ph eli-r-ph reopened this Nov 11, 2026
@eli-r-ph
eli-r-ph marked this pull request as draft November 11, 2026 22:16
@eli-r-ph
eli-r-ph marked this pull request as ready for review November 11, 2026 22:24
@eli-r-ph
eli-r-ph merged commit 7456de6 into v1 Nov 12, 2026
38 checks passed
@eli-r-ph
eli-r-ph deleted the eli/v1-sync-main-20261111 branch November 12, 2026 19:03
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.