Skip to content

feat(emails): name the integration that sends an email - #328

Draft
Silthus wants to merge 1 commit into
feat/emails-attachmentsfrom
feat/emails-integration-option
Draft

Silthus wants to merge 1 commit into
feat/emails-attachmentsfrom
feat/emails-integration-option

Conversation

@Silthus

@Silthus Silthus commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A library that sends email for an app through client.emails.send, such as the Action Mailer delivery method coming next in this stack, has no way to say so. Every send looks like a direct posthog-ruby call, so PostHog can't tell which integrations people send through.

Part of PostHog/michaelr-context#43. Merge first: #325 (this PR stacks on its branch, feat/emails-attachments).

Merge Danger

Low, two-way door. One optional keyword on emails.send. Without it, a send is byte-for-byte what it was. Nothing calls it until the Rails delivery method in the next PR. Held as a draft until the emails.send SDKs release.

Summary

client.emails.send takes integration:, a name/version string such as action-mailer/3.20.0.

  • The send carries it as the X-PostHog-Integration header, on every retry of that send.
  • The User-Agent stays posthog-ruby/<version>, and the analytics client is untouched.
  • The name is lowercase letters, digits and hyphens; the version is letters, digits, ., + and -; each 1 to 40 characters. Anything else raises ArgumentError before any request, like every other invalid option.

Changeset: posthog-ruby minor.

Evidence

  • Red on feat(emails): send cc, bcc and attachments #325's head (df3ff92): the new examples fail with ArgumentError: unknown keyword: :integration.
  • Green on this head: bundle exec rspec spec/posthog/emails_spec.rb -e "integration marker", 3 examples, 0 failures.
  • Lane gate on Ruby 3.2.2: bundle exec rspec 2093 examples, 0 failures (40 pending, the OpenTelemetry-bundle specs); bundle exec rubocop no offenses; bundle exec rake public_api:check passes with the updated snapshot.
  • Test seams: the HTTP request emails.send makes, stubbed with WebMock: the header with the option (on both attempts of a retried send), no header without it, and no request for an invalid value.
  • Live run: one real send through the next PR's delivery method, which passes this option, is recorded there.

Seams: the X-PostHog-Integration header is read by the send API's usage event (the instrumentation stack of the same plan, no PR open yet); posthog-node and posthog-php gain the same option with the same pattern in their own stacks. A reviewer should flag any divergence in the header name, the value pattern, or a value sent without validation.

SDK spec: sdk-specs' emails spec lists the send headers and doesn't name this option yet; this adds one optional header and changes none it lists.

Decisions

Question Answer Who Where
How does an integration identify its sends? One X-PostHog-Integration: <name>/<version> header, set by a per-send integration option; User-Agent unchanged 👍 Accepted default ticket
Where does the option live? On the email sender only, validated like every other send option 👍 Accepted default ticket

Involvement: no grilling or prototype rounds in this PR; one cross-model review; proof captured.

🤖 Generated with Claude Code

A mailer adapter that sends through `client.emails.send` passes
`integration: '<name>/<version>'`, and the send carries it as the
`X-PostHog-Integration` header, so PostHog can tell which integration
sent the email. The User-Agent stays the SDK's. A value outside
`name/version` raises ArgumentError before any request.

Ticket: https://github.com/PostHog/michaelr-context/issues/43

Co-Authored-By: Claude Opus 5.5 <[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 66b0217: no findings.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

posthog-ruby-sync Compliance Report

Date: 2026-11-05T12:26:51.240777+00:00
Duration: 55874ms

⚠️ Some Tests Failed

39/47 tests passed, 8 failed


Capture Tests

⚠️ 23/30 tests passed, 7 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 6ms
Format Validation.Event Has Uuid ✅ 5ms
Format Validation.Event Has Lib Properties ✅ 4ms
Format Validation.Distinct Id Is String ✅ 6ms
Format Validation.Token Is Present ✅ 5ms
Format Validation.Custom Properties Preserved ✅ 5ms
Format Validation.Event Has Timestamp ✅ 6ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 4ms
Retry Behavior.Retries On 503 ✅ 5256ms
Retry Behavior.Does Not Retry On 400 ❌ 6ms
Retry Behavior.Does Not Retry On 401 ❌ 3ms
Retry Behavior.Respects Retry After Header ✅ 8010ms
Retry Behavior.Implements Backoff ❌ 321ms
Retry Behavior.Retries On 500 ✅ 5156ms
Retry Behavior.Retries On 502 ✅ 5114ms
Retry Behavior.Retries On 504 ✅ 5114ms
Retry Behavior.Max Retries Respected ❌ 650ms
Deduplication.Generates Unique Uuids ✅ 12ms
Deduplication.Preserves Uuid On Retry ✅ 5112ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10313ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5117ms
Deduplication.No Duplicate Events In Batch ✅ 16ms
Deduplication.Different Events Have Different Uuids ✅ 6ms
Compression.Sends Gzip When Enabled ✅ 4ms
Batch Format.Uses Proper Batch Structure ✅ 5ms
Batch Format.Flush With No Events Sends Nothing ✅ 2ms
Batch Format.Multiple Events Batched Together ❌ 13ms
Error Handling.Does Not Retry On 403 ❌ 3ms
Error Handling.Does Not Retry On 413 ❌ 4ms
Error Handling.Retries On 408 ✅ 5113ms

Failures

retry_behavior.does_not_retry_on_400

500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'

retry_behavior.does_not_retry_on_401

500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'

retry_behavior.implements_backoff

500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'

retry_behavior.max_retries_respected

500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'

batch_format.multiple_events_batched_together

Expected 1 requests, got 5

error_handling.does_not_retry_on_403

500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'

error_handling.does_not_retry_on_413

500, message='Internal Server Error', url='http://sdk-adapter:8080/capture'

Feature_Flags Tests

⚠️ 16/17 tests passed, 1 failed

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

Failures

request_payload.disable_geoip_omitted_defaults_to_false

Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['distinct_id', 'groups', 'person_properties', 'group_properties', 'flag_keys_to_evaluate', 'token']

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

posthog-ruby-async Compliance Report

Date: 2026-11-05T12:27:36.301938+00:00
Duration: 98256ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 109ms
Format Validation.Event Has Uuid ✅ 107ms
Format Validation.Event Has Lib Properties ✅ 108ms
Format Validation.Distinct Id Is String ✅ 106ms
Format Validation.Token Is Present ✅ 106ms
Format Validation.Custom Properties Preserved ✅ 106ms
Format Validation.Event Has Timestamp ✅ 106ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 7ms
Retry Behavior.Retries On 503 ✅ 5308ms
Retry Behavior.Does Not Retry On 400 ✅ 2109ms
Retry Behavior.Does Not Retry On 401 ✅ 2109ms
Retry Behavior.Respects Retry After Header ✅ 8115ms
Retry Behavior.Implements Backoff ✅ 15510ms
Retry Behavior.Retries On 500 ✅ 5213ms
Retry Behavior.Retries On 502 ✅ 5213ms
Retry Behavior.Retries On 504 ✅ 5212ms
Retry Behavior.Max Retries Respected ✅ 15616ms
Deduplication.Generates Unique Uuids ✅ 111ms
Deduplication.Preserves Uuid On Retry ✅ 5211ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10308ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5214ms
Deduplication.No Duplicate Events In Batch ✅ 112ms
Deduplication.Different Events Have Different Uuids ✅ 107ms
Compression.Sends Gzip When Enabled ✅ 106ms
Batch Format.Uses Proper Batch Structure ✅ 106ms
Batch Format.Flush With No Events Sends Nothing ✅ 4ms
Batch Format.Multiple Events Batched Together ✅ 111ms
Error Handling.Does Not Retry On 403 ✅ 2109ms
Error Handling.Does Not Retry On 413 ✅ 2109ms
Error Handling.Retries On 408 ✅ 5212ms

Feature_Flags Tests

⚠️ 16/17 tests passed, 1 failed

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

Failures

request_payload.disable_geoip_omitted_defaults_to_false

Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['distinct_id', 'groups', 'person_properties', 'group_properties', 'flag_keys_to_evaluate', 'token']

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.

1 participant