New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/support feature flags #12
Changes from all commits
692e8ba
706baa3
ceae2ac
8c3d022
dbc3357
2a35f87
25aefc7
3b4bf56
bfc6cd7
dcff4fa
09c443d
587a1c6
5ddb701
bf461fe
86ef9fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
namespace PostHog; | ||
|
||
use Exception; | ||
use PostHog\Consumer\File; | ||
use PostHog\Consumer\ForkCurl; | ||
use PostHog\Consumer\LibCurl; | ||
|
@@ -10,35 +11,61 @@ | |
class Client | ||
{ | ||
|
||
private const CONSUMERS = [ | ||
"socket" => Socket::class, | ||
"file" => File::class, | ||
"fork_curl" => ForkCurl::class, | ||
"lib_curl" => LibCurl::class, | ||
]; | ||
|
||
private const LONG_SCALE = 0xFFFFFFFFFFFFFFF; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $apiKey; | ||
|
||
/** | ||
* Consumer object handles queueing and bundling requests to PostHog. | ||
* | ||
* @var Consumer | ||
*/ | ||
protected $consumer; | ||
|
||
protected $featureFlags = null; | ||
|
||
/** | ||
* @var string|null | ||
*/ | ||
private $personalApiKey; | ||
|
||
/** | ||
* @var HttpClient | ||
*/ | ||
private $httpClient; | ||
|
||
|
||
/** | ||
* Create a new posthog object with your app's API key | ||
* key | ||
* | ||
* @param string $apiKey | ||
* @param array $options array of consumer options [optional] | ||
* @param string Consumer constructor to use, libcurl by default. | ||
* | ||
* @param HttpClient|null $httpClient | ||
*/ | ||
public function __construct($apiKey, $options = array()) | ||
public function __construct(string $apiKey, array $options = [], ?HttpClient $httpClient = null) | ||
{ | ||
$consumers = array( | ||
"socket" => Socket::class, | ||
"file" => File::class, | ||
"fork_curl" => ForkCurl::class, | ||
"lib_curl" => LibCurl::class, | ||
); | ||
|
||
// Use our socket libcurl by default | ||
$consumer_type = $options["consumer"] ?? "lib_curl"; | ||
$Consumer = $consumers[$consumer_type]; | ||
$this->apiKey = $apiKey; | ||
$Consumer = self::CONSUMERS[$options["consumer"] ?? "lib_curl"]; | ||
$this->consumer = new $Consumer($apiKey, $options); | ||
$this->personalApiKey = $options["personal_api_key"] ?? null; | ||
$this->httpClient = $httpClient !== null ? $httpClient : new HttpClient( | ||
$options['host'] ?? "app.posthog.com", | ||
$options['ssl'] ?? true, | ||
10000, | ||
false, | ||
$options["debug"] ?? false | ||
); | ||
} | ||
|
||
public function __destruct() | ||
|
@@ -79,6 +106,81 @@ public function identify(array $message) | |
return $this->consumer->identify($message); | ||
} | ||
|
||
/** | ||
* decide if the feature flag is enabled for this distinct id. | ||
* | ||
* @param string $key | ||
* @param string $distinctId | ||
* @param mixed $defaultValue | ||
* @return bool | ||
* @throws Exception | ||
*/ | ||
public function isFeatureEnabled(string $key, string $distinctId, $defaultValue = false): bool | ||
{ | ||
if (null === $this->personalApiKey) { | ||
throw new Exception( | ||
"To use feature flags, please set a personal_api_key. | ||
For More information: https://posthog.com/docs/api/overview" | ||
); | ||
} | ||
|
||
if (null === $this->featureFlags) { | ||
$this->loadFeatureFlags(); | ||
} | ||
|
||
if (null === $this->featureFlags) { // if loading failed. | ||
return $defaultValue; | ||
} | ||
|
||
$selectedFlag = null; | ||
foreach ($this->featureFlags as $flag) { | ||
if ($flag['key'] === $key) { | ||
$selectedFlag = $flag; | ||
} | ||
} | ||
if (null === $selectedFlag) { | ||
return $defaultValue; | ||
} | ||
|
||
if ((bool) $selectedFlag['is_simple_flag']) { | ||
$result = $this->isSimpleFlagEnabled($key, $distinctId, $flag['rollout_percentage']); | ||
} else { | ||
$result = in_array($key, $this->fetchEnabledFeatureFlags($distinctId)); | ||
} | ||
|
||
$this->capture([ | ||
"properties" => [ | ||
'$feature_flag' => $key, | ||
'$feature_flag_response' => $result, | ||
], | ||
"distinct_id" => $distinctId, | ||
"event" => '$feature_flag_called', | ||
]); | ||
|
||
return $result ?? $defaultValue; | ||
} | ||
|
||
|
||
/** | ||
* @param string $distinctId | ||
* @return array | ||
* @throws Exception | ||
*/ | ||
public function fetchEnabledFeatureFlags(string $distinctId): array | ||
{ | ||
return json_decode($this->decide($distinctId), true)['featureFlags'] ?? []; | ||
} | ||
|
||
public function decide(string $distinctId) | ||
{ | ||
$payload = json_encode([ | ||
'api_key' => $this->apiKey, | ||
'distinct_id' => $distinctId, | ||
]); | ||
|
||
return $this->httpClient->sendRequest('/decide/', $payload)->getResponse(); | ||
} | ||
|
||
/** | ||
* Aliases from one user id to another | ||
* | ||
|
@@ -203,4 +305,42 @@ private function message($msg) | |
|
||
return $msg; | ||
} | ||
|
||
private function loadFeatureFlags(): void | ||
{ | ||
$response = $this->httpClient->sendRequest( | ||
'/api/feature_flag', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've made a change since you last edited this, so this should now be No way you could have known though! |
||
null, | ||
[ | ||
"Authorization: Bearer $this->personalApiKey", | ||
] | ||
); | ||
if (401 === $response->getResponseCode()) { | ||
throw new Exception( | ||
"Your personalApiKey is invalid. Are you sure you're not using your Project API key? | ||
More information: https://posthog.com/docs/api/overview" | ||
); | ||
} | ||
|
||
$responseBody = json_decode($response->getResponse(), true); | ||
if (null === $responseBody) { | ||
return; | ||
} | ||
|
||
if (empty($responseBody)) { | ||
$this->featureFlags = []; | ||
} | ||
|
||
$this->featureFlags = $responseBody['results']; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another change we made is that this should filter by active flags. The |
||
} | ||
|
||
private function isSimpleFlagEnabled(string $key, string $distinctId, ?int $rolloutPercentage): bool | ||
{ | ||
if (! (bool) $rolloutPercentage) { | ||
return true; | ||
} | ||
$hexValueOfHash = sha1("$key.$distinctId", false); | ||
$integerRepresentationOfHashSubset = intval(substr($hexValueOfHash, 0, 15), 16); | ||
return ($integerRepresentationOfHashSubset / self::LONG_SCALE) <= ($rolloutPercentage / 100); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok so I tried this locally and it didn't work. I suspect this is a potential problem. We're sending a POST request here, when it should actually be a GET request.