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 .changeset/three-pants-ask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-php": minor
---

The new `getRemoteConfigPayload()` reads the payload of a remote config feature flag by its key, with no distinct ID. It needs the `secretKey` the client was created with. It returns `null` and logs a warning on a failure.
36 changes: 36 additions & 0 deletions api/public-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,24 @@
"returnType": "?string",
"parameters": []
},
"getRemoteConfigPayload": {
"static": false,
"abstract": false,
"final": false,
"returnType": "mixed",
"parameters": [
{
"name": "key",
"type": "string",
"byReference": false,
"variadic": false,
"optional": false,
"default": null,
"defaultConstant": null,
"hasDefault": false
}
]
},
"groupIdentify": {
"static": false,
"abstract": false,
Expand Down Expand Up @@ -4836,6 +4854,24 @@
}
]
},
"getRemoteConfigPayload": {
"static": true,
"abstract": false,
"final": false,
"returnType": "mixed",
"parameters": [
{
"name": "key",
"type": "string",
"byReference": false,
"variadic": false,
"optional": false,
"default": null,
"defaultConstant": null,
"hasDefault": false
}
]
},
"groupIdentify": {
"static": true,
"abstract": false,
Expand Down
81 changes: 81 additions & 0 deletions lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,87 @@ public function getFeatureFlagPayload(
return $result?->getPayload();
}

/**
* Fetch the remote config payload of a feature flag from PostHog.
*
* Needs a secret key. A Personal API Key returns encrypted payloads decrypted. A Project Secret
* API Key returns them redacted. The call sends one request, with no retry, and captures no
* `$feature_flag_called` event. It logs a warning and returns null when the client is disabled,
* when no secret key is set, when the key is empty, `.` or `..`, when the request fails, or when
* PostHog answers with a non-2xx status. An empty response means the flag has no payload: the
* call returns null without a warning.
*
* @param string $key Feature flag key.
* @return mixed The decoded payload, or null when no payload could be read.
*/
public function getRemoteConfigPayload(string $key): mixed
{
if (!$this->enabled) {
$this->logWarning('The client is disabled');
return null;
}

if ($this->secretKey === null) {
$this->logWarning('A secret key is required to fetch a remote config payload from PostHog');
return null;
}

if (in_array($key, ['', '.', '..'], true)) {
$this->logWarning('A feature flag key is required to fetch a remote config payload from PostHog');
return null;
}

$response = $this->httpClient->sendRequest(
'/api/projects/@current/feature_flags/' . rawurlencode($key)
. '/remote_config?token=' . rawurlencode($this->apiKey),
null,
[
"User-Agent: " . $this->options['library_name'] . "/" . $this->options['library_version'],
"Authorization: Bearer " . $this->secretKey
],
[
'shouldRetry' => false,
'timeout' => $this->featureFlagsRequestTimeout
]
);

$code = $response->getResponseCode();
// cURL keeps the 2xx status when the transfer fails after the headers arrive.
$curlErrno = $response->getCurlErrno();
if ($curlErrno !== 0 || $code < 200 || $code >= 300) {
error_log(
'[PostHog][Client] Unable to fetch remote config payload: '
. ($curlErrno !== 0 || $code === 0
? 'connection error (curl errno ' . $curlErrno . ')'
: 'HTTP ' . $code)
);
return null;
}

// PostHog answers a flag that has no payload with an empty 2xx body.
$body = (string) $response->getResponse();
if (trim($body) === '') {
return null;
Comment thread
ioannisj marked this conversation as resolved.
Resolved
}

try {
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
Comment thread
ioannisj marked this conversation as resolved.
Resolved
} catch (\JsonException) {
error_log('[PostHog][Client] Unable to read remote config payload: invalid JSON response');
return null;
}

if (is_string($result)) {
try {
return json_decode($result, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return $result;
}
}

return $result;
}

/**
* get the feature flag value for this distinct id.
*
Expand Down
14 changes: 14 additions & 0 deletions lib/PostHog.php
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,20 @@ public static function getFeatureFlagPayload(
);
}

/**
* Fetch the remote config payload of a feature flag from PostHog.
* See Client::getRemoteConfigPayload()
*
* @param string $key Feature flag key.
* @return mixed The decoded payload, or null when no payload could be read.
*/
public static function getRemoteConfigPayload(string $key): mixed
{
self::checkClient();

return self::$client->getRemoteConfigPayload($key);
}

/**
* Evaluate every feature flag for a distinct id in a single round trip and return a snapshot.
* When distinctId is omitted, the current request context distinctId is used if available.
Expand Down
20 changes: 19 additions & 1 deletion test/MockedHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class MockedHttpClient extends \PostHog\HttpClient
private $batchEndpointResponse;
private $batchEndpointResponseCode;
private $batchEndpointCurlErrno;
private $remoteConfigEndpointResponse;
private $remoteConfigEndpointResponseCode;
private $remoteConfigEndpointCurlErrno;

public function __construct(
string $host,
Expand All @@ -47,7 +50,10 @@ public function __construct(
int $flagsEndpointCurlErrno = 0,
$batchEndpointResponse = '{"status":1}',
int $batchEndpointResponseCode = 200,
int $batchEndpointCurlErrno = 0
int $batchEndpointCurlErrno = 0,
$remoteConfigEndpointResponse = '{}',
int $remoteConfigEndpointResponseCode = 200,
int $remoteConfigEndpointCurlErrno = 0
) {
parent::__construct(
$host,
Expand All @@ -71,6 +77,9 @@ public function __construct(
$this->batchEndpointResponse = $batchEndpointResponse;
$this->batchEndpointResponseCode = $batchEndpointResponseCode;
$this->batchEndpointCurlErrno = $batchEndpointCurlErrno;
$this->remoteConfigEndpointResponse = $remoteConfigEndpointResponse;
$this->remoteConfigEndpointResponseCode = $remoteConfigEndpointResponseCode;
$this->remoteConfigEndpointCurlErrno = $remoteConfigEndpointCurlErrno;
}

/**
Expand Down Expand Up @@ -165,6 +174,15 @@ public function sendRequest(string $path, ?string $payload, array $extraHeaders
);
}

if (str_starts_with($path, "/api/projects/@current/feature_flags/")) {
return new HttpResponse(
$this->remoteConfigEndpointResponse,
$this->remoteConfigEndpointResponseCode,
null,
$this->remoteConfigEndpointCurlErrno
);
}

throw new \LogicException('Unexpected HTTP request in test: ' . $path);
}
}
Loading
Loading