Skip to content
Open
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 .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- main
paths:
- "**.php"
- "sdk_compliance_adapter/**"
- "bin/posthog"
- "composer.json"
- "composer.lock"
Expand Down Expand Up @@ -99,6 +100,10 @@ jobs:
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: ./vendor/bin/phpunit --bootstrap vendor/autoload.php --configuration phpunit.xml

- name: Run adapter fidelity and report checker tests
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: python3 -m unittest discover -s sdk_compliance_adapter -v

coverage:
runs-on: ubuntu-latest
needs:
Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ Run the test command used in CI:
XDEBUG_MODE=coverage ./vendor/bin/phpunit --bootstrap vendor/autoload.php --configuration phpunit.xml --coverage-text
```

The PHPUnit transport tests use a loopback HTTP server, not live PostHog endpoints. They require the cURL extension, the `curl` and `gzip` commands, and permission to start PHP subprocesses and listen on loopback ports.

Run the adapter fidelity and report checker tests (Python standard library only):

```bash
python3 -m unittest discover -s sdk_compliance_adapter -v
```

CI also runs PHP_CodeSniffer with `phpcs.xml`. You can run an equivalent local check with:

```bash
Expand Down
26 changes: 12 additions & 14 deletions sdk_compliance_adapter/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from server import Controller, Relay


MOCK_PORT = int(os.environ.get("TEST_MOCK_PORT", "19276"))
PROXY_PORT = int(os.environ.get("TEST_PROXY_PORT", "19277"))
MOCK_PORT = int(os.environ.get("TEST_MOCK_PORT", "0"))
PROXY_PORT = int(os.environ.get("TEST_PROXY_PORT", "0"))


class MockHandler(BaseHTTPRequestHandler):
Expand All @@ -33,21 +33,19 @@ def log_message(self, *_):

class AdapterTest(unittest.TestCase):
def setUp(self):
self.environment = patch.dict(os.environ, {"PROXY_PORT": str(PROXY_PORT)})
self.environment.start()
self.addCleanup(self.environment.stop)
self.mock = ThreadingHTTPServer(("127.0.0.1", MOCK_PORT), MockHandler)
self.addCleanup(self.mock.server_close)
self.mock.requests = []
self.mock.statuses = []
self.thread = threading.Thread(target=self.mock.serve_forever, daemon=True)
self.thread.start()
self.environment = patch.dict(os.environ, {"PROXY_PORT": str(PROXY_PORT)})
self.environment.start()
self.addCleanup(self.thread.join)
self.addCleanup(self.mock.shutdown)
self.controller = Controller()

def tearDown(self):
self.controller.reset()
self.mock.shutdown()
self.mock.server_close()
self.thread.join()
self.environment.stop()
self.addCleanup(self.controller.reset)

def call(self, path, data=None):
status, result = self.controller.handle("GET" if data is None else "POST", path, data or {})
Expand All @@ -58,7 +56,7 @@ def init(self, consumer="lib_curl", **options):
os.environ["POSTHOG_CONSUMER"] = consumer
self.mock.requests.clear()
self.mock.statuses.clear()
self.call("/init", {"host": f"http://127.0.0.1:{MOCK_PORT}",
self.call("/init", {"host": f"http://127.0.0.1:{self.mock.server_address[1]}",
"api_key": "phc_local_test", **options})

def capture(self):
Expand Down Expand Up @@ -172,7 +170,7 @@ def accept():
relay = Relay(("127.0.0.1", PROXY_PORT), target.getsockname())
relay_thread = threading.Thread(target=relay.serve_forever)
relay_thread.start()
with socket.create_connection(relay.server_address) as client:
with socket.create_connection(relay.server_address, timeout=5) as client:
self.assertTrue(accepted.wait(5))
relay.shutdown()
relay.server_close()
Expand Down Expand Up @@ -207,7 +205,7 @@ def respond():
relay_thread = threading.Thread(target=relay.serve_forever)
relay_thread.start()
try:
with socket.create_connection(relay.server_address) as client:
with socket.create_connection(relay.server_address, timeout=5) as client:
client.sendall(request)
data = b""
while True:
Expand Down
5 changes: 2 additions & 3 deletions test/ClockMockTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Symfony\Component\Clock\Clock;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Clock\NativeClock;

/**
* Trait providing time mocking functionality for tests using Symfony Clock.
Expand All @@ -26,13 +25,13 @@ protected function executeAtFrozenDateTime(\DateTimeInterface $dateTime, callabl
? $dateTime
: \DateTimeImmutable::createFromInterface($dateTime));

$previousClock = Clock::get();
Clock::set($mockClock);

try {
return $callback();
} finally {
// Reset to real clock
Clock::set(new NativeClock());
Clock::set($previousClock);
}
}
}
40 changes: 40 additions & 0 deletions test/ClockMockTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace PostHog\Test;

use PHPUnit\Framework\TestCase;
use RuntimeException;
use Symfony\Component\Clock\Clock;
use Symfony\Component\Clock\MockClock;

class ClockMockTraitTest extends TestCase
{
use ClockMockTrait;

public function testNestedFrozenClocksRestoreTheirCallerEvenOnException(): void
{
$original = Clock::get();
$outer = new MockClock('2024-01-01T00:00:00+00:00');
Clock::set($outer);
try {
$value = $this->executeAtFrozenDateTime(new \DateTimeImmutable('2025-01-01 UTC'), function (): string {
$inner = Clock::get();
try {
$this->executeAtFrozenDateTime(new \DateTimeImmutable('2026-01-01 UTC'), function (): void {
self::assertSame('2026-01-01', Clock::get()->now()->format('Y-m-d'));
throw new RuntimeException('callback failed');
});
self::fail('Expected callback exception');
} catch (RuntimeException $exception) {
self::assertSame('callback failed', $exception->getMessage());
}
self::assertSame($inner, Clock::get());
return Clock::get()->now()->format('Y-m-d');
});
self::assertSame('2025-01-01', $value);
self::assertSame($outer, Clock::get());
} finally {
Clock::set($original);
}
}
}
168 changes: 88 additions & 80 deletions test/ConsumerFileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,124 +4,132 @@

use PHPUnit\Framework\TestCase;
use PostHog\Client;
use PostHog\PostHog;

class ConsumerFileTest extends TestCase
{
private $client;
private $filename = "/tmp/posthog.log";
private Client $client;
private string $filename;

public function setUp(): void
{
date_default_timezone_set("UTC");
if (file_exists($this->filename())) {
unlink($this->filename());
}

$this->client = new Client(
"BrpS4SctoaCCsyjlnlun3OzyNJAafdlv__jUWaaJWXg",
array(
"consumer" => "file",
"filename" => $this->filename,
)
);
$this->filename = tempnam(sys_get_temp_dir(), 'posthog-file-test-');
$this->client = new Client('test-key', ['consumer' => 'file', 'filename' => $this->filename]);
}

public function tearDown(): void
{
$this->client->shutdown();
if (file_exists($this->filename)) {
unlink($this->filename);
}
}

public function testCapture(): void
{
self::assertTrue(
$this->client->capture(
array(
"distinctId" => "some-user",
"event" => "File PHP Event - Microtime",
"timestamp" => time(),
)
)
);
$this->checkWritten("File PHP Event - Microtime");
self::assertTrue($this->client->capture([
'distinctId' => 'some-user',
'event' => 'File PHP Event - Microtime',
'timestamp' => 1704067200,
]));
$event = $this->writtenEvent('File PHP Event - Microtime');
self::assertSame('some-user', $event['distinct_id']);
self::assertSame('2024-01-01T00:00:00+00:00', $event['timestamp']);
}

public function testIdentify(): void
{
self::assertTrue(
$this->client->identify(
array(
"distinctId" => "Calvin",
"properties" => array(
"loves_php" => false,
"type" => "posthog.log",
"birthday" => time(),
),
)
)
);
$this->checkWritten('$identify');
self::assertTrue($this->client->identify([
'distinctId' => 'Calvin',
'properties' => ['loves_php' => false, 'birthday' => 1704067200],
]));
$event = $this->writtenEvent('$identify');
self::assertSame('Calvin', $event['distinct_id']);
self::assertFalse($event['properties']['loves_php']);
self::assertSame(1704067200, $event['properties']['birthday']);
}

public function testAlias(): void
{
self::assertTrue(
$this->client->alias(
array(
"alias" => "previous-id",
"distinctId" => "user-id",
)
)
);

$this->checkWritten('$create_alias');
self::assertTrue($this->client->alias(['alias' => 'previous-id', 'distinctId' => 'user-id']));
$event = $this->writtenEvent('$create_alias');
self::assertSame('previous-id', $event['properties']['alias']);
self::assertSame('user-id', $event['properties']['distinct_id']);
}

public function testSend(): void
{
for ($i = 0; $i < 200; ++$i) {
$this->client->capture(
array(
"distinctId" => "distinctId",
"event" => "event",
)
self::assertTrue($this->client->capture(['distinctId' => 'distinctId', 'event' => "event-$i"]));
}
$this->client->shutdown();
$server = new LocalHttpServer();
try {
$process = proc_open(
[PHP_BINARY, __DIR__ . '/../send.php', '--apiKey', 'test-key', '--file', $this->filename],
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
null,
array_merge(getenv(), [PostHog::ENV_HOST => 'http://' . $server->address()])
);
self::assertIsResource($process);
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
self::assertSame(0, proc_close($process), $errors);
self::assertSame('sent 200 from 200 requests successfully', trim($output));
self::assertFileDoesNotExist($this->filename);
$requests = $server->requests();
self::assertCount(2, $requests);
$events = [];
foreach ($requests as $request) {
self::assertSame('POST /batch/ HTTP/1.1', $request['requestLine']);
$payload = json_decode($request['body'], true, 512, JSON_THROW_ON_ERROR);
self::assertSame('test-key', $payload['api_key']);
self::assertCount(100, $payload['batch']);
$events = array_merge($events, $payload['batch']);
}
self::assertSame(
array_map(static fn(int $i): string => "event-$i", range(0, 199)),
array_column($events, 'event')
);
self::assertSame(array_fill(0, 200, 'distinctId'), array_column($events, 'distinct_id'));
} finally {
$server->stop();
}
exec("php send.php --apiKey BrpS4SctoaCCsyjlnlun3OzyNJAafdlv__jUWaaJWXg --file /tmp/posthog.log", $output);
self::assertSame("sent 200 from 200 requests successfully", trim(implode('', $output)));
self::assertFileDoesNotExist($this->filename());
}

public function testProductionProblems(): void
{
// Open to a place where we should not have write access.
$client = new Client(
"BrpS4SctoaCCsyjlnlun3OzyNJAafdlv__jUWaaJWXg",
array(
"consumer" => "file",
"filename" => "/dev/x/xxxxxxx",
)
);

$captured = $client->capture(array("distinctId" => "some-user", "event" => "my event"));
self::assertFalse($captured);
}

private function checkWritten($event): void
{
exec("wc -l " . $this->filename, $output);
$out = trim($output[0]);
self::assertSame($out, "1 " . $this->filename);
$str = file_get_contents($this->filename);
$json = json_decode(trim($str));
self::assertObjectNotHasProperty('type', $json);
self::assertSame($event, $json->event);
unlink($this->filename);
$warnings = [];
set_error_handler(static function (int $severity, string $message) use (&$warnings): bool {
$warnings[] = [$severity, $message];
return true;
}, E_WARNING);
try {
$client = new Client('test-key', [
'consumer' => 'file',
'filename' => $this->filename . '/not-a-directory',
]);
} finally {
restore_error_handler();
}
self::assertCount(2, $warnings);
self::assertSame(E_WARNING, $warnings[0][0]);
self::assertStringContainsString('fopen(', $warnings[0][1]);
self::assertStringContainsString('chmod(', $warnings[1][1]);
self::assertFalse($client->capture(['distinctId' => 'some-user', 'event' => 'my event']));
}

public function filename(): string
private function writtenEvent(string $name): array
{
return '/tmp/posthog.log';
$lines = file($this->filename, FILE_IGNORE_NEW_LINES);
self::assertCount(1, $lines);
$event = json_decode($lines[0], true, 512, JSON_THROW_ON_ERROR);
self::assertArrayNotHasKey('type', $event);
self::assertSame($name, $event['event']);
return $event;
}
}
Loading
Loading