Repository navigation
feat(emails): let a code send choose its tracking and sensitivity - #308
Conversation
A send with template_key takes track_opens_and_clicks. With false, PostHog sends the email without its open pixel or click-counting links. The client sends the field only when the caller sets it, so a default send keeps today's request body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthog-php-fork_curl Compliance ReportDate: 2026-11-11T04:15:24.895761+00:00
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 26ms |
| Format Validation.Event Has Uuid | ✅ | 524ms |
| Format Validation.Event Has Lib Properties | ✅ | 524ms |
| Format Validation.Distinct Id Is String | ✅ | 524ms |
| Format Validation.Token Is Present | ✅ | 524ms |
| Format Validation.Custom Properties Preserved | ✅ | 524ms |
| Format Validation.Event Has Timestamp | ✅ | 523ms |
| Format Validation.Non Utc Event Timestamp Is Converted To Utc | ✅ | 524ms |
| Retry Behavior.Retries On 503 | ✅ | 5846ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 2526ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 2527ms |
| Retry Behavior.Respects Retry After Header | ❌ | 6338ms |
| Retry Behavior.Implements Backoff | ✅ | 16260ms |
| Retry Behavior.Retries On 500 | ✅ | 5143ms |
| Retry Behavior.Retries On 502 | ✅ | 5638ms |
| Retry Behavior.Retries On 504 | ✅ | 5638ms |
| Retry Behavior.Max Retries Respected | ✅ | 16260ms |
| Deduplication.Generates Unique Uuids | ✅ | 527ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 5635ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 10845ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 5146ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 528ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 526ms |
| Compression.Sends Gzip When Enabled | ✅ | 526ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 524ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 515ms |
| Batch Format.Multiple Events Batched Together | ✅ | 514ms |
| Error Handling.Does Not Retry On 403 | ✅ | 2527ms |
| Error Handling.Does Not Retry On 413 | ✅ | 2525ms |
| Error Handling.Retries On 408 | ✅ | 5641ms |
Failures
retry_behavior.respects_retry_after_header
Retry delay too short: 809ms < 2500ms
Feature_Flags Tests
✅ 17/17 tests passed
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 518ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 517ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 517ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 518ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 518ms |
| Request Payload.Groups Round Trip | ✅ | 517ms |
| Request Payload.Groups Default To Empty Object | ✅ | 517ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 516ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ✅ | 517ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 517ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 514ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 512ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 519ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 519ms |
| Retry Behavior.Retries Flags On 502 | ✅ | 620ms |
| Retry Behavior.Retries Flags On 504 | ✅ | 618ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 525ms |
posthog-php-socket Compliance ReportDate: 2026-11-11T04:15:31.292138+00:00
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 30ms |
| Format Validation.Event Has Uuid | ✅ | 524ms |
| Format Validation.Event Has Lib Properties | ✅ | 526ms |
| Format Validation.Distinct Id Is String | ✅ | 527ms |
| Format Validation.Token Is Present | ✅ | 527ms |
| Format Validation.Custom Properties Preserved | ✅ | 527ms |
| Format Validation.Event Has Timestamp | ✅ | 526ms |
| Format Validation.Non Utc Event Timestamp Is Converted To Utc | ✅ | 527ms |
| Retry Behavior.Retries On 503 | ✅ | 5837ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 2528ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 2530ms |
| Retry Behavior.Respects Retry After Header | ❌ | 6333ms |
| Retry Behavior.Implements Backoff | ✅ | 16240ms |
| Retry Behavior.Retries On 500 | ✅ | 5146ms |
| Retry Behavior.Retries On 502 | ✅ | 5635ms |
| Retry Behavior.Retries On 504 | ✅ | 5634ms |
| Retry Behavior.Max Retries Respected | ✅ | 16250ms |
| Deduplication.Generates Unique Uuids | ✅ | 534ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 5635ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 10834ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 5145ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 535ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 528ms |
| Compression.Sends Gzip When Enabled | ✅ | 527ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 528ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 522ms |
| Batch Format.Multiple Events Batched Together | ✅ | 513ms |
| Error Handling.Does Not Retry On 403 | ✅ | 2530ms |
| Error Handling.Does Not Retry On 413 | ✅ | 2528ms |
| Error Handling.Retries On 408 | ✅ | 5635ms |
Failures
retry_behavior.respects_retry_after_header
Retry delay too short: 802ms < 2500ms
Feature_Flags Tests
✅ 17/17 tests passed
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 526ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 524ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 525ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 525ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 526ms |
| Request Payload.Groups Round Trip | ✅ | 524ms |
| Request Payload.Groups Default To Empty Object | ✅ | 525ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 525ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ✅ | 524ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 525ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 520ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 508ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 529ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 527ms |
| Retry Behavior.Retries Flags On 502 | ✅ | 627ms |
| Retry Behavior.Retries Flags On 504 | ✅ | 626ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 528ms |
posthog-php-lib_curl Compliance ReportDate: 2026-11-11T04:15:28.563333+00:00
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 20ms |
| Format Validation.Event Has Uuid | ✅ | 516ms |
| Format Validation.Event Has Lib Properties | ✅ | 517ms |
| Format Validation.Distinct Id Is String | ✅ | 516ms |
| Format Validation.Token Is Present | ✅ | 515ms |
| Format Validation.Custom Properties Preserved | ✅ | 516ms |
| Format Validation.Event Has Timestamp | ✅ | 515ms |
| Format Validation.Non Utc Event Timestamp Is Converted To Utc | ✅ | 517ms |
| Retry Behavior.Retries On 503 | ✅ | 5821ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 2521ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 2519ms |
| Retry Behavior.Respects Retry After Header | ❌ | 6325ms |
| Retry Behavior.Implements Backoff | ✅ | 16238ms |
| Retry Behavior.Retries On 500 | ✅ | 5624ms |
| Retry Behavior.Retries On 502 | ✅ | 5624ms |
| Retry Behavior.Retries On 504 | ✅ | 5623ms |
| Retry Behavior.Max Retries Respected | ✅ | 17025ms |
| Deduplication.Generates Unique Uuids | ✅ | 235ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 5626ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 10830ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 5626ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 521ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 516ms |
| Compression.Sends Gzip When Enabled | ✅ | 516ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 516ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 513ms |
| Batch Format.Multiple Events Batched Together | ✅ | 507ms |
| Error Handling.Does Not Retry On 403 | ✅ | 2518ms |
| Error Handling.Does Not Retry On 413 | ✅ | 2519ms |
| Error Handling.Retries On 408 | ✅ | 5624ms |
Failures
retry_behavior.respects_retry_after_header
Retry delay too short: 802ms < 2500ms
Feature_Flags Tests
✅ 17/17 tests passed
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 518ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 516ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 516ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 514ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 515ms |
| Request Payload.Groups Round Trip | ✅ | 516ms |
| Request Payload.Groups Default To Empty Object | ✅ | 514ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 516ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ✅ | 516ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 517ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 512ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 505ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 518ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 515ms |
| Retry Behavior.Retries Flags On 502 | ✅ | 616ms |
| Retry Behavior.Retries Flags On 504 | ✅ | 616ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 517ms |
PostHog never rewrites the links of an email sent from code, so its clicks go uncounted whatever the field says. With false, the email goes out without PostHog's open pixel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace track_opens_and_clicks with tracking: true, false, or opens, clicks and engagement one by one. It is left out of the request when unset. capture_engagement is deprecated in favor of tracking's engagement and keeps working; the client leaves it out when tracking is set, unless the caller sets it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A code send that sets tracking must not also carry capture_engagement, or PostHog refuses it. PostHog reads a missing capture_engagement as true, so a default send still captures engagement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
PostHog Review · Standard · reviewed 9f8cade: 1 Must fix.
See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.
A send with template_key now takes an optional sensitive flag. The client sends it only when the caller sets it. Left out, PostHog keeps today's behavior: the matching template's setting, else sensitive. Click tracking follows it unless tracking sets clicks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR overviewNo security issues were found in this pull request. Reviews
|
There was a problem hiding this comment.
Approved.
The risky changes have tests and STRONG author familiarity. The current-head review reports a must-fix finding without supplying its substance, so it is not a clean review signal; no concrete unresolved concern can be verified from the supplied context.
- Author wrote 100% of the modified lines and has 1 merged PRs in these paths (familiarity STRONG).
- PostHog Review (Standard) reviewed the current head: 1 finding, posted on the PR.
- posthog[bot] reviewed the current head.
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✓ | no deny categories matched |
| size | ✓ | 84L, 1F substantive, 146L/5F incl. docs/generated/snapshots — within ceiling |
| tier | ✓ | T1-agent / T1d-complex (146L, 5F, cross-cutting, feat) |
| stamphog 2.7.0 | .stamphog/policy.yml @ 99f60a1 · reviewed head 99f60a1 |
💡 Motivation and Context
Problem: A code send (
template_keywith your own content) can't say what PostHog does with it. It always carries PostHog's open pixel. PostHog treats it as sensitive, with no click tracking, unless its key matches a template made in PostHog that isn't sensitive. Callers can't send an email fully untracked, such as a welcome email whose spec forbids open pixels. They can't turn on click tracking for links that are safe to rewrite, or mark an email as sensitive or not.Changes:
'tracking'. Left out, PostHog counts opens and captures engagement events, and tracks clicks only when the email isn't sensitive.trueturns on opens, clicks and engagement events.falseturns them all off.['clicks' => true]setsopens(default on),clicks(default: on unless the email is sensitive) andengagement(default on) one by one.'sensitive'too.truemarks an email that carries login links, reset tokens or codes: its links stay unchanged unless tracking turns clicks on, its subject stays off engagement events, and its content stays out of logs.sensitiveandtrackingonly when you set them.capture_engagementonly when you set it too. PostHog refuses a send that sets both, and it reads a missing value as on, so a default send captures engagement as before.capture_engagementis deprecated in its docblock and the README, in favor oftrackingwithengagement.trackingandsensitiveare refused on atemplate_idsend before any request. Asensitivethat isn't a bool throwsinvalid_form. A value that isn't a bool or a keyed array throwsinvalid_form. An empty array goes out as{}.send()docblock, its array shape and the README, which gains a sensitive emails bullet. Change intent:.changeset/emails-tracking.md(minor).This replaces
track_opens_and_clicksfrom the first version of this PR, which never shipped. The emails sdk-spec doesn't cover the code-content form, so no spec covers these fields.Release once PostHog's API takes
trackingandsensitivein both regions: #131780, #131756.💚 How did you test it?
In the
posthog-php-ci:8.4image (php:8.4-cliwith Composer 2), CI's commands:composer validate,composer api:check: pass.phpcson the changed files: no errors or warnings.phpunit: 2709 tests, one skipped, and all others but one pass.PackageArchiveTestfails only because it runsgit archiveand the container mounts a git worktree whose.gitsits outside the mount. CI runs it from a full clone.sdk_compliance_adapterunit tests: 16 pass, run on the previous head; this change doesn't touch the adapter.test/EmailsTest.phpasserts the raw JSON body: unset,true,false,['clicks' => true], null kinds dropped,[]as{},trackingwithcapture_engagement,sensitivetrue, false and unset, andsensitivenext totracking.argumentErrorscoverstrackingandsensitiveon the template form, a string, a list, a non-bool kind and a non-boolsensitive.With the line that sends
sensitiveremoved, the new tests fail, so they catch a client that drops the field.Not run against a live PostHog: the API takes
trackingandsensitiveonly after the two PRs above deploy. A real send through this branch is part of the SDK release gate.📝 Checklist
If releasing new changes
pnpm changeto generate a change intent file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
claude-opus-5-5) in Claude Code, as one of several SDK lanes adding the same field.🤖 Generated with Claude Code