From d54bb449dda0cea9b7e223af81e87b74f12b663c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Batlle?= Date: Wed, 15 Mar 2023 18:03:50 +0100 Subject: [PATCH 1/5] WIP --- .../laminascache-analytics_bucket.dat | Bin 0 -> 4218 bytes src/Endpoints/FeatureFlags.php | 6 + src/Featurit.php | 30 ++- src/FeaturitBuilder.php | 10 + src/Modules/Analytics/AnalyticsBucket.php | 128 ++++++++++ .../CantSendAnalyticsToServerException.php | 10 + .../Analytics/Services/AnalyticsSender.php | 38 +++ .../Services/FeatureAnalyticsService.php | 66 ++++++ .../Services/FeatureSegmentationService.php | 8 - .../laminascache-analytics_bucket.dat | Bin 0 -> 14899 bytes tests/FeaturitTest.php | 37 +++ .../Modules/Analytics/AnalyticsBucketTest.php | 220 ++++++++++++++++++ .../Services/AnalyticsSenderTest.php | 18 ++ .../Services/FeatureAnalyticsServiceTest.php | 149 ++++++++++++ .../DefaultFeaturitUserContextTest.php | 25 +- 15 files changed, 720 insertions(+), 25 deletions(-) create mode 100644 analytics/laminascache-c5/laminascache-analytics_bucket.dat create mode 100644 src/Modules/Analytics/AnalyticsBucket.php create mode 100644 src/Modules/Analytics/Exceptions/CantSendAnalyticsToServerException.php create mode 100644 src/Modules/Analytics/Services/AnalyticsSender.php create mode 100644 src/Modules/Analytics/Services/FeatureAnalyticsService.php create mode 100644 test_analytics/laminascache-c5/laminascache-analytics_bucket.dat create mode 100644 tests/Modules/Analytics/AnalyticsBucketTest.php create mode 100644 tests/Modules/Analytics/Services/AnalyticsSenderTest.php create mode 100644 tests/Modules/Analytics/Services/FeatureAnalyticsServiceTest.php diff --git a/analytics/laminascache-c5/laminascache-analytics_bucket.dat b/analytics/laminascache-c5/laminascache-analytics_bucket.dat new file mode 100644 index 0000000000000000000000000000000000000000..8965160ff91701ba36d4d91721e9d8db1975b4c0 GIT binary patch literal 4218 zcmdUyZEM>w5XbvbirzQ)^k`pRr5%Mrm%`TeRbY6rTErYDi{wIA`rUVO;%!^OGDpknt4EV1UH#CJ z+RM6p{A9nk)`xc;0gW#XGjgfp1VFamIWJe1kxyzDH6F)62>ZwPL`I%RD@XPOagZZ( zezpwmiFSUaQ#+#IR&;y0+OX+OT=Jm|_Lgi6#Xc$(S-OKL$~@!ZEYoKoPI-oqQ|KpJ zK(6!70f8)@_ha4IFX4GZ+Z>AB)xABgqh)uu7mNbv5C-6Yg8SL6Hnok(?QVD%(IBhr zL)q5dnm;(<|8a7oMJgvB5vP#4dt)FWpJGg$S|#UG4sae+lw2af#K=6$cVC5sB&q?p zV-aP~SroZU6{Q{zr-DKzi_(mUlhf}cHJ|wa=V3)zpoEFg8dS0QZFXcuWjPP&)Oa17j$8dQioAa>)ZjHP5jK zvj^3gNiGXu4E4_&s8B`Ps~h`QO$}8->r5TvHoUcp|Ij%ap^}^@&#AFZ@Z2`lFTbgW F{|R7c4<-Nr literal 0 HcmV?d00001 diff --git a/src/Endpoints/FeatureFlags.php b/src/Endpoints/FeatureFlags.php index 041f5a3..10d81a0 100644 --- a/src/Endpoints/FeatureFlags.php +++ b/src/Endpoints/FeatureFlags.php @@ -97,9 +97,15 @@ 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; } + $this->featurit->getFeatureAnalyticsService()->registerFeatureFlagRequest( + $featureFlags[$featureFlagName], + $this->featurit->getUserContext() + ); + return $featureFlags[$featureFlagName]->isActive(); } diff --git a/src/Featurit.php b/src/Featurit.php index 4e10c4d..374bc3f 100644 --- a/src/Featurit.php +++ b/src/Featurit.php @@ -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; @@ -24,9 +26,11 @@ class Featurit 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, @@ -37,6 +41,7 @@ public function __construct( ClientBuilder $clientBuilder = null, UriFactory $uriFactory = null, FeaturitUserContext $featuritUserContext = null, + int $sendAnalyticsIntervalMinutes = FeaturitBuilder::DEFAULT_SEND_ANALYTICS_INTERVAL_MINUTES, ) { $this->tenantIdentifier = $tenantIdentifier; $this->apiKey = $apiKey; @@ -50,10 +55,16 @@ public function __construct( $this->setHttpClientBuilder($clientBuilder, $uriFactory); $this->featureSegmentationService = new FeatureSegmentationService(); + + $this->featureAnalyticsService = new FeatureAnalyticsService( + $this->getAnalyticsCache(), + new AnalyticsSender($this->getHttpClient()), + $sendAnalyticsIntervalMinutes + ); } /** - * @throws HttpClient\Exceptions\InvalidApiKeyException + * @throws Featurit\Client\Modules\Analytics\Exceptions\InvalidApiKeyException */ public function isActive(string $featureName): bool { @@ -61,7 +72,7 @@ public function isActive(string $featureName): bool } /** - * @throws HttpClient\Exceptions\InvalidApiKeyException + * @throws Featurit\Client\Modules\Analytics\Exceptions\InvalidApiKeyException */ public function version(string $featureName): string { @@ -88,6 +99,11 @@ public function getCache(): CacheInterface return $this->cache; } + public function getAnalyticsCache(): CacheInterface + { + return $this->analyticsCache; + } + public function getBackupCache(): CacheInterface { return $this->backupCache; @@ -108,6 +124,11 @@ public function setUserContext(FeaturitUserContext $featuritUserContext): void $this->setFeaturitUserContextProvider($featuritUserContext); } + public function getFeatureAnalyticsService(): FeatureAnalyticsService + { + return $this->featureAnalyticsService; + } + /** * @param CacheInterface|null $cache * @param int $cacheTtlMinutes @@ -119,9 +140,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; diff --git a/src/FeaturitBuilder.php b/src/FeaturitBuilder.php index bc61718..870eb97 100644 --- a/src/FeaturitBuilder.php +++ b/src/FeaturitBuilder.php @@ -11,10 +11,12 @@ class FeaturitBuilder { public const DEFAULT_CACHE_TTL_MINUTES = 5; + public const DEFAULT_SEND_ANALYTICS_INTERVAL_MINUTES = 1; private string $tenantIdentifier; private string $apiKey; 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; @@ -42,6 +44,13 @@ public function setCacheTtlMinutes(int $cacheTtlMinutes): FeaturitBuilder return $this; } + public function setSendAnalyticsIntervalMinutes(int $sendAnalyticsIntervalMinutes): FeaturitBuilder + { + $this->sendAnalyticsIntervalMinutes = $sendAnalyticsIntervalMinutes; + + return $this; + } + public function setFeaturitUserContextProvider(FeaturitUserContextProvider $featuritUserContextProvider): FeaturitBuilder { $this->featuritUserContextProvider = $featuritUserContextProvider; @@ -99,6 +108,7 @@ public function build(): Featurit $this->httpClientBuilder ?? null, $this->uriFactory ?? null, $this->featuritUserContext ?? null, + $this->sendAnalyticsIntervalMinutes, ); } } \ No newline at end of file diff --git a/src/Modules/Analytics/AnalyticsBucket.php b/src/Modules/Analytics/AnalyticsBucket.php new file mode 100644 index 0000000..7eab92b --- /dev/null +++ b/src/Modules/Analytics/AnalyticsBucket.php @@ -0,0 +1,128 @@ + ["userId", "sessionId", "ipAddress", "custom"], + * "flag" => ["featureName", "featureVersion", "isActive"] + * ] + * ] + * @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, + FeaturitUserContext $featuritUserContext, + ?DateTimeInterface $insertionDateTime = null + ): void + { + if ($this->isClosed()) { + return; + } + + // Save the User Context. + $request["ctx"] = [ + BaseAttributes::USER_ID => $featuritUserContext->getUserId(), + BaseAttributes::SESSION_ID => $featuritUserContext->getSessionId(), + BaseAttributes::IP_ADDRESS => $featuritUserContext->getIpAddress(), + ]; + + if (count($featuritUserContext->getCustomAttributes()) > 0) { + $request["ctx"]["custom"] = $featuritUserContext->getCustomAttributes(); + } + + // Save the Feature Flag. + $request["flag"] = [ + "featureName" => $featureFlag->name(), + "featureVersion" => $featureFlag->selectedFeatureFlagVersion()->name(), + "isActive" => $featureFlag->isActive(), + ]; + + // Save the request timestamp. + if (is_null($insertionDateTime)) { + $insertionDateTime = new DateTime(); + } + + $request["timestamp"] = $insertionDateTime; + + $this->requests[] = $request; + } + + 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, + ]; + } +} \ No newline at end of file diff --git a/src/Modules/Analytics/Exceptions/CantSendAnalyticsToServerException.php b/src/Modules/Analytics/Exceptions/CantSendAnalyticsToServerException.php new file mode 100644 index 0000000..1d98f39 --- /dev/null +++ b/src/Modules/Analytics/Exceptions/CantSendAnalyticsToServerException.php @@ -0,0 +1,10 @@ +httpMethodsClient->post( + '/analytics', + [], + json_encode($analyticsBucket->jsonSerialize()) + ); + + if ($featureFlagsApiResponse->getStatusCode() != 200) { + throw new CantSendAnalyticsToServerException("Error sending Analytics to the API"); + } + + dump("Request sent to the API"); + } catch (\Http\Client\Exception $exception) { + throw new CantSendAnalyticsToServerException($exception->getMessage(), $exception->getCode(), $exception); + } + } +} \ No newline at end of file diff --git a/src/Modules/Analytics/Services/FeatureAnalyticsService.php b/src/Modules/Analytics/Services/FeatureAnalyticsService.php new file mode 100644 index 0000000..fa3df69 --- /dev/null +++ b/src/Modules/Analytics/Services/FeatureAnalyticsService.php @@ -0,0 +1,66 @@ +analyticsCache->has($analyticsCacheKey)) { + dump("Getting Analytics from cache"); + $analyticsBucket = $this->analyticsCache->get($analyticsCacheKey); + dump("Bucket Start DateTime: " . $analyticsBucket->startDateTime()->format('c')); + dump("Now: " . $now->format('c')); + } else { + dump("Creating a new bucket"); + $analyticsBucket = new AnalyticsBucket($now); + } + + $analyticsBucket->addFeatureFlagRequest($featureFlag, $featuritUserContext, $now); + dump("Time diff: " . $analyticsBucket->startDateTime()->diff($now)->i); + // TODO: This approach can have problems due to sending big payloads to the server in case of failure or huge traffic. + if ($analyticsBucket->startDateTime()->diff($now)->i >= $this->sendAnalyticsIntervalMinutes) { + try { + $analyticsBucket->closeBucket($now); + $this->analyticsSender->sendAnalyticsBucket($analyticsBucket); + + $this->analyticsCache->delete($analyticsCacheKey); + dump("Analytics removed from cache"); + } catch (CantSendAnalyticsToServerException $exception) { + dump("Error sending analytics to the API"); + $analyticsBucket->openBucket(); + $this->analyticsCache->set($analyticsCacheKey, $analyticsBucket); + } + } else { + $this->analyticsCache->set($analyticsCacheKey, $analyticsBucket); + } + } +} \ No newline at end of file diff --git a/src/Modules/Segmentation/Services/FeatureSegmentationService.php b/src/Modules/Segmentation/Services/FeatureSegmentationService.php index 327b35b..161e9c9 100644 --- a/src/Modules/Segmentation/Services/FeatureSegmentationService.php +++ b/src/Modules/Segmentation/Services/FeatureSegmentationService.php @@ -109,8 +109,6 @@ private function evaluateFeatureFlagSegments( } } -// Log::debug("- No segment evaluated to true, so it's false"); - return false; } @@ -138,16 +136,12 @@ private function evaluateFeatureFlagSegment( foreach ($featureFlagSegment->stringSegmentRules() as $stringSegmentRule) { if (! $this->evaluateSegmentRule(AttributeTypes::STRING, $stringSegmentRule, $featuritUserContext)) { -// Log::debug("- Rule {$stringSegmentRule->attribute()->name()} {$stringSegmentRule->operator()} {$stringSegmentRule->value()} is false"); - return false; } } foreach ($featureFlagSegment->numberSegmentRules() as $numberSegmentRule) { if (! $this->evaluateSegmentRule(AttributeTypes::NUMBER, $numberSegmentRule, $featuritUserContext)) { -// Log::debug("- Rule {$numberSegmentRule->attribute()->name()} {$numberSegmentRule->operator()} {$numberSegmentRule->value()} is false"); - return false; } } @@ -174,8 +168,6 @@ private function evaluateSegmentRule( $segmentRuleAttributeValue = $segmentRule->value(); $featuritUserContextAttributeValue = $featuritUserContext->getAttribute($attributeName); -// Log::debug("- Attribute name {$attributeName}, operator {$operator}, ruleValue {$segmentRuleAttributeValue}, userValue {$featuritUserContextAttributeValue}"); - return $this->attributeEvaluators[$attributeType]->evaluate( $featuritUserContextAttributeValue, $operator, diff --git a/test_analytics/laminascache-c5/laminascache-analytics_bucket.dat b/test_analytics/laminascache-c5/laminascache-analytics_bucket.dat new file mode 100644 index 0000000000000000000000000000000000000000..75de6e1190386d3824870f1a9cac1f9a48092b87 GIT binary patch literal 14899 zcmeI2O>f&U3_$x+2HzW)lIJFLBQI~5qbL=BAPYq3(SEArn*Ic+hZK5!+? z#aMevZCRFH7>ZB!rOU=H`QXd2+g9QFd{OyTxc;=B?H0biK3kQGyHHL4`1Ec!{q95J zth=vWk-6mLuP;8SL%9v_%iyo7rB5c8Zsd}ue-Axl$1`k@OloHemzdPp)3i8+^i7(& z6kKWsY4aCbfOSd0u70go{%g3~;ANGwcmn%lhWCdyRTdtKSicE$Z7Z)Ph<^Zm+Wf4{WxnT3NC@S^JQ5r2GjL2 z9>V+AzOJkF>d*wifz5{xr>f1_Y_`SoPgjr40E*E726;S$Xs5e6#I8yQ54SJp2Y|!L z9!X(`H;Z!Kj5Ia?9K%f$FaDy8UtiNC20?R=*!*zzi{Hj!#iI!O7UBo|LaO>~8me2o z@EcAVZ-6t1UlInza`P{OVP83*!q3&YrUgC-!MB`WBEmOE2j6rb`BLyRCVWeU zZ?z&nvz=St;$uIB<-boFYUQEygVuIK{Ubk#CXE}Qq;R$Zys z=bA2%RacFECR_8g<$WRghO4@8gszKh1nd0{W8ZLH*N|OT&9)RCY6;amhU>b9?7C{t zb2Hf;&Mq?#*L4lqb=BzG-mJQY>$-;Qx@wsxD|(anxvp!-uB*1sV@+@9b6wYvU02Pv zjBM<;<$bADm+QJX)nj{wO<|u$s@~_iE|zyh`f8cSbidOe75iM*#igLb6ppUYa)HM%p<(h(6-Aa z75iM*wPe@Tqu+hKFH~__uIpN|>#ETgew(Vx*yp+~PL-H>)ardJ8~a_-=ejOVl}KN$ g=D{Mk>!&MHd7taLmh8HEnWy_V38m=sTz9|!4FPC8U;qFB literal 0 HcmV?d00001 diff --git a/tests/FeaturitTest.php b/tests/FeaturitTest.php index b4e1b33..9ff1ad1 100644 --- a/tests/FeaturitTest.php +++ b/tests/FeaturitTest.php @@ -7,6 +7,7 @@ use Featurit\Client\FeaturitBuilder; use Featurit\Client\HttpClient\ClientBuilder; use Featurit\Client\HttpClient\Exceptions\InvalidApiKeyException; +use Featurit\Client\Modules\Analytics\AnalyticsBucket; use Featurit\Client\Modules\Segmentation\ConstantCollections\BaseVersions; use Featurit\Client\Modules\Segmentation\DefaultFeaturitUserContext; use Featurit\Client\Modules\Segmentation\DefaultFeaturitUserContextProvider; @@ -284,6 +285,41 @@ public function test_passing_user_context_in_setter_overrides_user_context_provi $this->assertEquals("1357", $featurit->getUserContext()->getUserId()); } + public function test_that_cache_stores_dates_properly(): void + { + $featurit = $this->getFeaturit(self::VALID_API_KEY); + + $date = new \DateTime("2020-02-11"); + + $featurit->getCache()->set("test_datetime_serdes", $date); + + $cachedDate = $featurit->getCache()->get("test_datetime_serdes"); + + $this->assertEquals($date, $cachedDate); + + $date = new \DateTime('now'); + + $featurit->getCache()->set("test_datetime_serdes", $date); + + $cachedDate = $featurit->getCache()->get("test_datetime_serdes"); + + $this->assertEquals($date, $cachedDate); + } + + public function test_that_cache_stores_analytics_bucket_properly(): void + { + $featurit = $this->getFeaturit(self::VALID_API_KEY); + + $date = new \DateTime("2020-02-11"); + $analyticsBucket = new AnalyticsBucket($date); + + $featurit->getCache()->set("test_analytics_bucket_serdes", $analyticsBucket); + + $cachedAnalyticsBucket = $featurit->getCache()->get("test_analytics_bucket_serdes"); + + $this->assertEquals($analyticsBucket, $cachedAnalyticsBucket); + } + /** * @param string $apiKey * @param int $status @@ -313,6 +349,7 @@ private function getFeaturit( ->setTenantIdentifier(self::TENANT_IDENTIFIER) ->setApiKey($apiKey) ->setCacheTtlMinutes(5) + ->setSendAnalyticsIntervalMinutes(1) ->setHttpClientBuilder($clientBuilder); if (! is_null($featuritUserContextProvider)) { diff --git a/tests/Modules/Analytics/AnalyticsBucketTest.php b/tests/Modules/Analytics/AnalyticsBucketTest.php new file mode 100644 index 0000000..99970c1 --- /dev/null +++ b/tests/Modules/Analytics/AnalyticsBucketTest.php @@ -0,0 +1,220 @@ +addFeatureFlagRequest($featureFlag, $featuritUserContext, $insertionTime1); + + $endDateTime = new DateTime(); + $bucket->closeBucket($endDateTime); + + $insertionTime2 = new DateTime(); + $bucket->addFeatureFlagRequest($featureFlag, $featuritUserContext, $insertionTime2); + + $result = $bucket->jsonSerialize(); + + $expectedResult = [ + "start" => $startDateTime, + "end" => $endDateTime, + "reqs" => [ + [ + "ctx" => [ + BaseAttributes::USER_ID => $featuritUserContext->getUserId(), + BaseAttributes::SESSION_ID => $featuritUserContext->getSessionId(), + BaseAttributes::IP_ADDRESS => $featuritUserContext->getIpAddress(), + ], + "flag" => [ + "featureName" => $featureFlag->name(), + "featureVersion" => $featureFlag->selectedFeatureFlagVersion()->name(), + "isActive" => $featureFlag->isActive(), + ], + "timestamp" => $insertionTime1, + ], + ], + ]; + + $this->assertEquals($expectedResult, $result); + } + + public function test_it_cant_be_serialized_if_not_closed(): void + { + $startDateTime = new DateTime(); + $bucket = new AnalyticsBucket($startDateTime); + + $this->expectException(Exception::class); + + $bucket->jsonSerialize(); + } + + public function test_it_serializes_properly_when_empty(): void + { + $startDateTime = new DateTime(); + $bucket = new AnalyticsBucket($startDateTime); + + $endDateTime = new DateTime(); + $bucket->closeBucket($endDateTime); + + $result = $bucket->jsonSerialize(); + + $expectedResult = [ + "start" => $startDateTime, + "end" => $endDateTime, + "reqs" => [], + ]; + + $this->assertEquals($expectedResult, $result); + } + + public function test_it_stores_one_request_properly(): void + { + $startDateTime = new DateTime(); + $bucket = new AnalyticsBucket($startDateTime); + + $featureFlag = new FeatureFlag( + "Test", + true, + BaseAttributes::USER_ID, + [], + [] + ); + + $featuritUserContext = new DefaultFeaturitUserContext( + "1234", + "1357", + "192.168.1.1" + ); + + $insertionDateTime = new DateTime(); + $bucket->addFeatureFlagRequest($featureFlag, $featuritUserContext, $insertionDateTime); + + $endDateTime = new DateTime(); + $bucket->closeBucket($endDateTime); + + $result = $bucket->jsonSerialize(); + $expectedReqs = [ + [ + "ctx" => [ + BaseAttributes::USER_ID => $featuritUserContext->getUserId(), + BaseAttributes::SESSION_ID => $featuritUserContext->getSessionId(), + BaseAttributes::IP_ADDRESS => $featuritUserContext->getIpAddress(), + ], + "flag" => [ + "featureName" => $featureFlag->name(), + "featureVersion" => $featureFlag->selectedFeatureFlagVersion()->name(), + "isActive" => $featureFlag->isActive(), + ], + "timestamp" => $insertionDateTime, + ], + ]; + + $this->assertEquals($expectedReqs, $result["reqs"]); + } + + public function test_it_stores_multiple_requests_properly(): void + { + $startDateTime = new DateTime(); + $bucket = new AnalyticsBucket($startDateTime); + + $featureFlag1 = new FeatureFlag( + "Test", + true, + BaseAttributes::USER_ID, + [], + [] + ); + + $featuritUserContext1 = new DefaultFeaturitUserContext( + "1234", + "1357", + "192.168.1.1" + ); + + $insertionDateTime1 = new DateTime(); + + $bucket->addFeatureFlagRequest($featureFlag1, $featuritUserContext1, $insertionDateTime1); + + $featuritUserContext2 = new DefaultFeaturitUserContext( + "2468", + "1357", + "192.168.1.1" + ); + + $featureFlag2 = new FeatureFlag( + "Test2", + false, + BaseAttributes::SESSION_ID, + [], + [], + new FeatureFlagVersion("v1", 100) + ); + + $insertionDateTime2 = new DateTime(); + + $bucket->addFeatureFlagRequest($featureFlag2, $featuritUserContext2, $insertionDateTime2); + + $endDateTime = new DateTime(); + $bucket->closeBucket($endDateTime); + + $result = $bucket->jsonSerialize(); + $expectedReqs = [ + [ + "ctx" => [ + BaseAttributes::USER_ID => $featuritUserContext1->getUserId(), + BaseAttributes::SESSION_ID => $featuritUserContext1->getSessionId(), + BaseAttributes::IP_ADDRESS => $featuritUserContext1->getIpAddress(), + ], + "flag" => [ + "featureName" => $featureFlag1->name(), + "featureVersion" => $featureFlag1->selectedFeatureFlagVersion()->name(), + "isActive" => $featureFlag1->isActive(), + ], + "timestamp" => $insertionDateTime1, + ], + [ + "ctx" => [ + BaseAttributes::USER_ID => $featuritUserContext2->getUserId(), + BaseAttributes::SESSION_ID => $featuritUserContext2->getSessionId(), + BaseAttributes::IP_ADDRESS => $featuritUserContext2->getIpAddress(), + ], + "flag" => [ + "featureName" => $featureFlag2->name(), + "featureVersion" => $featureFlag2->selectedFeatureFlagVersion()->name(), + "isActive" => $featureFlag2->isActive(), + ], + "timestamp" => $insertionDateTime2, + ], + ]; + + $this->assertEquals($expectedReqs, $result["reqs"]); + } +} \ No newline at end of file diff --git a/tests/Modules/Analytics/Services/AnalyticsSenderTest.php b/tests/Modules/Analytics/Services/AnalyticsSenderTest.php new file mode 100644 index 0000000..d5cec72 --- /dev/null +++ b/tests/Modules/Analytics/Services/AnalyticsSenderTest.php @@ -0,0 +1,18 @@ +addPlugin( + new BaseUriPlugin( +// $uriFactory->createUri("https://{$tenantIdentifier}.featurit.com/api/v1/{$apiKey}") + $uriFactory->createUri("http://{$tenantIdentifier}.localhost/api/v1/{$apiKey}") + ) + ); + + $clientBuilder->addPlugin( + new HeaderDefaultsPlugin( + [ + 'User-Agent' => 'FeaturIT', + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ] + ) + ); + + $analyticsService = new FeatureAnalyticsService( + $cacheFactory->setLocalCache(0, "test_analytics", false), + new AnalyticsSender($clientBuilder->getHttpClient()), + -2 + ); + + $featureFlag = new FeatureFlag( + 'TEST', + true, + 'userId', + [], + [], + null + ); + + $userContext = new DefaultFeaturitUserContext( + '1234', + '1357', + '192.168.1.1', + [] + ); + + $currentTime = new \DateTime('2021-10-10 00:00:00'); + $analyticsService->registerFeatureFlagRequest( + $featureFlag, + $userContext, + $currentTime + ); + + $this->assertTrue(true); + } + + public function test_it_works_properly_with_a_minute_time_interval(): void + { + $cacheFactory = new LocalCacheFactory(); + + $clientBuilder = new ClientBuilder(); + $uriFactory = Psr17FactoryDiscovery::findUriFactory(); + + $tenantIdentifier = 'test'; + $apiKey = '5b436559-e1d0-44be-96a3-65c716950c99'; + + $clientBuilder->addPlugin( + new BaseUriPlugin( +// $uriFactory->createUri("https://{$tenantIdentifier}.featurit.com/api/v1/{$apiKey}") + $uriFactory->createUri("http://{$tenantIdentifier}.localhost/api/v1/{$apiKey}") + ) + ); + + $clientBuilder->addPlugin( + new HeaderDefaultsPlugin( + [ + 'User-Agent' => 'FeaturIT', + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ] + ) + ); + + $analyticsService = new FeatureAnalyticsService( + $cacheFactory->setLocalCache(0, "test_analytics", false), + new AnalyticsSender($clientBuilder->getHttpClient()), + 1 + ); + + $currentTime = new \DateTime('2023-06-10 00:00:00'); + + for ($i = 0; $i < 100; $i++) { + $featureFlag = new FeatureFlag( + 'Feat', + rand(0, 1) == 0, + 'userId', + [], + [], + new FeatureFlagVersion( + rand(1, 0) == 0 ? 'v1' : 'v2', + 100 + ) + ); + + $userContext = new DefaultFeaturitUserContext( + rand(1, 5) . '@gmail.com', + '1357', + '192.168.1.5', + + [ + 'age' => rand(25, 50), + ] + ); + + $analyticsService->registerFeatureFlagRequest( + $featureFlag, + $userContext, + $currentTime + ); + + $currentTime->add(new \DateInterval('PT1S')); + + sleep(1); + } + + $this->assertTrue(true); + } +} \ No newline at end of file diff --git a/tests/Modules/Segmentation/DefaultFeaturitUserContextTest.php b/tests/Modules/Segmentation/DefaultFeaturitUserContextTest.php index 2ed59b8..b999ae9 100644 --- a/tests/Modules/Segmentation/DefaultFeaturitUserContextTest.php +++ b/tests/Modules/Segmentation/DefaultFeaturitUserContextTest.php @@ -2,6 +2,7 @@ namespace Featurit\Client\Tests\Modules\Segmentation; +use Featurit\Client\Modules\Segmentation\ConstantCollections\BaseAttributes; use Featurit\Client\Modules\Segmentation\DefaultFeaturitUserContext; use PHPUnit\Framework\TestCase; @@ -93,17 +94,17 @@ public function test_it_returns_all_custom_attributes(): void public function test_it_converts_to_array(): void { $expectedArray = [ - "userId" => 146, - "sessionId" => null, - "ipAddress" => "192.168.1.1", + BaseAttributes::USER_ID => 146, + BaseAttributes::SESSION_ID => null, + BaseAttributes::IP_ADDRESS => "192.168.1.1", "gender" => "Female", "description" => "I like trains", ]; $defaultFeaturitUserContext = new DefaultFeaturitUserContext( - $expectedArray["userId"], - $expectedArray["sessionId"], - $expectedArray["ipAddress"], + $expectedArray[BaseAttributes::USER_ID], + $expectedArray[BaseAttributes::SESSION_ID], + $expectedArray[BaseAttributes::IP_ADDRESS], [ "gender" => $expectedArray["gender"], "description" => $expectedArray["description"], @@ -116,18 +117,18 @@ public function test_it_converts_to_array(): void public function test_main_attributes_arent_overwritten_by_custom_attributes_when_it_converts_to_array(): void { $expectedArray = [ - "userId" => "totoro@gmail.com", - "sessionId" => "a124s3243e12321", - "ipAddress" => "192.168.1.1", + BaseAttributes::USER_ID => "totoro@gmail.com", + BaseAttributes::SESSION_ID => "a124s3243e12321", + BaseAttributes::IP_ADDRESS => "192.168.1.1", "birth date" => "20/10/1999", "purchase_amount" => 979.23, "currency" => "EUR", ]; $defaultFeaturitUserContext = new DefaultFeaturitUserContext( - $expectedArray["userId"], - $expectedArray["sessionId"], - $expectedArray["ipAddress"], + $expectedArray[BaseAttributes::USER_ID], + $expectedArray[BaseAttributes::SESSION_ID], + $expectedArray[BaseAttributes::IP_ADDRESS], $expectedArray ); From 9a339233216f9b17bb06bcccee23dbe78d742c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Batlle?= Date: Mon, 27 Mar 2023 21:37:26 +0200 Subject: [PATCH 2/5] WIP --- tests/FeaturitTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/FeaturitTest.php b/tests/FeaturitTest.php index 9ff1ad1..fedc496 100644 --- a/tests/FeaturitTest.php +++ b/tests/FeaturitTest.php @@ -304,6 +304,8 @@ public function test_that_cache_stores_dates_properly(): void $cachedDate = $featurit->getCache()->get("test_datetime_serdes"); $this->assertEquals($date, $cachedDate); + + $featurit->getCache()->delete("test_datetime_serdes"); } public function test_that_cache_stores_analytics_bucket_properly(): void @@ -313,11 +315,13 @@ public function test_that_cache_stores_analytics_bucket_properly(): void $date = new \DateTime("2020-02-11"); $analyticsBucket = new AnalyticsBucket($date); - $featurit->getCache()->set("test_analytics_bucket_serdes", $analyticsBucket); + $featurit->getBackupCache()->set("test_analytics_bucket_serdes", $analyticsBucket); - $cachedAnalyticsBucket = $featurit->getCache()->get("test_analytics_bucket_serdes"); + $cachedAnalyticsBucket = $featurit->getBackupCache()->get("test_analytics_bucket_serdes"); $this->assertEquals($analyticsBucket, $cachedAnalyticsBucket); + + $featurit->getBackupCache()->delete("test_analytics_bucket_serdes"); } /** From 6ef5eb5ed8d71406975fa4a7bf2d4e38141a60c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Batlle?= Date: Wed, 21 Jun 2023 20:22:22 +0200 Subject: [PATCH 3/5] WIP --- src/Featurit.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Featurit.php b/src/Featurit.php index 374bc3f..8d60cba 100644 --- a/src/Featurit.php +++ b/src/Featurit.php @@ -64,7 +64,7 @@ public function __construct( } /** - * @throws Featurit\Client\Modules\Analytics\Exceptions\InvalidApiKeyException + * @throws \Featurit\Client\HttpClient\Exceptions\InvalidApiKeyException */ public function isActive(string $featureName): bool { @@ -72,7 +72,7 @@ public function isActive(string $featureName): bool } /** - * @throws Featurit\Client\Modules\Analytics\Exceptions\InvalidApiKeyException + * @throws \Featurit\Client\HttpClient\Exceptions\InvalidApiKeyException */ public function version(string $featureName): string { From 35669065a1235b108c65aa7dca28e22853dc017c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Batlle?= Date: Wed, 28 Jun 2023 18:44:20 +0200 Subject: [PATCH 4/5] Final version of the raw analytics version, now rewriting for the minimalistic one --- .../laminascache-analytics_bucket.dat | Bin 4218 -> 3655 bytes .../Analytics/Services/AnalyticsSender.php | 1 + 2 files changed, 1 insertion(+) diff --git a/analytics/laminascache-c5/laminascache-analytics_bucket.dat b/analytics/laminascache-c5/laminascache-analytics_bucket.dat index 8965160ff91701ba36d4d91721e9d8db1975b4c0..2c3c8886236bc64bf549abf598924349c471b54c 100644 GIT binary patch literal 3655 zcmeH~-*4J55XbwcOnxtNevrWBW#LD+Fi02#RwN`85>vTL0*jq$Q1QR-9NMZ+lNxCn z4-pTrF=QLFS0e)j`&BX_ z1#u#<;h6))hbpD0F_yoT-K;VUq!g_1l4_qoH1&uhQecpmv8LNCik~VzCOj^M+-FLLu4J@< zuq@(fr2?m;B;)yKvERv(w_>#rDWE+svn?xv-TWdCqjS)mGbS8??$YQ%MXdIzX=td* zj`@+Q0%QVY?vCBrs*0u@F7r!PsJ$wyjb*`hQJG3LLcCnKvE)zazXeT!#oQ?5SYp}X zf0WjlqmG6pvYfiBsXM*`mbTHcDj6Sb6Ub6;SgLlbU`~d9xS0&YufgCNpxQ>g-j7g? z2CC|vp1%yd{&0Q;G&{8Bn;dDH4NcJSwb%2;W6%Gx@%+1BHgQ8Q@UMZaLA!OEZ@VE2 z8eVx;!D6-vXRbeY`wHt-oLGyt>vFXkuAt$SdJz2Z$ALT8^cG?Gx6_Ijmx5J)0si5Y Mw`*86PwM&n2ZdR1{Qv*} literal 4218 zcmdUyZEM>w5XbvbirzQ)^k`pRr5%Mrm%`TeRbY6rTErYDi{wIA`rUVO;%!^OGDpknt4EV1UH#CJ z+RM6p{A9nk)`xc;0gW#XGjgfp1VFamIWJe1kxyzDH6F)62>ZwPL`I%RD@XPOagZZ( zezpwmiFSUaQ#+#IR&;y0+OX+OT=Jm|_Lgi6#Xc$(S-OKL$~@!ZEYoKoPI-oqQ|KpJ zK(6!70f8)@_ha4IFX4GZ+Z>AB)xABgqh)uu7mNbv5C-6Yg8SL6Hnok(?QVD%(IBhr zL)q5dnm;(<|8a7oMJgvB5vP#4dt)FWpJGg$S|#UG4sae+lw2af#K=6$cVC5sB&q?p zV-aP~SroZU6{Q{zr-DKzi_(mUlhf}cHJ|wa=V3)zpoEFg8dS0QZFXcuWjPP&)Oa17j$8dQioAa>)ZjHP5jK zvj^3gNiGXu4E4_&s8B`Ps~h`QO$}8->r5TvHoUcp|Ij%ap^}^@&#AFZ@Z2`lFTbgW F{|R7c4<-Nr diff --git a/src/Modules/Analytics/Services/AnalyticsSender.php b/src/Modules/Analytics/Services/AnalyticsSender.php index dfd9c10..f7daa31 100644 --- a/src/Modules/Analytics/Services/AnalyticsSender.php +++ b/src/Modules/Analytics/Services/AnalyticsSender.php @@ -27,6 +27,7 @@ public function sendAnalyticsBucket(AnalyticsBucket $analyticsBucket): void ); if ($featureFlagsApiResponse->getStatusCode() != 200) { + dump("Response Status code: " . $featureFlagsApiResponse->getStatusCode()); throw new CantSendAnalyticsToServerException("Error sending Analytics to the API"); } From a634fbf82fdafecef4c29a9cc560b0c590700351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Batlle?= Date: Thu, 29 Jun 2023 14:04:07 +0200 Subject: [PATCH 5/5] Cleanup, incremented testing coverage, added method to flag the analytics module on the builder --- .gitignore | 6 +- analytics/.gitkeep | 0 .../laminascache-analytics_bucket.dat | Bin 3655 -> 481 bytes src/Endpoints/FeatureFlags.php | 9 +- src/Featurit.php | 64 ++-- src/FeaturitBuilder.php | 9 + src/Modules/Analytics/AnalyticsBucket.php | 78 +++-- .../Analytics/Services/AnalyticsSender.php | 3 - .../Services/FeatureAnalyticsService.php | 12 +- .../laminascache-analytics_bucket.dat | Bin 14899 -> 0 bytes tests/FeaturitTest.php | 1 + tests/LocalCacheFactoryTest.php | 2 +- .../Modules/Analytics/AnalyticsBucketTest.php | 300 ++++++++++++------ .../Services/AnalyticsSenderTest.php | 34 +- .../Services/FeatureAnalyticsServiceTest.php | 187 ++++++----- 15 files changed, 450 insertions(+), 255 deletions(-) create mode 100644 analytics/.gitkeep delete mode 100644 test_analytics/laminascache-c5/laminascache-analytics_bucket.dat diff --git a/.gitignore b/.gitignore index be09eec..c87b4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ vendor .idea -cache/* -!cache/.gitkeep +analytics/* +!analytics/.gitkeep backup/* !backup/.gitkeep +cache/* +!cache/.gitkeep .phpunit* \ No newline at end of file diff --git a/analytics/.gitkeep b/analytics/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/analytics/laminascache-c5/laminascache-analytics_bucket.dat b/analytics/laminascache-c5/laminascache-analytics_bucket.dat index 2c3c8886236bc64bf549abf598924349c471b54c..c7ad7762e73b49c011e8646bff85f42866313cba 100644 GIT binary patch delta 122 zcmX>u^N@MM20=>&LqjW5AhOgmF)_C^Fr9etP@JJvb+MJ9rInJAfswJUftjumSeb!= ym4Shhb)pqS-oQ#JI5W2(C)F)Au>>w`Zl#oxnwD6agAg*bQUY=^t&FW}K>z?!IwP6@ literal 3655 zcmeH~-*4J55XbwcOnxtNevrWBW#LD+Fi02#RwN`85>vTL0*jq$Q1QR-9NMZ+lNxCn z4-pTrF=QLFS0e)j`&BX_ z1#u#<;h6))hbpD0F_yoT-K;VUq!g_1l4_qoH1&uhQecpmv8LNCik~VzCOj^M+-FLLu4J@< zuq@(fr2?m;B;)yKvERv(w_>#rDWE+svn?xv-TWdCqjS)mGbS8??$YQ%MXdIzX=td* zj`@+Q0%QVY?vCBrs*0u@F7r!PsJ$wyjb*`hQJG3LLcCnKvE)zazXeT!#oQ?5SYp}X zf0WjlqmG6pvYfiBsXM*`mbTHcDj6Sb6Ub6;SgLlbU`~d9xS0&YufgCNpxQ>g-j7g? z2CC|vp1%yd{&0Q;G&{8Bn;dDH4NcJSwb%2;W6%Gx@%+1BHgQ8Q@UMZaLA!OEZ@VE2 z8eVx;!D6-vXRbeY`wHt-oLGyt>vFXkuAt$SdJz2Z$ALT8^cG?Gx6_Ijmx5J)0si5Y Mw`*86PwM&n2ZdR1{Qv*} diff --git a/src/Endpoints/FeatureFlags.php b/src/Endpoints/FeatureFlags.php index 10d81a0..41fc39c 100644 --- a/src/Endpoints/FeatureFlags.php +++ b/src/Endpoints/FeatureFlags.php @@ -101,10 +101,11 @@ public function isActive(string $featureFlagName): bool return false; } - $this->featurit->getFeatureAnalyticsService()->registerFeatureFlagRequest( - $featureFlags[$featureFlagName], - $this->featurit->getUserContext() - ); + if ($this->featurit->isAnalyticsModuleEnabled()) { + $this->featurit->getFeatureAnalyticsService()->registerFeatureFlagRequest( + $featureFlags[$featureFlagName] + ); + } return $featureFlags[$featureFlagName]->isActive(); } diff --git a/src/Featurit.php b/src/Featurit.php index 8d60cba..17ab29e 100644 --- a/src/Featurit.php +++ b/src/Featurit.php @@ -23,6 +23,8 @@ class Featurit private string $tenantIdentifier; private string $apiKey; + private bool $isAnalyticsEnabled; + private FeaturitUserContextProvider $featuritUserContextProvider; private ClientBuilder $clientBuilder; private CacheInterface $cache; @@ -41,6 +43,7 @@ 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; @@ -56,11 +59,7 @@ public function __construct( $this->featureSegmentationService = new FeatureSegmentationService(); - $this->featureAnalyticsService = new FeatureAnalyticsService( - $this->getAnalyticsCache(), - new AnalyticsSender($this->getHttpClient()), - $sendAnalyticsIntervalMinutes - ); + $this->setupAnalytics($enableAnalytics, $sendAnalyticsIntervalMinutes); } /** @@ -119,14 +118,41 @@ 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); } - public function getFeatureAnalyticsService(): FeatureAnalyticsService + /** + * @param FeaturitUserContext|null $featuritUserContext + * @param FeaturitUserContextProvider|null $featuritUserContextProvider + * @return void + */ + public function setFeaturitUserContextProvider(?FeaturitUserContext $featuritUserContext = null, ?FeaturitUserContextProvider $featuritUserContextProvider = null): void { - return $this->featureAnalyticsService; + 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; } /** @@ -175,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); + $this->isAnalyticsEnabled = $enableAnalytics; - return; - } - - if (is_null($featuritUserContextProvider)) { - $featuritUserContextProvider = new DefaultFeaturitUserContextProvider( - new DefaultFeaturitUserContext(null, null, null) - ); - } - - $this->featuritUserContextProvider = $featuritUserContextProvider; + $this->featureAnalyticsService = new FeatureAnalyticsService( + $this->getAnalyticsCache(), + new AnalyticsSender($this->getHttpClient()), + $sendAnalyticsIntervalMinutes + ); } } \ No newline at end of file diff --git a/src/FeaturitBuilder.php b/src/FeaturitBuilder.php index 870eb97..020863a 100644 --- a/src/FeaturitBuilder.php +++ b/src/FeaturitBuilder.php @@ -15,6 +15,7 @@ class FeaturitBuilder 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; @@ -37,6 +38,13 @@ 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; @@ -108,6 +116,7 @@ public function build(): Featurit $this->httpClientBuilder ?? null, $this->uriFactory ?? null, $this->featuritUserContext ?? null, + $this->isAnalyticsModuleEnabled, $this->sendAnalyticsIntervalMinutes, ); } diff --git a/src/Modules/Analytics/AnalyticsBucket.php b/src/Modules/Analytics/AnalyticsBucket.php index 7eab92b..4862c9d 100644 --- a/src/Modules/Analytics/AnalyticsBucket.php +++ b/src/Modules/Analytics/AnalyticsBucket.php @@ -5,9 +5,7 @@ use DateTime; use DateTimeInterface; use Exception; -use Featurit\Client\Modules\Segmentation\ConstantCollections\BaseAttributes; use Featurit\Client\Modules\Segmentation\Entities\FeatureFlag; -use Featurit\Client\Modules\Segmentation\FeaturitUserContext; use JsonSerializable; class AnalyticsBucket implements JsonSerializable @@ -17,11 +15,13 @@ class AnalyticsBucket implements JsonSerializable /** * [ - * [ - * "timestamp", - * "ctx" => ["userId", "sessionId", "ipAddress", "custom"], - * "flag" => ["featureName", "featureVersion", "isActive"] - * ] + * "$hour" => [ + * "$featureName" => [ + * "$featureVersion" => [ + * "$isActive" => $count, + * ], + * ], + * ], * ] * @var array */ @@ -46,40 +46,40 @@ public function startDateTime(): DateTimeInterface public function addFeatureFlagRequest( FeatureFlag $featureFlag, - FeaturitUserContext $featuritUserContext, - ?DateTimeInterface $insertionDateTime = null + DateTime $currentTime = null ): void { if ($this->isClosed()) { return; } - // Save the User Context. - $request["ctx"] = [ - BaseAttributes::USER_ID => $featuritUserContext->getUserId(), - BaseAttributes::SESSION_ID => $featuritUserContext->getSessionId(), - BaseAttributes::IP_ADDRESS => $featuritUserContext->getIpAddress(), - ]; - - if (count($featuritUserContext->getCustomAttributes()) > 0) { - $request["ctx"]["custom"] = $featuritUserContext->getCustomAttributes(); + if (is_null($currentTime)) { + $currentTime = new DateTime(); } - // Save the Feature Flag. - $request["flag"] = [ - "featureName" => $featureFlag->name(), - "featureVersion" => $featureFlag->selectedFeatureFlagVersion()->name(), - "isActive" => $featureFlag->isActive(), - ]; + // 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"] = []; + } - // Save the request timestamp. - if (is_null($insertionDateTime)) { - $insertionDateTime = new DateTime(); + if (!isset($this->requests["$hourKey"]["$flagNameKey"])) { + $this->requests["$hourKey"]["$flagNameKey"] = []; } - $request["timestamp"] = $insertionDateTime; + if (!isset($this->requests["$hourKey"]["$flagNameKey"]["$flagVersionKey"])) { + $this->requests["$hourKey"]["$flagNameKey"]["$flagVersionKey"] = []; + } - $this->requests[] = $request; + 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 @@ -125,4 +125,24 @@ public function jsonSerialize(): array "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"; + } } \ No newline at end of file diff --git a/src/Modules/Analytics/Services/AnalyticsSender.php b/src/Modules/Analytics/Services/AnalyticsSender.php index f7daa31..a7825cc 100644 --- a/src/Modules/Analytics/Services/AnalyticsSender.php +++ b/src/Modules/Analytics/Services/AnalyticsSender.php @@ -27,11 +27,8 @@ public function sendAnalyticsBucket(AnalyticsBucket $analyticsBucket): void ); if ($featureFlagsApiResponse->getStatusCode() != 200) { - dump("Response Status code: " . $featureFlagsApiResponse->getStatusCode()); throw new CantSendAnalyticsToServerException("Error sending Analytics to the API"); } - - dump("Request sent to the API"); } catch (\Http\Client\Exception $exception) { throw new CantSendAnalyticsToServerException($exception->getMessage(), $exception->getCode(), $exception); } diff --git a/src/Modules/Analytics/Services/FeatureAnalyticsService.php b/src/Modules/Analytics/Services/FeatureAnalyticsService.php index fa3df69..44a8a96 100644 --- a/src/Modules/Analytics/Services/FeatureAnalyticsService.php +++ b/src/Modules/Analytics/Services/FeatureAnalyticsService.php @@ -6,7 +6,6 @@ use Featurit\Client\Modules\Analytics\AnalyticsBucket; use Featurit\Client\Modules\Analytics\Exceptions\CantSendAnalyticsToServerException; use Featurit\Client\Modules\Segmentation\Entities\FeatureFlag; -use Featurit\Client\Modules\Segmentation\FeaturitUserContext; use Psr\SimpleCache\CacheInterface; class FeatureAnalyticsService @@ -21,7 +20,6 @@ public function __construct( public function registerFeatureFlagRequest( FeatureFlag $featureFlag, - FeaturitUserContext $featuritUserContext, DateTime $currentTime = null, ): void { @@ -35,17 +33,13 @@ public function registerFeatureFlagRequest( // Get or create the analytics bucket. if ($this->analyticsCache->has($analyticsCacheKey)) { - dump("Getting Analytics from cache"); $analyticsBucket = $this->analyticsCache->get($analyticsCacheKey); - dump("Bucket Start DateTime: " . $analyticsBucket->startDateTime()->format('c')); - dump("Now: " . $now->format('c')); } else { - dump("Creating a new bucket"); $analyticsBucket = new AnalyticsBucket($now); } - $analyticsBucket->addFeatureFlagRequest($featureFlag, $featuritUserContext, $now); - dump("Time diff: " . $analyticsBucket->startDateTime()->diff($now)->i); + $analyticsBucket->addFeatureFlagRequest($featureFlag, $currentTime); + // TODO: This approach can have problems due to sending big payloads to the server in case of failure or huge traffic. if ($analyticsBucket->startDateTime()->diff($now)->i >= $this->sendAnalyticsIntervalMinutes) { try { @@ -53,9 +47,7 @@ public function registerFeatureFlagRequest( $this->analyticsSender->sendAnalyticsBucket($analyticsBucket); $this->analyticsCache->delete($analyticsCacheKey); - dump("Analytics removed from cache"); } catch (CantSendAnalyticsToServerException $exception) { - dump("Error sending analytics to the API"); $analyticsBucket->openBucket(); $this->analyticsCache->set($analyticsCacheKey, $analyticsBucket); } diff --git a/test_analytics/laminascache-c5/laminascache-analytics_bucket.dat b/test_analytics/laminascache-c5/laminascache-analytics_bucket.dat deleted file mode 100644 index 75de6e1190386d3824870f1a9cac1f9a48092b87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14899 zcmeI2O>f&U3_$x+2HzW)lIJFLBQI~5qbL=BAPYq3(SEArn*Ic+hZK5!+? z#aMevZCRFH7>ZB!rOU=H`QXd2+g9QFd{OyTxc;=B?H0biK3kQGyHHL4`1Ec!{q95J zth=vWk-6mLuP;8SL%9v_%iyo7rB5c8Zsd}ue-Axl$1`k@OloHemzdPp)3i8+^i7(& z6kKWsY4aCbfOSd0u70go{%g3~;ANGwcmn%lhWCdyRTdtKSicE$Z7Z)Ph<^Zm+Wf4{WxnT3NC@S^JQ5r2GjL2 z9>V+AzOJkF>d*wifz5{xr>f1_Y_`SoPgjr40E*E726;S$Xs5e6#I8yQ54SJp2Y|!L z9!X(`H;Z!Kj5Ia?9K%f$FaDy8UtiNC20?R=*!*zzi{Hj!#iI!O7UBo|LaO>~8me2o z@EcAVZ-6t1UlInza`P{OVP83*!q3&YrUgC-!MB`WBEmOE2j6rb`BLyRCVWeU zZ?z&nvz=St;$uIB<-boFYUQEygVuIK{Ubk#CXE}Qq;R$Zys z=bA2%RacFECR_8g<$WRghO4@8gszKh1nd0{W8ZLH*N|OT&9)RCY6;amhU>b9?7C{t zb2Hf;&Mq?#*L4lqb=BzG-mJQY>$-;Qx@wsxD|(anxvp!-uB*1sV@+@9b6wYvU02Pv zjBM<;<$bADm+QJX)nj{wO<|u$s@~_iE|zyh`f8cSbidOe75iM*#igLb6ppUYa)HM%p<(h(6-Aa z75iM*wPe@Tqu+hKFH~__uIpN|>#ETgew(Vx*yp+~PL-H>)ardJ8~a_-=ejOVl}KN$ g=D{Mk>!&MHd7taLmh8HEnWy_V38m=sTz9|!4FPC8U;qFB diff --git a/tests/FeaturitTest.php b/tests/FeaturitTest.php index fedc496..5afa579 100644 --- a/tests/FeaturitTest.php +++ b/tests/FeaturitTest.php @@ -352,6 +352,7 @@ private function getFeaturit( $featuritBuilder = (new FeaturitBuilder()) ->setTenantIdentifier(self::TENANT_IDENTIFIER) ->setApiKey($apiKey) + ->setIsAnalyticsModuleEnabled(true) ->setCacheTtlMinutes(5) ->setSendAnalyticsIntervalMinutes(1) ->setHttpClientBuilder($clientBuilder); diff --git a/tests/LocalCacheFactoryTest.php b/tests/LocalCacheFactoryTest.php index 70dbabe..89d877f 100644 --- a/tests/LocalCacheFactoryTest.php +++ b/tests/LocalCacheFactoryTest.php @@ -8,7 +8,7 @@ class LocalCacheFactoryTest extends TestCase { - const TEST_CACHE_DIR = "test"; + const TEST_CACHE_DIR = "cache_test"; private $testCacheDir = ""; protected function setUp(): void diff --git a/tests/Modules/Analytics/AnalyticsBucketTest.php b/tests/Modules/Analytics/AnalyticsBucketTest.php index 99970c1..9f658c0 100644 --- a/tests/Modules/Analytics/AnalyticsBucketTest.php +++ b/tests/Modules/Analytics/AnalyticsBucketTest.php @@ -6,61 +6,26 @@ use Exception; use Featurit\Client\Modules\Analytics\AnalyticsBucket; use Featurit\Client\Modules\Segmentation\ConstantCollections\BaseAttributes; -use Featurit\Client\Modules\Segmentation\DefaultFeaturitUserContext; use Featurit\Client\Modules\Segmentation\Entities\FeatureFlag; use Featurit\Client\Modules\Segmentation\Entities\FeatureFlagVersion; use PHPUnit\Framework\TestCase; class AnalyticsBucketTest extends TestCase { - public function test_new_data_cant_be_added_after_closing(): void + public function test_it_serializes_properly_when_empty(): void { $startDateTime = new DateTime(); $bucket = new AnalyticsBucket($startDateTime); - $featuritUserContext = new DefaultFeaturitUserContext( - "1234", - "1357", - "192.168.1.1" - ); - - $featureFlag = new FeatureFlag( - "Test", - true, - BaseAttributes::USER_ID, - [], - [] - ); - - $insertionTime1 = new DateTime(); - $bucket->addFeatureFlagRequest($featureFlag, $featuritUserContext, $insertionTime1); - $endDateTime = new DateTime(); $bucket->closeBucket($endDateTime); - $insertionTime2 = new DateTime(); - $bucket->addFeatureFlagRequest($featureFlag, $featuritUserContext, $insertionTime2); - $result = $bucket->jsonSerialize(); $expectedResult = [ "start" => $startDateTime, "end" => $endDateTime, - "reqs" => [ - [ - "ctx" => [ - BaseAttributes::USER_ID => $featuritUserContext->getUserId(), - BaseAttributes::SESSION_ID => $featuritUserContext->getSessionId(), - BaseAttributes::IP_ADDRESS => $featuritUserContext->getIpAddress(), - ], - "flag" => [ - "featureName" => $featureFlag->name(), - "featureVersion" => $featureFlag->selectedFeatureFlagVersion()->name(), - "isActive" => $featureFlag->isActive(), - ], - "timestamp" => $insertionTime1, - ], - ], + "reqs" => [], ]; $this->assertEquals($expectedResult, $result); @@ -76,31 +41,49 @@ public function test_it_cant_be_serialized_if_not_closed(): void $bucket->jsonSerialize(); } - public function test_it_serializes_properly_when_empty(): void + public function test_it_stores_one_flag_properly(): void { $startDateTime = new DateTime(); $bucket = new AnalyticsBucket($startDateTime); + $featureFlag = new FeatureFlag( + "Test", + true, + BaseAttributes::USER_ID, + [], + [] + ); + + $bucket->addFeatureFlagRequest($featureFlag, $startDateTime); + $endDateTime = new DateTime(); $bucket->closeBucket($endDateTime); - $result = $bucket->jsonSerialize(); + $hour = $bucket->generateHourKey($startDateTime); + $flagNameKey = $bucket->generateFeatureFlagNameKey($featureFlag); + $flagVersionKey = $bucket->generateFeatureFlagVersionKey($featureFlag); + $flagIsActiveKey = $bucket->generateFeatureFlagIsActiveKey($featureFlag); - $expectedResult = [ - "start" => $startDateTime, - "end" => $endDateTime, - "reqs" => [], + $result = $bucket->jsonSerialize(); + $expectedReqs = [ + "$hour" => [ + "$flagNameKey" => [ + "$flagVersionKey" => [ + "$flagIsActiveKey" => 1, + ], + ], + ], ]; - $this->assertEquals($expectedResult, $result); + $this->assertEquals($expectedReqs, $result["reqs"]); } - public function test_it_stores_one_request_properly(): void + public function test_it_stores_multiple_different_flags_properly(): void { $startDateTime = new DateTime(); $bucket = new AnalyticsBucket($startDateTime); - $featureFlag = new FeatureFlag( + $featureFlag1 = new FeatureFlag( "Test", true, BaseAttributes::USER_ID, @@ -108,44 +91,56 @@ public function test_it_stores_one_request_properly(): void [] ); - $featuritUserContext = new DefaultFeaturitUserContext( - "1234", - "1357", - "192.168.1.1" + $bucket->addFeatureFlagRequest($featureFlag1, $startDateTime); + + $featureFlag2 = new FeatureFlag( + "Test2", + false, + BaseAttributes::SESSION_ID, + [], + [], + new FeatureFlagVersion("v1", 100) ); - $insertionDateTime = new DateTime(); - $bucket->addFeatureFlagRequest($featureFlag, $featuritUserContext, $insertionDateTime); + $bucket->addFeatureFlagRequest($featureFlag2, $startDateTime); $endDateTime = new DateTime(); $bucket->closeBucket($endDateTime); + $hour = $bucket->generateHourKey($startDateTime); + $flagNameKey1 = $bucket->generateFeatureFlagNameKey($featureFlag1); + $flagVersionKey1 = $bucket->generateFeatureFlagVersionKey($featureFlag1); + $flagIsActiveKey1 = $bucket->generateFeatureFlagIsActiveKey($featureFlag1); + + $flagNameKey2 = $bucket->generateFeatureFlagNameKey($featureFlag2); + $flagVersionKey2 = $bucket->generateFeatureFlagVersionKey($featureFlag2); + $flagIsActiveKey2 = $bucket->generateFeatureFlagIsActiveKey($featureFlag2); + $result = $bucket->jsonSerialize(); $expectedReqs = [ - [ - "ctx" => [ - BaseAttributes::USER_ID => $featuritUserContext->getUserId(), - BaseAttributes::SESSION_ID => $featuritUserContext->getSessionId(), - BaseAttributes::IP_ADDRESS => $featuritUserContext->getIpAddress(), + "$hour" => [ + "$flagNameKey1" => [ + "$flagVersionKey1" => [ + "$flagIsActiveKey1" => 1, + ], ], - "flag" => [ - "featureName" => $featureFlag->name(), - "featureVersion" => $featureFlag->selectedFeatureFlagVersion()->name(), - "isActive" => $featureFlag->isActive(), + "$flagNameKey2" => [ + "$flagVersionKey2" => [ + "$flagIsActiveKey2" => 1, + ], ], - "timestamp" => $insertionDateTime, ], ]; $this->assertEquals($expectedReqs, $result["reqs"]); } - public function test_it_stores_multiple_requests_properly(): void + public function test_it_stores_multiple_equal_flags_properly(): void { $startDateTime = new DateTime(); $bucket = new AnalyticsBucket($startDateTime); - $featureFlag1 = new FeatureFlag( + $featureFlag = new FeatureFlag( "Test", true, BaseAttributes::USER_ID, @@ -153,68 +148,165 @@ public function test_it_stores_multiple_requests_properly(): void [] ); - $featuritUserContext1 = new DefaultFeaturitUserContext( - "1234", - "1357", - "192.168.1.1" - ); + $bucket->addFeatureFlagRequest($featureFlag, $startDateTime); - $insertionDateTime1 = new DateTime(); + $bucket->addFeatureFlagRequest($featureFlag, $startDateTime); - $bucket->addFeatureFlagRequest($featureFlag1, $featuritUserContext1, $insertionDateTime1); + $endDateTime = new DateTime(); + $bucket->closeBucket($endDateTime); - $featuritUserContext2 = new DefaultFeaturitUserContext( - "2468", - "1357", - "192.168.1.1" + $hour = $bucket->generateHourKey($startDateTime); + $flagNameKey = $bucket->generateFeatureFlagNameKey($featureFlag); + $flagVersionKey = $bucket->generateFeatureFlagVersionKey($featureFlag); + $flagIsActiveKey = $bucket->generateFeatureFlagIsActiveKey($featureFlag); + + $result = $bucket->jsonSerialize(); + $expectedReqs = [ + "$hour" => [ + "$flagNameKey" => [ + "$flagVersionKey" => [ + "$flagIsActiveKey" => 2, + ], + ], + ], + ]; + + $this->assertEquals($expectedReqs, $result["reqs"]); + } + + public function test_it_stores_multiple_equal_flags_with_different_active_values_properly(): void + { + $startDateTime = new DateTime(); + $bucket = new AnalyticsBucket($startDateTime); + + $featureFlag = new FeatureFlag( + "Test", + true, + BaseAttributes::USER_ID, + [], + [] ); - $featureFlag2 = new FeatureFlag( - "Test2", + $bucket->addFeatureFlagRequest($featureFlag, $startDateTime); + + $featureFlag = new FeatureFlag( + "Test", false, - BaseAttributes::SESSION_ID, - [], + BaseAttributes::USER_ID, [], - new FeatureFlagVersion("v1", 100) + [] ); - $insertionDateTime2 = new DateTime(); - - $bucket->addFeatureFlagRequest($featureFlag2, $featuritUserContext2, $insertionDateTime2); + $bucket->addFeatureFlagRequest($featureFlag); $endDateTime = new DateTime(); $bucket->closeBucket($endDateTime); + $hour = $bucket->generateHourKey($startDateTime); + $flagNameKey = $bucket->generateFeatureFlagNameKey($featureFlag); + $flagVersionKey = $bucket->generateFeatureFlagVersionKey($featureFlag); + $result = $bucket->jsonSerialize(); $expectedReqs = [ - [ - "ctx" => [ - BaseAttributes::USER_ID => $featuritUserContext1->getUserId(), - BaseAttributes::SESSION_ID => $featuritUserContext1->getSessionId(), - BaseAttributes::IP_ADDRESS => $featuritUserContext1->getIpAddress(), - ], - "flag" => [ - "featureName" => $featureFlag1->name(), - "featureVersion" => $featureFlag1->selectedFeatureFlagVersion()->name(), - "isActive" => $featureFlag1->isActive(), + "$hour" => [ + "$flagNameKey" => [ + "$flagVersionKey" => [ + "t" => 1, + "f" => 1, + ], ], - "timestamp" => $insertionDateTime1, ], - [ - "ctx" => [ - BaseAttributes::USER_ID => $featuritUserContext2->getUserId(), - BaseAttributes::SESSION_ID => $featuritUserContext2->getSessionId(), - BaseAttributes::IP_ADDRESS => $featuritUserContext2->getIpAddress(), + ]; + + $this->assertEquals($expectedReqs, $result["reqs"]); + } + + public function test_it_stores_multiple_equal_flags_with_different_insertion_hour_in_different_keys(): void + { + $startDateTime = new DateTime("2023-06-06 11:54:48"); + $bucket = new AnalyticsBucket($startDateTime); + + $featureFlag = new FeatureFlag( + "Test", + true, + BaseAttributes::USER_ID, + [], + [] + ); + + $bucket->addFeatureFlagRequest($featureFlag, $startDateTime); + + $endDateTime = new DateTime("2023-06-06 12:00:01"); + $bucket->addFeatureFlagRequest($featureFlag, $endDateTime); + + $bucket->closeBucket($endDateTime); + + $hour1 = $bucket->generateHourKey($startDateTime); + $hour2 = $bucket->generateHourKey($endDateTime); + $flagNameKey = $bucket->generateFeatureFlagNameKey($featureFlag); + $flagVersionKey = $bucket->generateFeatureFlagVersionKey($featureFlag); + + $result = $bucket->jsonSerialize(); + $expectedReqs = [ + "$hour1" => [ + "$flagNameKey" => [ + "$flagVersionKey" => [ + "t" => 1, + ], ], - "flag" => [ - "featureName" => $featureFlag2->name(), - "featureVersion" => $featureFlag2->selectedFeatureFlagVersion()->name(), - "isActive" => $featureFlag2->isActive(), + ], + "$hour2" => [ + "$flagNameKey" => [ + "$flagVersionKey" => [ + "t" => 1, + ], ], - "timestamp" => $insertionDateTime2, ], ]; $this->assertEquals($expectedReqs, $result["reqs"]); } + + public function test_new_data_cant_be_added_after_closing(): void + { + $startDateTime = new DateTime(); + $bucket = new AnalyticsBucket($startDateTime); + + $featureFlag = new FeatureFlag( + "Test", + true, + BaseAttributes::USER_ID, + [], + [] + ); + + $bucket->addFeatureFlagRequest($featureFlag, $startDateTime); + + $endDateTime = new DateTime(); + $bucket->closeBucket($endDateTime); + + $bucket->addFeatureFlagRequest($featureFlag, $startDateTime); + + $hour = $bucket->generateHourKey($startDateTime); + $flagNameKey = $bucket->generateFeatureFlagNameKey($featureFlag); + $flagVersionKey = $bucket->generateFeatureFlagVersionKey($featureFlag); + $flagIsActiveKey = $bucket->generateFeatureFlagIsActiveKey($featureFlag); + + $result = $bucket->jsonSerialize(); + $expectedResult = [ + "start" => $startDateTime, + "end" => $endDateTime, + "reqs" => [ + "$hour" => [ + "$flagNameKey" => [ + "$flagVersionKey" => [ + "$flagIsActiveKey" => 1, + ], + ], + ], + ], + ]; + + $this->assertEquals($expectedResult, $result); + } } \ No newline at end of file diff --git a/tests/Modules/Analytics/Services/AnalyticsSenderTest.php b/tests/Modules/Analytics/Services/AnalyticsSenderTest.php index d5cec72..502db96 100644 --- a/tests/Modules/Analytics/Services/AnalyticsSenderTest.php +++ b/tests/Modules/Analytics/Services/AnalyticsSenderTest.php @@ -2,17 +2,49 @@ namespace Featurit\Client\Tests\Modules\Analytics\Services; +use DateTime; +use Featurit\Client\Modules\Analytics\AnalyticsBucket; +use Featurit\Client\Modules\Analytics\Exceptions\CantSendAnalyticsToServerException; +use Featurit\Client\Modules\Analytics\Services\AnalyticsSender; +use Http\Client\Common\HttpMethodsClientInterface; +use Laminas\Diactoros\Response; use PHPUnit\Framework\TestCase; class AnalyticsSenderTest extends TestCase { public function test_it_can_send_a_simple_request(): void { + $mockHttpMethodsClient = $this->createMock(HttpMethodsClientInterface::class); + $mockHttpMethodsClient->method('post')->willReturn(new Response( + 'php://memory', + 200 + )); + $now = new DateTime(); + $analyticsBucket = new AnalyticsBucket($now); + $analyticsBucket->closeBucket($now); + + $analyticsSender = new AnalyticsSender($mockHttpMethodsClient); + $analyticsSender->sendAnalyticsBucket($analyticsBucket); + + $this->assertTrue(true); } - public function test_it_works_properly_with_a_minute_time_interval(): void + public function test_it_sends_the_right_exception_on_non_200_http_status_response(): void { + $mockHttpMethodsClient = $this->createMock(HttpMethodsClientInterface::class); + $mockHttpMethodsClient->method('post')->willReturn(new Response( + 'php://memory', + 500 + )); + + $now = new DateTime(); + $analyticsBucket = new AnalyticsBucket($now); + $analyticsBucket->closeBucket($now); + + $this->expectException(CantSendAnalyticsToServerException::class); + $analyticsSender = new AnalyticsSender($mockHttpMethodsClient); + $analyticsSender->sendAnalyticsBucket($analyticsBucket); } } \ No newline at end of file diff --git a/tests/Modules/Analytics/Services/FeatureAnalyticsServiceTest.php b/tests/Modules/Analytics/Services/FeatureAnalyticsServiceTest.php index d3b61d0..c24437b 100644 --- a/tests/Modules/Analytics/Services/FeatureAnalyticsServiceTest.php +++ b/tests/Modules/Analytics/Services/FeatureAnalyticsServiceTest.php @@ -2,148 +2,177 @@ namespace Featurit\Client\Tests\Modules\Analytics\Services; -use Featurit\Client\HttpClient\ClientBuilder; +use DateInterval; +use DateTime; use Featurit\Client\LocalCacheFactory; 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\ConstantCollections\BaseAttributes; use Featurit\Client\Modules\Segmentation\Entities\FeatureFlag; use Featurit\Client\Modules\Segmentation\Entities\FeatureFlagVersion; -use Http\Client\Common\Plugin\BaseUriPlugin; -use Http\Client\Common\Plugin\HeaderDefaultsPlugin; -use Http\Discovery\Psr17FactoryDiscovery; use PHPUnit\Framework\TestCase; class FeatureAnalyticsServiceTest extends TestCase { - public function test_it_can_send_a_simple_request(): void + const TEST_CACHE_DIR = "analytics_test"; + private $testCacheDir = ""; + + protected function setUp(): void { - $cacheFactory = new LocalCacheFactory(); + $this->testCacheDir = join(DIRECTORY_SEPARATOR, [dirname(__FILE__), '..', self::TEST_CACHE_DIR]); + } - $clientBuilder = new ClientBuilder(); - $uriFactory = Psr17FactoryDiscovery::findUriFactory(); + protected function tearDown(): void + { + $this->deleteDirectory($this->testCacheDir); + } - $tenantIdentifier = 'test'; - $apiKey = '5b436559-e1d0-44be-96a3-65c716950c99'; + private function deleteDirectory($dir): bool + { + if (!file_exists($dir)) { + return true; + } - $clientBuilder->addPlugin( - new BaseUriPlugin( -// $uriFactory->createUri("https://{$tenantIdentifier}.featurit.com/api/v1/{$apiKey}") - $uriFactory->createUri("http://{$tenantIdentifier}.localhost/api/v1/{$apiKey}") - ) - ); + if (!is_dir($dir)) { + return unlink($dir); + } - $clientBuilder->addPlugin( - new HeaderDefaultsPlugin( - [ - 'User-Agent' => 'FeaturIT', - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ] - ) - ); + foreach (scandir($dir) as $item) { + if ($item == '.' || $item == '..') { + continue; + } + + if (!$this->deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) { + return false; + } + } + + return rmdir($dir); + } + + public function test_it_can_send_a_simple_request_with_negative_analytics_interval(): void + { + $cacheFactory = new LocalCacheFactory(); + + $mockAnalyticsSender = $this->createMock(AnalyticsSender::class); $analyticsService = new FeatureAnalyticsService( - $cacheFactory->setLocalCache(0, "test_analytics", false), - new AnalyticsSender($clientBuilder->getHttpClient()), + $cacheFactory->setLocalCache(0, self::TEST_CACHE_DIR, false), + $mockAnalyticsSender, -2 ); $featureFlag = new FeatureFlag( 'TEST', true, - 'userId', + BaseAttributes::USER_ID, [], [], null ); - $userContext = new DefaultFeaturitUserContext( - '1234', - '1357', - '192.168.1.1', - [] - ); - - $currentTime = new \DateTime('2021-10-10 00:00:00'); + $currentTime = new DateTime('2021-10-10 00:00:00'); $analyticsService->registerFeatureFlagRequest( $featureFlag, - $userContext, $currentTime ); $this->assertTrue(true); } - public function test_it_works_properly_with_a_minute_time_interval(): void + public function test_it_works_properly_with_60_seconds_time_interval(): void { $cacheFactory = new LocalCacheFactory(); - $clientBuilder = new ClientBuilder(); - $uriFactory = Psr17FactoryDiscovery::findUriFactory(); - - $tenantIdentifier = 'test'; - $apiKey = '5b436559-e1d0-44be-96a3-65c716950c99'; - - $clientBuilder->addPlugin( - new BaseUriPlugin( -// $uriFactory->createUri("https://{$tenantIdentifier}.featurit.com/api/v1/{$apiKey}") - $uriFactory->createUri("http://{$tenantIdentifier}.localhost/api/v1/{$apiKey}") - ) - ); - - $clientBuilder->addPlugin( - new HeaderDefaultsPlugin( - [ - 'User-Agent' => 'FeaturIT', - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ] - ) - ); + $mockAnalyticsSender = $this->createMock(AnalyticsSender::class); $analyticsService = new FeatureAnalyticsService( - $cacheFactory->setLocalCache(0, "test_analytics", false), - new AnalyticsSender($clientBuilder->getHttpClient()), + $cacheFactory->setLocalCache(0, self::TEST_CACHE_DIR, false), + $mockAnalyticsSender, 1 ); - $currentTime = new \DateTime('2023-06-10 00:00:00'); + $currentTime = new DateTime('2023-06-10 00:00:00'); - for ($i = 0; $i < 100; $i++) { + for ($i = 0; $i < 60; $i++) { $featureFlag = new FeatureFlag( 'Feat', rand(0, 1) == 0, - 'userId', + BaseAttributes::USER_ID, [], [], new FeatureFlagVersion( - rand(1, 0) == 0 ? 'v1' : 'v2', + rand(0, 1) == 0 ? 'v1' : 'v2', 100 ) ); - $userContext = new DefaultFeaturitUserContext( - rand(1, 5) . '@gmail.com', - '1357', - '192.168.1.5', - - [ - 'age' => rand(25, 50), - ] - ); - $analyticsService->registerFeatureFlagRequest( $featureFlag, - $userContext, $currentTime ); - $currentTime->add(new \DateInterval('PT1S')); + $currentTime->add(new DateInterval('PT1S')); sleep(1); } $this->assertTrue(true); } + +// public function test_it_works_properly_indefinitely(): void +// { +// $cacheFactory = new LocalCacheFactory(); +// +// $clientBuilder = new ClientBuilder(); +// $uriFactory = Psr17FactoryDiscovery::findUriFactory(); +// +// $tenantIdentifier = 'test'; +// $apiKey = '5b436559-e1d0-44be-96a3-65c716950c99'; +// +// $clientBuilder->addPlugin( +// new BaseUriPlugin( +//// $uriFactory->createUri("https://{$tenantIdentifier}.featurit.com/api/v1/{$apiKey}") +// $uriFactory->createUri("http://{$tenantIdentifier}.localhost/api/v1/{$apiKey}") +// ) +// ); +// +// $clientBuilder->addPlugin( +// new HeaderDefaultsPlugin( +// [ +// 'User-Agent' => 'FeaturIT', +// 'Content-Type' => 'application/json', +// 'Accept' => 'application/json', +// ] +// ) +// ); +// +// $analyticsService = new FeatureAnalyticsService( +// $cacheFactory->setLocalCache(0, self::TEST_CACHE_DIR, false), +// new AnalyticsSender($clientBuilder->getHttpClient()), +// 1 +// ); +// +// while (true) { +// $featureFlag = new FeatureFlag( +// 'Feat' . rand(0, 100), +// rand(0, 1) == 0, +// BaseAttributes::USER_ID, +// [], +// [], +// new FeatureFlagVersion( +// 'v' . rand(0, 5), +// 100 +// ) +// ); +// +// $analyticsService->registerFeatureFlagRequest( +// $featureFlag +// ); +// +// usleep(10); +// } +// +// $this->assertTrue(true); +// } } \ No newline at end of file