Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-owls-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-php": minor
---

Add a before_send callback for modifying or dropping fully enriched events.
43 changes: 43 additions & 0 deletions lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ class Client implements FeatureFlagEvaluationsHost
* flush_interval_seconds?: int|float,
* compress_request?: bool|string,
* error_handler?: callable,
* before_send?: callable(array<string, mixed>): (array<string, mixed>|null),
* filename?: string,
* is_server?: bool,
* flag_definition_cache_provider?: FlagDefinitionCacheProvider,
Expand Down Expand Up @@ -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<string, mixed> $message
* @return array<string, mixed>|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.
*
Expand Down
1 change: 1 addition & 0 deletions lib/PostHog.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class PostHog
* flush_interval_seconds?: int|float,
* compress_request?: bool|string,
* error_handler?: callable,
* before_send?: callable(array<string, mixed>): (array<string, mixed>|null),
* filename?: string,
* flag_definition_cache_provider?: FlagDefinitionCacheProvider,
* error_tracking?: array{
Expand Down
57 changes: 57 additions & 0 deletions test/PostHogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? []);
}
Comment on lines +529 to +545

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing tests for applyBeforeSend error branches

applyBeforeSend has three distinct error/defensive paths — a non-callable before_send, a callback that throws, and a callback that returns a non-array non-null value — but none of these are exercised by the new tests. A misconfigured callback will silently drop every capture call with only an error_log entry, so verifying these branches is important. Given the project's preference for parameterised tests, a @dataProvider covering all five cases (modify, drop-via-null, non-callable, throws, non-array-return) in a single test method would also align with the established pattern in this file.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!



/**
* @dataProvider facadeNoOpBeforeInitCases
Expand Down
Loading