fix(error-tracking): add flat $exception_type and $exception_message - #253
posthog[bot] wants to merge 1 commit into
Conversation
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
posthog-php-lib_curl Compliance ReportDate: 2026-10-04T06:08:17.174587+00:00 ✅ All Tests Passed!47/47 tests passed Capture Tests✅ 30/30 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
posthog-php-fork_curl Compliance ReportDate: 2026-10-04T06:08:16.771212+00:00
|
| 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 |
posthog-php-socket Compliance ReportDate: 2026-10-04T06:08:36.682309+00:00
|
| 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 |
💡 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_messageproperties 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_messageproperties on the enqueued event, in addition to the structured$exception_list.Compliance matrix note (posthog-php, n2 — Capture Exception):
What changed
ExceptionPayloadBuilder::getPrimarySummaryProperties(), mirroring the existinggetPrimaryHandled()helper, returns$exception_type/$exception_messagefrom the primary (first, outermost)$exception_listentry. Keys are omitted when the primary entry has no stringtype/value, so a malformed list never emits empty summary properties.Client::captureException()and the automaticExceptionCapture::sendExceptionEvent()(uncaught-exception and error-handler capture).Backwards compatibility
Additive. Two new properties are added to
$exceptionevents; nothing emitted today is removed or renamed, no public signature changes, and$exception_listkeeps its existing shape. The values are derived from data already on the event, so no new data is collected. Caller-supplied$additionalPropertiesprecedence 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.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/phpcson the changed files — 0 errors.composer api:checkpasses aftercomposer 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
Captureand theException Event Metadatacontract.📝 Checklist
If releasing new changes
pnpm changeto generate a change intent fileA
posthog-php: patchintent was added manually in the existing.changesetformat; 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