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
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
vendor
.idea
cache/*
!cache/.gitkeep
analytics/*
!analytics/.gitkeep
backup/*
!backup/.gitkeep
cache/*
!cache/.gitkeep
.phpunit*
Empty file added analytics/.gitkeep
Empty file.
Binary file not shown.
7 changes: 7 additions & 0 deletions src/Endpoints/FeatureFlags.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,16 @@ public function isActive(string $featureFlagName): bool

// If you ask for an non-existing feature flag, it returns false
if (! array_key_exists($featureFlagName, $featureFlags)) {
// TODO: Should we send analytics here?
return false;
}

if ($this->featurit->isAnalyticsModuleEnabled()) {
$this->featurit->getFeatureAnalyticsService()->registerFeatureFlagRequest(
$featureFlags[$featureFlagName]
);
}

return $featureFlags[$featureFlagName]->isActive();
}

Expand Down
80 changes: 60 additions & 20 deletions src/Featurit.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Featurit\Client\Endpoints\FeatureFlags;
use Featurit\Client\HttpClient\ClientBuilder;
use Featurit\Client\Modules\Analytics\Services\AnalyticsSender;
use Featurit\Client\Modules\Analytics\Services\FeatureAnalyticsService;
use Featurit\Client\Modules\Segmentation\DefaultFeaturitUserContext;
use Featurit\Client\Modules\Segmentation\DefaultFeaturitUserContextProvider;
use Featurit\Client\Modules\Segmentation\FeaturitUserContext;
Expand All @@ -21,12 +23,16 @@ class Featurit
private string $tenantIdentifier;
private string $apiKey;

private bool $isAnalyticsEnabled;

private FeaturitUserContextProvider $featuritUserContextProvider;
private ClientBuilder $clientBuilder;
private CacheInterface $cache;
private CacheInterface $analyticsCache;
private CacheInterface $backupCache;
private FeatureSegmentationService $featureSegmentationService;
private LocalCacheFactory $localCacheFactory;
private FeatureAnalyticsService $featureAnalyticsService;

public function __construct(
string $tenantIdentifier,
Expand All @@ -37,6 +43,8 @@ public function __construct(
ClientBuilder $clientBuilder = null,
UriFactory $uriFactory = null,
FeaturitUserContext $featuritUserContext = null,
bool $enableAnalytics = false,
int $sendAnalyticsIntervalMinutes = FeaturitBuilder::DEFAULT_SEND_ANALYTICS_INTERVAL_MINUTES,
) {
$this->tenantIdentifier = $tenantIdentifier;
$this->apiKey = $apiKey;
Expand All @@ -50,18 +58,20 @@ public function __construct(
$this->setHttpClientBuilder($clientBuilder, $uriFactory);

$this->featureSegmentationService = new FeatureSegmentationService();

$this->setupAnalytics($enableAnalytics, $sendAnalyticsIntervalMinutes);
}

/**
* @throws HttpClient\Exceptions\InvalidApiKeyException
* @throws \Featurit\Client\HttpClient\Exceptions\InvalidApiKeyException
*/
public function isActive(string $featureName): bool
{
return $this->featureFlags()->isActive($featureName);
}

/**
* @throws HttpClient\Exceptions\InvalidApiKeyException
* @throws \Featurit\Client\HttpClient\Exceptions\InvalidApiKeyException
*/
public function version(string $featureName): string
{
Expand All @@ -88,6 +98,11 @@ public function getCache(): CacheInterface
return $this->cache;
}

public function getAnalyticsCache(): CacheInterface
{
return $this->analyticsCache;
}

public function getBackupCache(): CacheInterface
{
return $this->backupCache;
Expand All @@ -103,11 +118,43 @@ public function getFeatureSegmentationService(): FeatureSegmentationService
return $this->featureSegmentationService;
}

public function getFeatureAnalyticsService(): FeatureAnalyticsService
{
return $this->featureAnalyticsService;
}

public function isAnalyticsModuleEnabled(): bool
{
return $this->isAnalyticsEnabled;
}

public function setUserContext(FeaturitUserContext $featuritUserContext): void
{
$this->setFeaturitUserContextProvider($featuritUserContext);
}

/**
* @param FeaturitUserContext|null $featuritUserContext
* @param FeaturitUserContextProvider|null $featuritUserContextProvider
* @return void
*/
public function setFeaturitUserContextProvider(?FeaturitUserContext $featuritUserContext = null, ?FeaturitUserContextProvider $featuritUserContextProvider = null): void
{
if (! is_null($featuritUserContext)) {
$this->featuritUserContextProvider = new DefaultFeaturitUserContextProvider($featuritUserContext);

return;
}

if (is_null($featuritUserContextProvider)) {
$featuritUserContextProvider = new DefaultFeaturitUserContextProvider(
new DefaultFeaturitUserContext(null, null, null)
);
}

$this->featuritUserContextProvider = $featuritUserContextProvider;
}

/**
* @param CacheInterface|null $cache
* @param int $cacheTtlMinutes
Expand All @@ -119,9 +166,8 @@ private function setCache(?CacheInterface $cache, int $cacheTtlMinutes): void
$cache = $this->localCacheFactory->setLocalCache($cacheTtlMinutes, 'cache' , true);
}

/**
* Backup cache will be used when there's some problem with the FeaturIT API.
*/
$this->analyticsCache = $this->localCacheFactory->setLocalCache(0, 'analytics', false);

$this->backupCache = $this->localCacheFactory->setLocalCache(0, 'backup', false);

$this->cache = $cache;
Expand Down Expand Up @@ -155,24 +201,18 @@ private function setHttpClientBuilder(?ClientBuilder $clientBuilder, ?UriFactory
}

/**
* @param FeaturitUserContext|null $featuritUserContext
* @param FeaturitUserContextProvider|null $featuritUserContextProvider
* @param bool $enableAnalytics
* @param int $sendAnalyticsIntervalMinutes
* @return void
*/
public function setFeaturitUserContextProvider(?FeaturitUserContext $featuritUserContext = null, ?FeaturitUserContextProvider $featuritUserContextProvider = null): void
private function setupAnalytics(bool $enableAnalytics, int $sendAnalyticsIntervalMinutes): void
{
if (! is_null($featuritUserContext)) {
$this->featuritUserContextProvider = new DefaultFeaturitUserContextProvider($featuritUserContext);

return;
}

if (is_null($featuritUserContextProvider)) {
$featuritUserContextProvider = new DefaultFeaturitUserContextProvider(
new DefaultFeaturitUserContext(null, null, null)
);
}
$this->isAnalyticsEnabled = $enableAnalytics;

$this->featuritUserContextProvider = $featuritUserContextProvider;
$this->featureAnalyticsService = new FeatureAnalyticsService(
$this->getAnalyticsCache(),
new AnalyticsSender($this->getHttpClient()),
$sendAnalyticsIntervalMinutes
);
}
}
19 changes: 19 additions & 0 deletions src/FeaturitBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
class FeaturitBuilder
{
public const DEFAULT_CACHE_TTL_MINUTES = 5;
public const DEFAULT_SEND_ANALYTICS_INTERVAL_MINUTES = 1;

private string $tenantIdentifier;
private string $apiKey;
private bool $isAnalyticsModuleEnabled = false;
private int $cacheTtlMinutes = self::DEFAULT_CACHE_TTL_MINUTES;
private int $sendAnalyticsIntervalMinutes = self::DEFAULT_SEND_ANALYTICS_INTERVAL_MINUTES;
private FeaturitUserContextProvider $featuritUserContextProvider;
private CacheInterface $cache;
private ClientBuilder $httpClientBuilder;
Expand All @@ -35,13 +38,27 @@ public function setApiKey(string $apiKey): FeaturitBuilder
return $this;
}

public function setIsAnalyticsModuleEnabled(bool $isAnalyticsModuleEnabled): FeaturitBuilder
{
$this->isAnalyticsModuleEnabled = $isAnalyticsModuleEnabled;

return $this;
}

public function setCacheTtlMinutes(int $cacheTtlMinutes): FeaturitBuilder
{
$this->cacheTtlMinutes = $cacheTtlMinutes;

return $this;
}

public function setSendAnalyticsIntervalMinutes(int $sendAnalyticsIntervalMinutes): FeaturitBuilder
{
$this->sendAnalyticsIntervalMinutes = $sendAnalyticsIntervalMinutes;

return $this;
}

public function setFeaturitUserContextProvider(FeaturitUserContextProvider $featuritUserContextProvider): FeaturitBuilder
{
$this->featuritUserContextProvider = $featuritUserContextProvider;
Expand Down Expand Up @@ -99,6 +116,8 @@ public function build(): Featurit
$this->httpClientBuilder ?? null,
$this->uriFactory ?? null,
$this->featuritUserContext ?? null,
$this->isAnalyticsModuleEnabled,
$this->sendAnalyticsIntervalMinutes,
);
}
}
148 changes: 148 additions & 0 deletions src/Modules/Analytics/AnalyticsBucket.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

namespace Featurit\Client\Modules\Analytics;

use DateTime;
use DateTimeInterface;
use Exception;
use Featurit\Client\Modules\Segmentation\Entities\FeatureFlag;
use JsonSerializable;

class AnalyticsBucket implements JsonSerializable
{
private DateTimeInterface $startDateTime;
private ?DateTimeInterface $endDateTime = null;

/**
* [
* "$hour" => [
* "$featureName" => [
* "$featureVersion" => [
* "$isActive" => $count,
* ],
* ],
* ],
* ]
* @var array
*/
private array $requests = [];

public function __construct(
DateTimeInterface $startDateTime,
?DateTimeInterface $endDateTime = null
)
{
$this->startDateTime = clone $startDateTime;

if (! is_null($endDateTime)) {
$this->endDateTime = clone $endDateTime;
}
}

public function startDateTime(): DateTimeInterface
{
return $this->startDateTime;
}

public function addFeatureFlagRequest(
FeatureFlag $featureFlag,
DateTime $currentTime = null
): void
{
if ($this->isClosed()) {
return;
}

if (is_null($currentTime)) {
$currentTime = new DateTime();
}

// Save the Feature Flag Request.
$hourKey = $this->generateHourKey($currentTime);
$flagNameKey = $this->generateFeatureFlagNameKey($featureFlag);
$flagVersionKey = $this->generateFeatureFlagVersionKey($featureFlag);
$flagIsActiveKey = $this->generateFeatureFlagIsActiveKey($featureFlag);

if (!isset($this->requests["$hourKey"])) {
$this->requests["$hourKey"] = [];
}

if (!isset($this->requests["$hourKey"]["$flagNameKey"])) {
$this->requests["$hourKey"]["$flagNameKey"] = [];
}

if (!isset($this->requests["$hourKey"]["$flagNameKey"]["$flagVersionKey"])) {
$this->requests["$hourKey"]["$flagNameKey"]["$flagVersionKey"] = [];
}

if (!isset($this->requests["$hourKey"]["$flagNameKey"]["$flagVersionKey"]["$flagIsActiveKey"])) {
$this->requests["$hourKey"]["$flagNameKey"]["$flagVersionKey"]["$flagIsActiveKey"] = 1;
} else {
$this->requests["$hourKey"]["$flagNameKey"]["$flagVersionKey"]["$flagIsActiveKey"]++;
}
}

public function openBucket(): void
{
if (!$this->isClosed()) {
return;
}

$this->endDateTime = null;
}

public function closeBucket(?DateTimeInterface $endDateTime = null): void
{
if ($this->isClosed()) {
return;
}

if (! is_null($endDateTime)) {
$this->endDateTime = $endDateTime;
return;
}

$this->endDateTime = new DateTime();
}

private function isClosed(): bool
{
return ! is_null($this->endDateTime);
}

/**
* @throws Exception
*/
public function jsonSerialize(): array
{
if (! $this->isClosed()) {
throw new Exception("Can't serialize an open bucket.");
}

return [
"start" => $this->startDateTime,
"end" => $this->endDateTime,
"reqs" => $this->requests,
];
}

public function generateHourKey(DateTime $currentTime): string
{
return $currentTime->format("Y-m-d H:00:00");
}

public function generateFeatureFlagNameKey(FeatureFlag $featureFlag): string
{
return $featureFlag->name();
}

public function generateFeatureFlagVersionKey(FeatureFlag $featureFlag): string
{
return $featureFlag->selectedFeatureFlagVersion()->name();
}

public function generateFeatureFlagIsActiveKey(FeatureFlag $featureFlag): string
{
return $featureFlag->isActive() ? "t" : "f";
}
}
Loading