Skip to content
Merged
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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Specifically, the [PHP integration](https://posthog.com/docs/integrations/php-in
## Features

- ✅ Event capture and user identification
- ✅ Error tracking with manual exception capture
- ✅ Opt-in automatic PHP exception, error, and fatal shutdown capture
- ✅ Feature flag local evaluation
- ✅ **Feature flag dependencies** (new!) - Create conditional flags based on other flags
- ✅ Multivariate flags and payloads
Expand All @@ -21,6 +23,40 @@ Specifically, the [PHP integration](https://posthog.com/docs/integrations/php-in
1. Copy `.env.example` to `.env` and add your PostHog credentials
2. Run `php example.php` to see interactive examples of all features

## Error Tracking

Manual exception capture:

```php
PostHog::captureException($exception, 'user-123', [
'$current_url' => 'https://example.com/settings',
]);
```

Opt-in automatic capture from the core SDK:

```php
PostHog::init('phc_xxx', [
'error_tracking' => [
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

:chef-kiss:

'enabled' => true,
'capture_errors' => true,
'excluded_exceptions' => [
\InvalidArgumentException::class,
],
'context_provider' => static function (array $payload): array {
return [
'distinctId' => $_SESSION['user_id'] ?? null,
'properties' => [
'$current_url' => $_SERVER['REQUEST_URI'] ?? null,
],
];
},
],
]);
```

Auto error tracking is off by default. When enabled, the SDK chains existing exception and error handlers instead of replacing app behavior.

## Questions?

### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
Expand Down
124 changes: 115 additions & 9 deletions example.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

// phpcs:disable PSR1.Files.SideEffects

// PostHog PHP library example
//
// This script demonstrates various PostHog PHP SDK capabilities including:
Expand Down Expand Up @@ -89,9 +91,10 @@ function loadEnvFile()
echo "3. Feature flag dependencies examples\n";
echo "4. Context management and tagging examples\n";
echo "5. ETag polling examples (for local evaluation)\n";
echo "6. Run all examples\n";
echo "7. Exit\n";
$choice = trim(readline("\nEnter your choice (1-7): "));
echo "6. Error tracking examples\n";
echo "7. Run all examples\n";
echo "8. Exit\n";
$choice = trim(readline("\nEnter your choice (1-8): "));

function identifyAndCaptureExamples()
{
Expand Down Expand Up @@ -266,7 +269,11 @@ function flagDependencyExamples()
[],
true
);
echo "📊 Beta feature comparison - @example.com: " . json_encode($beta1) . ", regular: " . json_encode($beta2) . "\n";
echo "📊 Beta feature comparison - @example.com: "
. json_encode($beta1)
. ", regular: "
. json_encode($beta2)
. "\n";

echo "\n🎯 Results Summary:\n";
echo " - Flag dependencies evaluated locally: " . ($result1 != $result2 ? "✅ YES" : "❌ NO") . "\n";
Expand Down Expand Up @@ -301,7 +308,10 @@ function flagDependencyExamples()
true
);
if ($dependentResult3 !== "breaking-bad") {
echo " ❌ Something went wrong evaluating 'multivariate-root-flag' with pineapple@example.com. Expected 'breaking-bad', got '" . json_encode($dependentResult3) . "'\n";
echo " ❌ Something went wrong evaluating 'multivariate-root-flag' with pineapple@example.com. "
. "Expected 'breaking-bad', got '"
. json_encode($dependentResult3)
. "'\n";
} else {
echo "✅ 'multivariate-root-flag' with email pineapple@example.com succeeded\n";
}
Expand All @@ -316,7 +326,10 @@ function flagDependencyExamples()
true
);
if ($dependentResult4 !== "the-wire") {
echo " ❌ Something went wrong evaluating multivariate-root-flag with mango@example.com. Expected 'the-wire', got '" . json_encode($dependentResult4) . "'\n";
echo " ❌ Something went wrong evaluating multivariate-root-flag with mango@example.com. "
. "Expected 'the-wire', got '"
. json_encode($dependentResult4)
. "'\n";
} else {
echo "✅ 'multivariate-root-flag' with email mango@example.com succeeded\n";
}
Expand Down Expand Up @@ -490,14 +503,101 @@ function etagPollingExamples()
if ($beforeEtag === $afterEtag && $beforeEtag !== null) {
echo "No change (304 Not Modified) - $currentFlagCount flag(s)\n";
} else {
echo "🔄 Flags updated! New ETag: " . ($afterEtag ? substr($afterEtag, 0, 20) . "..." : "none") . " - $currentFlagCount flag(s)\n";
echo "🔄 Flags updated! New ETag: "
. ($afterEtag ? substr($afterEtag, 0, 20) . "..." : "none")
. " - $currentFlagCount flag(s)\n";
}

$iteration++;
sleep(5);
}
}

function errorTrackingExamples()
{
echo "\n" . str_repeat("=", 60) . "\n";
echo "ERROR TRACKING EXAMPLES\n";
echo str_repeat("=", 60) . "\n";

PostHog::init(
$_ENV['POSTHOG_PROJECT_API_KEY'],
[
'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com',
Comment thread
hpouillot marked this conversation as resolved.
'debug' => true,
'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://'),
'error_tracking' => [
'enabled' => true,
'context_provider' => static function (array $payload): array {
return [
'distinctId' => 'sdk-demo-user',
'properties' => [
'$error_source' => $payload['source'],
],
];
},
],
],
null,
$_ENV['POSTHOG_PERSONAL_API_KEY']
);

echo "Auto capture enabled for uncaught exceptions, PHP errors, and fatal shutdown errors.\n";
echo "The demo below still uses manual capture so it can finish without crashing the process.\n\n";

// 1. Capture a plain string error (no user context)
echo "1. Capturing anonymous string error...\n";
PostHog::captureException('Something went wrong during startup');
echo " -> sent with auto-generated distinct_id, \$process_person_profile=false\n\n";

// 2. Capture an exception for a known user
echo "2. Capturing exception for a known user...\n";
try {
throw new \RuntimeException('Database connection failed');
} catch (\RuntimeException $e) {
PostHog::captureException($e, 'user-123');
}
echo " -> sent as \$exception event with stacktrace\n\n";

// 3. Capture with additional context properties
echo "3. Capturing exception with request context...\n";
try {
throw new \InvalidArgumentException('Invalid email address provided');
} catch (\InvalidArgumentException $e) {
PostHog::captureException($e, 'user-456', [
'$current_url' => 'https://example.com/signup',
'$request_method' => 'POST',
'form_field' => 'email',
]);
}
echo " -> sent with URL and request context\n\n";

// 4. Capture a chained exception (cause + wrapper both appear in \$exception_list)
echo "4. Capturing chained exception...\n";
try {
try {
throw new \PDOException('SQLSTATE[HY000]: General error: disk full');
} catch (\PDOException $cause) {
throw new \RuntimeException('Failed to save user record', 0, $cause);
}
} catch (\RuntimeException $e) {
PostHog::captureException($e, 'user-789');
}
echo " -> sent with 2 entries in \$exception_list (cause + wrapper)\n\n";

// 5. Capture a PHP Error (not just Exception)
echo "5. Capturing a TypeError (PHP Error subclass)...\n";
try {
$result = array_sum('not-an-array');
} catch (\TypeError $e) {
PostHog::captureException($e, 'user-123');
}
echo " -> any Throwable (Error or Exception) is accepted\n\n";

PostHog::flush();
echo "Flushed all events.\n";
echo "Check your PostHog dashboard -> Error Tracking to see the captured exceptions.\n";
}

function runAllExamples()
{
identifyAndCaptureExamples();
Expand All @@ -510,6 +610,9 @@ function runAllExamples()
echo "\n" . str_repeat("-", 60) . "\n";

contextManagementExamples();
echo "\n" . str_repeat("-", 60) . "\n";

errorTrackingExamples();

echo "\n🎉 All examples completed!\n";
echo " (ETag polling skipped - run separately with option 5)\n";
Expand All @@ -533,13 +636,16 @@ function runAllExamples()
etagPollingExamples();
break;
case '6':
runAllExamples();
errorTrackingExamples();
break;
case '7':
runAllExamples();
break;
case '8':
echo "👋 Goodbye!\n";
exit(0);
default:
echo "❌ Invalid choice. Please run the script again and choose 1-7.\n";
echo "❌ Invalid choice. Please run the script again and choose 1-8.\n";
exit(1);
}

Expand Down
56 changes: 55 additions & 1 deletion lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ class Client
*/
private $debug;

/**
* @var array<string, mixed>
*/
private $options;

/**
* Create a new posthog object with your app's API key
* key
Expand All @@ -100,6 +105,7 @@ public function __construct(
) {
$this->apiKey = $apiKey;
$this->personalAPIKey = $personalAPIKey;
$this->options = $options;
$this->debug = $options["debug"] ?? false;
$Consumer = self::CONSUMERS[$options["consumer"] ?? "lib_curl"];
$this->consumer = new $Consumer($apiKey, $options, $httpClient);
Expand All @@ -120,6 +126,8 @@ public function __construct(
$this->distinctIdsFeatureFlagsReported = new SizeLimitedHash(SIZE_LIMIT);
$this->flagsEtag = null;

ExceptionCapture::configure($this, $options['error_tracking'] ?? []);

// Populate featureflags and grouptypemapping if possible
if (
count($this->featureFlags) == 0
Expand Down Expand Up @@ -155,7 +163,8 @@ public function capture(array $message)
$flags = [];

if (count($this->featureFlags) != 0) {
# Local evaluation is enabled, flags are loaded, so try and get all flags we can without going to the server
// Local evaluation is enabled, flags are loaded, so try and get all flags
// we can without going to the server.
$flags = $this->getAllFlags($message["distinct_id"], $message["groups"], [], [], true);
} else {
$flags = $this->fetchFeatureVariants($message["distinct_id"], $message["groups"]);
Expand All @@ -178,6 +187,51 @@ public function capture(array $message)
return $this->consumer->capture($message);
}

/**
* Captures an exception as a PostHog error tracking event.
*
* @param \Throwable|string $exception The exception to capture or a plain string message
* @param string|null $distinctId User ID; a random UUID is used when omitted (no person profile created)
* @param array $additionalProperties Extra properties merged into the event
* @return bool whether the capture call succeeded
*/
public function captureException(
\Throwable|string $exception,
?string $distinctId = null,
array $additionalProperties = []
): bool {
$noDistinctIdProvided = $distinctId === null;
if ($noDistinctIdProvided) {
$distinctId = Uuid::v4();
}

$errorTrackingConfig = $this->options['error_tracking'] ?? [];
$maxFrames = max(0, (int) ($errorTrackingConfig['max_frames'] ?? 20));

$exceptionList = ExceptionPayloadBuilder::buildExceptionList($exception, $maxFrames);
if (empty($exceptionList)) {
return false;
}

$properties = array_merge(
$additionalProperties,
[
'$exception_list' => $exceptionList,
'$exception_handled' => ExceptionPayloadBuilder::getPrimaryHandled($exceptionList),
]
);

if ($noDistinctIdProvided) {
$properties['$process_person_profile'] = false;
}

return $this->capture([
'distinctId' => $distinctId,
'event' => '$exception',
'properties' => $properties,
]);
}

/**
* Tags properties about the user.
*
Expand Down
Loading
Loading