Skip to content

fix(error-tracking): add flat $exception_type and $exception_message - #253

Draft
posthog[bot] wants to merge 1 commit into
mainfrom
posthog/php-flat-exception-type-message
Draft

posthog[bot] wants to merge 1 commit into
mainfrom
posthog/php-flat-exception-type-message

Conversation

@posthog

@posthog posthog Bot commented Oct 4, 2026

Copy link
Copy Markdown

💡 Motivation and Context

Why: the cross-SDK compliance matrix for posthog-php flags Capture Exception as 🟡 Partial because the SDK never emits the flat $exception_type / $exception_message properties that the contract requires. Error-tracking consumers that read those top-level properties see nothing for PHP today.

Contract: capture-exception — the scenario "Capturing a handled exception emits an exception event" requires flat, top-level $exception_type / $exception_message properties on the enqueued event, in addition to the structured $exception_list.

Compliance matrix note (posthog-php, n2 — Capture Exception):

Client::captureException() builds $exception_list and $exception_handled via ExceptionPayloadBuilder::buildExceptionList()/getPrimaryHandled(), but never sets a top-level $exception_type or $exception_message property — type/value only exist nested inside $exception_list[0].type/.value. Everything else in the contract (outermost-first list ordering, bottom-up frame ordering, ascending source-context windows, stack preservation) is correctly implemented and tested.
Remediation: In Client::captureException() (and the equivalent auto-capture path in ExceptionCapture::sendExceptionEvent()), add $properties['$exception_type']/$properties['$exception_message'] mirrored from the first (outermost) entry of the built $exception_list.

What changed

  • New ExceptionPayloadBuilder::getPrimarySummaryProperties(), mirroring the existing getPrimaryHandled() helper, returns $exception_type / $exception_message from the primary (first, outermost) $exception_list entry. Keys are omitted when the primary entry has no string type / value, so a malformed list never emits empty summary properties.
  • Both capture paths now merge those properties onto the event: the manual Client::captureException() and the automatic ExceptionCapture::sendExceptionEvent() (uncaught-exception and error-handler capture).

Backwards compatibility

Additive. Two new properties are added to $exception events; nothing emitted today is removed or renamed, no public signature changes, and $exception_list keeps its existing shape. The values are derived from data already on the event, so no new data is collected. Caller-supplied $additionalProperties precedence is unchanged (reserved exception properties still win, as they already did for $exception_list / $exception_handled).

💚 How did you test it?

PHP 8.3.6 locally:

  • vendor/bin/phpunit --filter 'ExceptionPayloadBuilderTest|ExceptionCaptureTest' — 45 tests, 223 assertions, all passing.
  • Three new tests cover the manual path: a simple throwable, a chained exception (flat properties must mirror the outermost exception, not the root cause), and a string message (Error / the string). One existing auto-capture test was extended to assert the flat properties on the uncaught-exception path.
  • vendor/bin/phpunit (full suite) — the 17 errors and 1 failure are pre-existing in this sandbox and reproduce identically on an unmodified checkout; they are the tests that need outbound network/subprocess access.
  • vendor/bin/phpcs on the changed files — 0 errors.
  • composer api:check passes after composer api:update (snapshot updated for the one new helper).

Follow-up

Not in scope here: the other open posthog-php compliance gaps, including the runtime opt-out mechanism on Capture and the Exception Event Metadata contract.

📝 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 change to generate a change intent file

A posthog-php: patch intent was added manually in the existing .changeset format; no package manager or release command was run.

🤖 Agent context

Autonomy: Fully autonomous

Opened by the scheduled SDK-compliance workflow, which reads the compliance matrices in PostHog/sdk-specs and implements one backwards-compatible gap per run. This gap was picked over the other open posthog-php items because it is additive, self-contained, and user-facing. The null-custom-property serialization gap on the same SDK was considered and skipped: a previous attempt at it (#241) was closed without merging, so it needs a human decision rather than another autonomous attempt. Left unassigned for the owning team to triage.


Created with PostHog Desktop

🤖 Generated with Claude Code

The capture-exception contract requires flat, top-level $exception_type and
$exception_message properties alongside the structured $exception_list. The PHP
SDK emitted only $exception_list, so both properties were missing from every
exception event.

Mirror them from the primary (first, outermost) $exception_list entry in both
the manual Client::captureException() path and the automatic
ExceptionCapture::sendExceptionEvent() path.

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

Generated-By: PostHog Desktop
Task-Id: 6fb47dbf-3d24-4483-b8e2-ded190b110a4
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

posthog-php-lib_curl Compliance Report

Date: 2026-10-04T06:08:17.174587+00:00
Duration: 118390ms

✅ All Tests Passed!

47/47 tests passed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 21ms
Format Validation.Event Has Uuid ✅ 517ms
Format Validation.Event Has Lib Properties ✅ 518ms
Format Validation.Distinct Id Is String ✅ 519ms
Format Validation.Token Is Present ✅ 519ms
Format Validation.Custom Properties Preserved ✅ 519ms
Format Validation.Event Has Timestamp ✅ 519ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 518ms
Retry Behavior.Retries On 503 ✅ 5827ms
Retry Behavior.Does Not Retry On 400 ✅ 2521ms
Retry Behavior.Does Not Retry On 401 ✅ 2521ms
Retry Behavior.Respects Retry After Header ✅ 8536ms
Retry Behavior.Implements Backoff ✅ 16236ms
Retry Behavior.Retries On 500 ✅ 5624ms
Retry Behavior.Retries On 502 ✅ 5623ms
Retry Behavior.Retries On 504 ✅ 5624ms
Retry Behavior.Max Retries Respected ✅ 17037ms
Deduplication.Generates Unique Uuids ✅ 223ms
Deduplication.Preserves Uuid On Retry ✅ 5628ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10835ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5626ms
Deduplication.No Duplicate Events In Batch ✅ 522ms
Deduplication.Different Events Have Different Uuids ✅ 519ms
Compression.Sends Gzip When Enabled ✅ 518ms
Batch Format.Uses Proper Batch Structure ✅ 518ms
Batch Format.Flush With No Events Sends Nothing ✅ 515ms
Batch Format.Multiple Events Batched Together ✅ 509ms
Error Handling.Does Not Retry On 403 ✅ 2520ms
Error Handling.Does Not Retry On 413 ✅ 2519ms
Error Handling.Retries On 408 ✅ 5623ms

Feature_Flags Tests

✅ 17/17 tests passed

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

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

posthog-php-fork_curl Compliance Report

Date: 2026-10-04T06:08:16.771212+00:00
Duration: 112935ms

⚠️ Some Tests Failed

36/47 tests passed, 11 failed


Capture Tests

⚠️ 19/30 tests passed, 11 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 38ms
Format Validation.Event Has Uuid ✅ 528ms
Format Validation.Event Has Lib Properties ✅ 531ms
Format Validation.Distinct Id Is String ✅ 532ms
Format Validation.Token Is Present ✅ 530ms
Format Validation.Custom Properties Preserved ✅ 532ms
Format Validation.Event Has Timestamp ✅ 531ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 531ms
Retry Behavior.Retries On 503 ❌ 5537ms
Retry Behavior.Does Not Retry On 400 ✅ 2534ms
Retry Behavior.Does Not Retry On 401 ✅ 2532ms
Retry Behavior.Respects Retry After Header ❌ 5537ms
Retry Behavior.Implements Backoff ❌ 15548ms
Retry Behavior.Retries On 500 ❌ 5540ms
Retry Behavior.Retries On 502 ❌ 5535ms
Retry Behavior.Retries On 504 ❌ 5538ms
Retry Behavior.Max Retries Respected ❌ 15546ms
Deduplication.Generates Unique Uuids ✅ 538ms
Deduplication.Preserves Uuid On Retry ❌ 5535ms
Deduplication.Preserves Uuid And Timestamp On Retry ❌ 10541ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ❌ 5541ms
Deduplication.No Duplicate Events In Batch ✅ 539ms
Deduplication.Different Events Have Different Uuids ✅ 531ms
Compression.Sends Gzip When Enabled ✅ 532ms
Batch Format.Uses Proper Batch Structure ✅ 531ms
Batch Format.Flush With No Events Sends Nothing ✅ 519ms
Batch Format.Multiple Events Batched Together ✅ 521ms
Error Handling.Does Not Retry On 403 ✅ 2533ms
Error Handling.Does Not Retry On 413 ✅ 2532ms
Error Handling.Retries On 408 ❌ 5536ms

Failures

retry_behavior.retries_on_503

Expected at least 3 requests, got 1

retry_behavior.respects_retry_after_header

Expected at least 2 requests, got 1

retry_behavior.implements_backoff

Expected at least 3 requests, got 1

retry_behavior.retries_on_500

Expected at least 2 requests, got 1

retry_behavior.retries_on_502

Expected at least 2 requests, got 1

retry_behavior.retries_on_504

Expected at least 2 requests, got 1

retry_behavior.max_retries_respected

Expected 4 requests, got 1

deduplication.preserves_uuid_on_retry

Need at least 2 requests to check retry

deduplication.preserves_uuid_and_timestamp_on_retry

Expected at least 3 requests, got 1

deduplication.preserves_uuid_and_timestamp_on_batch_retry

Expected at least 2 requests, got 1

error_handling.retries_on_408

Expected at least 2 requests, got 1

Feature_Flags Tests

✅ 17/17 tests passed

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

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

posthog-php-socket Compliance Report

Date: 2026-10-04T06:08:36.682309+00:00
Duration: 148266ms

⚠️ Some Tests Failed

36/47 tests passed, 11 failed


Capture Tests

⚠️ 19/30 tests passed, 11 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 27ms
Format Validation.Event Has Uuid ✅ 522ms
Format Validation.Event Has Lib Properties ✅ 523ms
Format Validation.Distinct Id Is String ✅ 524ms
Format Validation.Token Is Present ✅ 524ms
Format Validation.Custom Properties Preserved ✅ 525ms
Format Validation.Event Has Timestamp ✅ 524ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 526ms
Retry Behavior.Retries On 503 ❌ 9233ms
Retry Behavior.Does Not Retry On 400 ✅ 2526ms
Retry Behavior.Does Not Retry On 401 ✅ 2528ms
Retry Behavior.Respects Retry After Header ❌ 9235ms
Retry Behavior.Implements Backoff ❌ 19235ms
Retry Behavior.Retries On 500 ❌ 8749ms
Retry Behavior.Retries On 502 ❌ 9231ms
Retry Behavior.Retries On 504 ❌ 8739ms
Retry Behavior.Max Retries Respected ❌ 19246ms
Deduplication.Generates Unique Uuids ✅ 534ms
Deduplication.Preserves Uuid On Retry ❌ 9235ms
Deduplication.Preserves Uuid And Timestamp On Retry ❌ 14240ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ❌ 9237ms
Deduplication.No Duplicate Events In Batch ✅ 32ms
Deduplication.Different Events Have Different Uuids ✅ 525ms
Compression.Sends Gzip When Enabled ✅ 525ms
Batch Format.Uses Proper Batch Structure ✅ 524ms
Batch Format.Flush With No Events Sends Nothing ✅ 520ms
Batch Format.Multiple Events Batched Together ✅ 512ms
Error Handling.Does Not Retry On 403 ✅ 2526ms
Error Handling.Does Not Retry On 413 ✅ 2526ms
Error Handling.Retries On 408 ❌ 5531ms

Failures

retry_behavior.retries_on_503

Expected at least 3 requests, got 1

retry_behavior.respects_retry_after_header

Expected at least 2 requests, got 1

retry_behavior.implements_backoff

Expected at least 3 requests, got 1

retry_behavior.retries_on_500

Expected at least 2 requests, got 1

retry_behavior.retries_on_502

Expected at least 2 requests, got 1

retry_behavior.retries_on_504

Expected at least 2 requests, got 1

retry_behavior.max_retries_respected

Expected 4 requests, got 1

deduplication.preserves_uuid_on_retry

Need at least 2 requests to check retry

deduplication.preserves_uuid_and_timestamp_on_retry

Expected at least 3 requests, got 1

deduplication.preserves_uuid_and_timestamp_on_batch_retry

Expected at least 2 requests, got 1

error_handling.retries_on_408

Expected at least 2 requests, got 1

Feature_Flags Tests

✅ 17/17 tests passed

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

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.

0 participants