From f4354ce544769e5c540a249d47b8f5bf3679f179 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Sun, 5 Jul 2026 12:26:57 +0200 Subject: [PATCH 1/4] feat: add before send callback --- .changeset/bright-owls-filter.md | 5 +++ lib/Client.php | 43 ++++++++++++++++++++++++ lib/PostHog.php | 1 + test/PostHogTest.php | 57 ++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 .changeset/bright-owls-filter.md diff --git a/.changeset/bright-owls-filter.md b/.changeset/bright-owls-filter.md new file mode 100644 index 0000000..46491cb --- /dev/null +++ b/.changeset/bright-owls-filter.md @@ -0,0 +1,5 @@ +--- +"posthog-php": minor +--- + +Add a before_send callback for modifying or dropping fully enriched events. diff --git a/lib/Client.php b/lib/Client.php index 2701ac0..839979a 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -172,6 +172,7 @@ class Client implements FeatureFlagEvaluationsHost * flush_interval_seconds?: int|float, * compress_request?: bool|string, * error_handler?: callable, + * before_send?: callable(array): (array|null), * filename?: string, * is_server?: bool, * flag_definition_cache_provider?: FlagDefinitionCacheProvider, @@ -394,9 +395,51 @@ public function capture(array $message) unset($message["send_feature_flags"]); + $message = $this->applyBeforeSend($message); + if ($message === null) { + return false; + } + return $this->consumer->capture($message); } + /** + * Run the configured before_send callback for a fully enriched capture event. + * + * @param array $message + * @return array|null + */ + private function applyBeforeSend(array $message): ?array + { + $beforeSend = $this->options['before_send'] ?? null; + if ($beforeSend === null) { + return $message; + } + + if (!is_callable($beforeSend)) { + error_log('[PostHog][Client] before_send is not callable; dropping event'); + return null; + } + + try { + $result = $beforeSend($message); + } catch (Throwable $e) { + error_log('[PostHog][Client] before_send callback threw; dropping event: ' . $e->getMessage()); + return null; + } + + if ($result === null) { + return null; + } + + if (!is_array($result)) { + error_log('[PostHog][Client] before_send must return an array or null; dropping event'); + return null; + } + + return $result; + } + /** * Captures an exception as a PostHog error tracking event. * diff --git a/lib/PostHog.php b/lib/PostHog.php index f961ec4..1b90b4b 100644 --- a/lib/PostHog.php +++ b/lib/PostHog.php @@ -49,6 +49,7 @@ class PostHog * flush_interval_seconds?: int|float, * compress_request?: bool|string, * error_handler?: callable, + * before_send?: callable(array): (array|null), * filename?: string, * flag_definition_cache_provider?: FlagDefinitionCacheProvider, * error_tracking?: array{ diff --git a/test/PostHogTest.php b/test/PostHogTest.php index fcb0e07..edf8cd7 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -487,6 +487,63 @@ public function testBatchSizeOneFlushesImmediately(): void $this->assertSame('/batch/', $httpClient->calls[0]['path']); } + public function testBeforeSendCanModifyFullyEnrichedEvent(): void + { + $httpClient = new MockedHttpClient("app.posthog.com"); + $sawFullyEnrichedEvent = false; + $client = new Client( + self::FAKE_API_KEY, + [ + "batch_size" => 1, + "before_send" => function (array $event) use (&$sawFullyEnrichedEvent): array { + $sawFullyEnrichedEvent = isset( + $event['properties']['$lib'], + $event['properties']['$lib_version'], + $event['properties']['$lib_consumer'], + $event['properties']['$is_server'] + ); + unset($event['properties']['secret']); + $event['properties']['before_send'] = true; + + return $event; + }, + ], + $httpClient, + null, + false + ); + + $this->assertTrue($client->capture([ + "distinctId" => "john", + "event" => "Module PHP Event", + "properties" => ["secret" => "remove"], + ])); + + $this->assertTrue($sawFullyEnrichedEvent); + $payload = json_decode($httpClient->calls[0]['payload'], true); + $properties = $payload['batch'][0]['properties']; + $this->assertTrue($properties['before_send']); + $this->assertArrayNotHasKey('secret', $properties); + } + + public function testBeforeSendCanDropEvent(): void + { + $httpClient = new MockedHttpClient("app.posthog.com"); + $client = new Client( + self::FAKE_API_KEY, + ["batch_size" => 1, "before_send" => static fn(array $event): ?array => null], + $httpClient, + null, + false + ); + + $this->assertFalse($client->capture([ + "distinctId" => "john", + "event" => "Module PHP Event", + ])); + $this->assertSame([], $httpClient->calls ?? []); + } + /** * @dataProvider facadeNoOpBeforeInitCases From 2affa7a1482a92ed4675246680bce431952f5f12 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Sun, 5 Jul 2026 13:38:57 +0200 Subject: [PATCH 2/4] test: cover before send edge cases --- test/PostHogTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/PostHogTest.php b/test/PostHogTest.php index edf8cd7..b7df78b 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -7,6 +7,7 @@ use Exception; use PHPUnit\Framework\TestCase; +use RuntimeException; use PostHog\Client; use PostHog\Consumer\NoOp; use PostHog\PostHog; @@ -544,6 +545,34 @@ public function testBeforeSendCanDropEvent(): void $this->assertSame([], $httpClient->calls ?? []); } + /** + * @dataProvider invalidBeforeSendCases + */ + public function testBeforeSendInvalidCallbackPathsDropEvent(mixed $beforeSend): void + { + $httpClient = new MockedHttpClient("app.posthog.com"); + $client = new Client( + self::FAKE_API_KEY, + ["batch_size" => 1, "before_send" => $beforeSend], + $httpClient, + null, + false + ); + + $this->assertFalse($client->capture([ + "distinctId" => "john", + "event" => "Module PHP Event", + ])); + $this->assertSame([], $httpClient->calls ?? []); + } + + public static function invalidBeforeSendCases(): iterable + { + yield 'throws' => [static fn(array $event): array => throw new RuntimeException('before_send failed')]; + yield 'returns non-array' => [static fn(array $event): string => 'invalid']; + yield 'not callable' => ['not-callable']; + } + /** * @dataProvider facadeNoOpBeforeInitCases From a80bf968e7522b61b684646ed36f74bc041530fc Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 6 Jul 2026 09:08:27 +0200 Subject: [PATCH 3/4] fix: capture events with invalid before_send callback --- lib/Client.php | 8 ++++---- test/PostHogTest.php | 31 +++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/Client.php b/lib/Client.php index 839979a..dfc4d8f 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -417,8 +417,8 @@ private function applyBeforeSend(array $message): ?array } if (!is_callable($beforeSend)) { - error_log('[PostHog][Client] before_send is not callable; dropping event'); - return null; + error_log('[PostHog][Client] before_send is not callable; sending original event'); + return $message; } try { @@ -433,8 +433,8 @@ private function applyBeforeSend(array $message): ?array } if (!is_array($result)) { - error_log('[PostHog][Client] before_send must return an array or null; dropping event'); - return null; + error_log('[PostHog][Client] before_send must return an array or null; sending original event'); + return $message; } return $result; diff --git a/test/PostHogTest.php b/test/PostHogTest.php index b7df78b..d2a1191 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -545,10 +545,31 @@ public function testBeforeSendCanDropEvent(): void $this->assertSame([], $httpClient->calls ?? []); } + public function testBeforeSendThrowingCallbackDropsEvent(): void + { + $httpClient = new MockedHttpClient("app.posthog.com"); + $client = new Client( + self::FAKE_API_KEY, + [ + "batch_size" => 1, + "before_send" => static fn(array $event): array => throw new RuntimeException('before_send failed'), + ], + $httpClient, + null, + false + ); + + $this->assertFalse($client->capture([ + "distinctId" => "john", + "event" => "Module PHP Event", + ])); + $this->assertSame([], $httpClient->calls ?? []); + } + /** * @dataProvider invalidBeforeSendCases */ - public function testBeforeSendInvalidCallbackPathsDropEvent(mixed $beforeSend): void + public function testBeforeSendInvalidCallbackPathsCaptureOriginalEvent(mixed $beforeSend): void { $httpClient = new MockedHttpClient("app.posthog.com"); $client = new Client( @@ -559,16 +580,18 @@ public function testBeforeSendInvalidCallbackPathsDropEvent(mixed $beforeSend): false ); - $this->assertFalse($client->capture([ + $this->assertTrue($client->capture([ "distinctId" => "john", "event" => "Module PHP Event", + "properties" => ["original" => true], ])); - $this->assertSame([], $httpClient->calls ?? []); + + $payload = json_decode($httpClient->calls[0]['payload'], true); + $this->assertTrue($payload['batch'][0]['properties']['original']); } public static function invalidBeforeSendCases(): iterable { - yield 'throws' => [static fn(array $event): array => throw new RuntimeException('before_send failed')]; yield 'returns non-array' => [static fn(array $event): string => 'invalid']; yield 'not callable' => ['not-callable']; } From 84fdf468a685c335f39602962cbc52f3666921e8 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 6 Jul 2026 09:11:30 +0200 Subject: [PATCH 4/4] fix: run before_send for identify and alias --- lib/Client.php | 10 ++++++++ test/PostHogTest.php | 61 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/lib/Client.php b/lib/Client.php index dfc4d8f..8ed3337 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -497,6 +497,11 @@ public function identify(array $message) $message["event"] = '$identify'; unset($message["send_feature_flags"]); + $message = $this->applyBeforeSend($message); + if ($message === null) { + return false; + } + return $this->consumer->identify($message); } @@ -1847,6 +1852,11 @@ public function alias(array $message) $message['distinct_id'] = null; unset($message['alias']); + $message = $this->applyBeforeSend($message); + if ($message === null) { + return false; + } + return $this->consumer->alias($message); } diff --git a/test/PostHogTest.php b/test/PostHogTest.php index d2a1191..3906cc3 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -545,6 +545,67 @@ public function testBeforeSendCanDropEvent(): void $this->assertSame([], $httpClient->calls ?? []); } + public function testBeforeSendRunsForIdentify(): void + { + $httpClient = new MockedHttpClient("app.posthog.com"); + $client = new Client( + self::FAKE_API_KEY, + [ + "batch_size" => 1, + "before_send" => static function (array $event): array { + $event['properties']['before_send'] = true; + unset($event['properties']['secret']); + return $event; + }, + ], + $httpClient, + null, + false + ); + + $this->assertTrue($client->identify([ + "distinctId" => "john", + "properties" => ["secret" => "remove"], + ])); + + $payload = json_decode($httpClient->calls[0]['payload'], true); + $event = $payload['batch'][0]; + $this->assertSame('$identify', $event['event']); + $this->assertTrue($event['properties']['before_send']); + $this->assertArrayNotHasKey('secret', $event['properties']); + } + + public function testBeforeSendRunsForAlias(): void + { + $httpClient = new MockedHttpClient("app.posthog.com"); + $client = new Client( + self::FAKE_API_KEY, + [ + "batch_size" => 1, + "before_send" => static function (array $event): array { + $event['properties']['before_send'] = true; + unset($event['properties']['secret']); + return $event; + }, + ], + $httpClient, + null, + false + ); + + $this->assertTrue($client->alias([ + "distinctId" => "john", + "alias" => "anonymous-id", + "properties" => ["secret" => "remove"], + ])); + + $payload = json_decode($httpClient->calls[0]['payload'], true); + $event = $payload['batch'][0]; + $this->assertSame('$create_alias', $event['event']); + $this->assertTrue($event['properties']['before_send']); + $this->assertArrayNotHasKey('secret', $event['properties']); + } + public function testBeforeSendThrowingCallbackDropsEvent(): void { $httpClient = new MockedHttpClient("app.posthog.com");