diff --git a/.changeset/flat-exception-summary-properties.md b/.changeset/flat-exception-summary-properties.md new file mode 100644 index 0000000..3168739 --- /dev/null +++ b/.changeset/flat-exception-summary-properties.md @@ -0,0 +1,5 @@ +--- +"posthog-php": patch +--- + +Add flat `$exception_type` and `$exception_message` properties to captured exception events, mirroring the outermost entry of `$exception_list`. diff --git a/api/public-api.json b/api/public-api.json index d164eae..b9dff58 100644 --- a/api/public-api.json +++ b/api/public-api.json @@ -2031,6 +2031,24 @@ } ] }, + "getPrimarySummaryProperties": { + "static": true, + "abstract": false, + "final": false, + "returnType": "array", + "parameters": [ + { + "name": "exceptionList", + "type": "array", + "byReference": false, + "variadic": false, + "optional": false, + "default": null, + "defaultConstant": null, + "hasDefault": false + } + ] + }, "overridePrimaryMechanism": { "static": true, "abstract": false, diff --git a/lib/Client.php b/lib/Client.php index 84c6b6b..f4cff0a 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -580,7 +580,8 @@ public function captureException( [ '$exception_list' => $exceptionList, '$exception_handled' => ExceptionPayloadBuilder::getPrimaryHandled($exceptionList), - ] + ], + ExceptionPayloadBuilder::getPrimarySummaryProperties($exceptionList) ); $message = [ diff --git a/lib/ExceptionCapture.php b/lib/ExceptionCapture.php index 32c18c6..d54fb10 100644 --- a/lib/ExceptionCapture.php +++ b/lib/ExceptionCapture.php @@ -419,11 +419,14 @@ private static function sendExceptionEvent( 'line' => $exception->getLine(), ]); - $properties = [ - '$exception_list' => $exceptionList, - '$exception_handled' => ExceptionPayloadBuilder::getPrimaryHandled($exceptionList), - '$exception_source' => $eventSource, - ]; + $properties = array_merge( + [ + '$exception_list' => $exceptionList, + '$exception_handled' => ExceptionPayloadBuilder::getPrimaryHandled($exceptionList), + '$exception_source' => $eventSource, + ], + ExceptionPayloadBuilder::getPrimarySummaryProperties($exceptionList) + ); if ($severity !== null) { $properties['$php_error_severity'] = $severity; diff --git a/lib/ExceptionPayloadBuilder.php b/lib/ExceptionPayloadBuilder.php index a0aa4af..4840300 100644 --- a/lib/ExceptionPayloadBuilder.php +++ b/lib/ExceptionPayloadBuilder.php @@ -123,6 +123,31 @@ public static function getPrimaryHandled(array $exceptionList): bool return (bool) (($exceptionList[0]['mechanism']['handled'] ?? false) === true); } + /** + * Flat `$exception_type`/`$exception_message` properties mirrored from the primary + * (first, outermost) exception. Keys are omitted when the primary entry has no string + * type/value, so a malformed list never emits empty summary properties. + * + * @param array[] $exceptionList Exception entries. + * @return array + */ + public static function getPrimarySummaryProperties(array $exceptionList): array + { + $properties = []; + + $type = $exceptionList[0]['type'] ?? null; + if (is_string($type)) { + $properties['$exception_type'] = $type; + } + + $message = $exceptionList[0]['value'] ?? null; + if (is_string($message)) { + $properties['$exception_message'] = $message; + } + + return $properties; + } + private static function buildThrowableException(\Throwable $exception, int $maxFrames): array { return self::buildSingleException( diff --git a/test/ExceptionCaptureTest.php b/test/ExceptionCaptureTest.php index 2d4dec0..e5fb0c3 100644 --- a/test/ExceptionCaptureTest.php +++ b/test/ExceptionCaptureTest.php @@ -167,6 +167,8 @@ public function testExceptionHandlerCapturesFlushesAndChainsPreviousHandler(): v $event['properties']['$exception_list'][0]['mechanism'] ); $this->assertSame('RuntimeException', $event['properties']['$exception_list'][0]['type']); + $this->assertSame('RuntimeException', $event['properties']['$exception_type']); + $this->assertSame('uncaught boom', $event['properties']['$exception_message']); $this->assertFalse($event['properties']['$process_person_profile']); $this->assertMatchesRegularExpression( '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', diff --git a/test/ExceptionPayloadBuilderTest.php b/test/ExceptionPayloadBuilderTest.php index 5e5b705..caea542 100644 --- a/test/ExceptionPayloadBuilderTest.php +++ b/test/ExceptionPayloadBuilderTest.php @@ -429,6 +429,43 @@ public function testCaptureExceptionSendsExceptionEvent(): void }); } + public function testCaptureExceptionSetsFlatTypeAndMessageProperties(): void + { + $this->client->captureException(new \TypeError('boom'), 'user-flat'); + PostHog::flush(); + + $batchCall = $this->findBatchCall(); + $props = json_decode($batchCall['payload'], true)['batch'][0]['properties']; + + $this->assertSame('TypeError', $props['$exception_type']); + $this->assertSame('boom', $props['$exception_message']); + } + + public function testCaptureExceptionFlatPropertiesMirrorOutermostExceptionOfAChain(): void + { + $cause = new \InvalidArgumentException('root cause'); + $this->client->captureException(new \RuntimeException('wrapped', 0, $cause), 'user-flat-chain'); + PostHog::flush(); + + $batchCall = $this->findBatchCall(); + $props = json_decode($batchCall['payload'], true)['batch'][0]['properties']; + + $this->assertSame('RuntimeException', $props['$exception_type']); + $this->assertSame('wrapped', $props['$exception_message']); + } + + public function testCaptureExceptionFromStringSetsFlatTypeAndMessageProperties(): void + { + $this->client->captureException('something broke', 'user-flat-string'); + PostHog::flush(); + + $batchCall = $this->findBatchCall(); + $props = json_decode($batchCall['payload'], true)['batch'][0]['properties']; + + $this->assertSame('Error', $props['$exception_type']); + $this->assertSame('something broke', $props['$exception_message']); + } + public function testCaptureExceptionUsesOuterExceptionAsPrimaryForChains(): void { $cause = new \InvalidArgumentException('root cause');