Skip to content

feat(emails): let a code send choose its tracking and sensitivity - #308

Merged
Silthus merged 6 commits into
mainfrom
feat/emails-track-opens-and-clicks
Nov 12, 2026
Merged

Silthus merged 6 commits into
mainfrom
feat/emails-track-opens-and-clicks

Conversation

@Silthus

@Silthus Silthus commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Problem: A code send (template_key with 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:

  • The code-send form takes 'tracking'. Left out, PostHog counts opens and captures engagement events, and tracks clicks only when the email isn't sensitive.
  • true turns on opens, clicks and engagement events. false turns them all off.
  • ['clicks' => true] sets opens (default on), clicks (default: on unless the email is sensitive) and engagement (default on) one by one.
  • The code-send form takes 'sensitive' too. true marks 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.
  • Left out, PostHog keeps today's behavior: the email takes the setting of the template whose key matches, and is sensitive when none matches. No existing caller sees a change.
  • The client sends sensitive and tracking only when you set them.
  • The client now sends capture_engagement only 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_engagement is deprecated in its docblock and the README, in favor of tracking with engagement.
  • tracking and sensitive are refused on a template_id send before any request. A sensitive that isn't a bool throws invalid_form. A value that isn't a bool or a keyed array throws invalid_form. An empty array goes out as {}.
  • Docs: the 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_clicks from 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 tracking and sensitive in both regions: #131780, #131756.

💚 How did you test it?

In the posthog-php-ci:8.4 image (php:8.4-cli with Composer 2), CI's commands:

  • composer validate, composer api:check: pass.
  • phpcs on the changed files: no errors or warnings.
  • phpunit: 2709 tests, one skipped, and all others but one pass. PackageArchiveTest fails only because it runs git archive and the container mounts a git worktree whose .git sits outside the mount. CI runs it from a full clone.
  • sdk_compliance_adapter unit tests: 16 pass, run on the previous head; this change doesn't touch the adapter.

test/EmailsTest.php asserts the raw JSON body: unset, true, false, ['clicks' => true], null kinds dropped, [] as {}, tracking with capture_engagement, sensitive true, false and unset, and sensitive next to tracking. argumentErrors covers tracking and sensitive on the template form, a string, a list, a non-bool kind and a non-bool sensitive.

With the line that sends sensitive removed, the new tests fail, so they catch a client that drops the field.

Not run against a live PostHog: the API takes tracking and sensitive only after the two PRs above deploy. A real send through this branch is part of the SDK release gate.

📝 Checklist

  • All commits are signed (see Commit signing)
  • 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 change to generate a change intent file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Written by Claude Opus 5.5 (claude-opus-5-5) in Claude Code, as one of several SDK lanes adding the same field.
  • The SDK checks only types. PostHog's API refuses unknown kinds and opens off with clicks on, so new kinds need no SDK release to be refused clearly.
  • The change intent was written by hand in the shape the repo's tool produces.

🤖 Generated with Claude Code

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>
@Silthus Silthus self-assigned this Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-fork_curl Compliance Report

Date: 2026-11-11T04:15:24.895761+00:00
Duration: 115006ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
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

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-socket Compliance Report

Date: 2026-11-11T04:15:31.292138+00:00
Duration: 115270ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
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

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-lib_curl Compliance Report

Date: 2026-11-11T04:15:28.563333+00:00
Duration: 116130ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
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>
@Silthus Silthus mentioned this pull request Nov 9, 2026
6 tasks done
Silthus and others added 3 commits November 10, 2026 08:20
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>
@Silthus Silthus changed the title feat(emails): let a code send turn off open and click tracking feat(emails): let a code send choose its tracking Nov 10, 2026

@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 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.

Comment thread lib/Emails.php Resolved
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>
@Silthus Silthus changed the title feat(emails): let a code send choose its tracking feat(emails): let a code send choose its tracking and sensitivity Nov 11, 2026

@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 99f60a1: 1 Must fix.

Comment thread lib/Emails.php Resolved
@Silthus
Silthus marked this pull request as ready for review November 11, 2026 08:00
@Silthus
Silthus requested a review from a team as a code owner November 11, 2026 08:00
@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

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@Silthus
Silthus merged commit 53bb1cf into main Nov 12, 2026
37 checks passed
@Silthus
Silthus deleted the feat/emails-track-opens-and-clicks branch November 12, 2026 16:35
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.

1 participant