Commit 434afa0a authored by Emiliano Balbuena's avatar Emiliano Balbuena

(wip): Metrics

1 merge request!235WIP: Boost Campaigns (&24)
Pipeline #70348276 pending with stages
......@@ -5,6 +5,7 @@ namespace Minds\Controllers\api\v2\analytics;
use Minds\Api\Factory;
use Minds\Common\Urn;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Entities;
......@@ -23,14 +24,55 @@ class views implements Interfaces\Api
{
$viewsManager = new Core\Analytics\Views\Manager();
/** @var Core\Boost\Campaigns\Manager $campaignsManager */
$campaignsManager = Di::_()->get('Boost\Campaigns\Manager');
/** @var Core\Boost\Campaigns\Metrics $campaignsMetricsManager */
$campaignsMetricsManager = Di::_()->get('Boost\Campaigns\Metrics');
switch ($pages[0]) {
case 'boost':
$urn = new Urn(
is_numeric($pages[1]) ?
"urn:boost:newsfeed:{$pages[1]}" :
$pages[1]
);
if ($urn->getNid() === 'campaign') {
// Boost Campaigns
try {
$campaign = $campaignsManager->get((string) $urn);
$campaignsMetricsManager
->setCampaign($campaign)
->increment();
// NOTE: Campaigns have a _single_ entity, for now. Refactor this when we support multiple
// Ideally, we should use a composite URN, like: urn:campaign-entity:100000321:(urn:activity:100000500)
foreach ($campaign->getEntityUrns() as $entityUrn) {
$viewsManager->record(
(new Core\Analytics\Views\View())
->setEntityUrn($entityUrn)
->setClientMeta($_POST['client_meta'] ?? [])
);
}
} catch (\Exception $e) {
return Factory::response([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
return Factory::response([]);
}
$urn = (string) $urn;
$expire = Di::_()->get('Boost\Network\Expire');
$metrics = Di::_()->get('Boost\Network\Metrics');
$manager = Di::_()->get('Boost\Network\Manager');
$urn = "urn:boost:newsfeed:{$pages[1]}";
$boost = $manager->get($urn, [ 'hydrate' => true ]);
if (!$boost) {
return Factory::response([
......
......@@ -62,6 +62,9 @@ class BoostProvider extends Provider
$this->di->bind('Boost\Campaigns\Dispatcher', function ($di) {
return new Campaigns\Dispatcher();
}, ['useFactory' => true]);
$this->di->bind('Boost\Campaigns\Metrics', function ($di) {
return new Campaigns\Metrics();
}, ['useFactory' => true]);
$this->di->bind('Boost\Campaigns\Manager', function ($di) {
return new Campaigns\Manager();
}, ['useFactory' => true]);
......
......@@ -217,6 +217,14 @@ class Campaign implements JsonSerializable
return ($this->budget / $this->impressions) * 1000;
}
/**
* @return bool
*/
public function isDelivering()
{
return $this->getDeliveryStatus() === static::APPROVED_STATUS;
}
/**
* @param $now
* @return bool
......@@ -235,7 +243,7 @@ class Campaign implements JsonSerializable
*/
public function shouldBeCompleted($now)
{
$isDelivering = $this->getDeliveryStatus() !== static::CREATED_STATUS;
$isDelivering = $this->isDelivering();
$ended = $now >= $this->getEnd();
$fulfilled = $this->getImpressionsMet() >= $this->getImpressions();
......
......@@ -13,21 +13,28 @@ class Dispatcher
/** @var Manager */
protected $manager;
/** @var Metrics */
protected $metrics;
/**
* Dispatcher constructor.
* @param Manager $manager
* @param Metrics $metrics
* @throws Exception
*/
public function __construct(
$manager = null
$manager = null,
$metrics = null
)
{
$this->manager = $manager ?: new Manager();
$this->metrics = $metrics ?: new Metrics();
}
/**
* @param Campaign $campaignRef
* @throws CampaignException
* @throws Exception
*/
public function onLifecycle(Campaign $campaignRef)
{
......@@ -35,6 +42,16 @@ class Dispatcher
$campaign = $this->manager->get($campaignRef->getUrn());
if ($campaign->isDelivering()) {
$campaign = $this->metrics
->setCampaign($campaign)
->syncImpressionsMet();
// TODO: Save campaign every 10/20 impressions to avoid ES overloading
error_log("[BoostCampaignsDispatcher] Saving updated {$campaign->getUrn()}...");
$this->manager->sync($campaign);
}
if ($campaign->shouldBeCompleted($now)) {
error_log("[BoostCampaignsDispatcher] Completing {$campaign->getUrn()}...");
$this->manager->complete($campaign);
......
......@@ -289,6 +289,33 @@ class Manager
return $campaign;
}
/**
* @param Campaign $campaign
* @return Campaign
* @throws Exception
*/
public function sync(Campaign $campaign)
{
$this->repository->update($campaign);
$this->elasticRepository->update($campaign);
return $campaign;
}
/**
* @param Campaign $campaignRef
*/
public function onImpression(Campaign $campaignRef)
{
// Queue for lifecycle events
$this->queueClient
->setQueue('BoostCampaignDispatcher')
->send([
'campaign' => serialize($campaignRef),
]);
}
/**
* @param Campaign $campaignRef
* @return Campaign
......
<?php
/**
* Metrics
* @author edgebal
*/
namespace Minds\Core\Boost\Campaigns;
use Exception;
use Minds\Common\Urn;
use Minds\Core\Counters\Manager as Counters;
use Minds\Core\Di\Di;
use Minds\Core\Entities\Resolver;
class Metrics
{
/** @var Manager */
protected $manager;
/** @var Counters */
protected $counters;
/** @var Resolver */
protected $resolver;
/** @var Campaign */
protected $campaign;
/**
* Metrics constructor.
* @param Manager $manager
* @param Counters $counters
* @param Resolver $resolver
* @throws Exception
*/
public function __construct(
$manager = null,
$counters = null,
$resolver = null
)
{
$this->manager = $manager ?: new Manager();
$this->counters = $counters ?: Di::_()->get('Counters');
$this->resolver = $resolver ?: new Resolver();
}
/**
* @param Campaign|Urn|string $campaign
* @return Metrics
* @throws Exception
*/
public function setCampaign($campaign)
{
if (is_object($campaign) && $campaign instanceof Campaign) {
$this->campaign = $campaign;
} else {
$this->campaign = $this->manager->get((string) $campaign);
}
return $this;
}
/**
* @return bool
* @throws Exception
*/
public function increment()
{
// Increment general boosts counter
$this->counters
->setEntityGuid(0)
->setMetric('boost_impressions')
->increment();
// Increment boost counter
$this->counters
->setEntityGuid($this->campaign->getGuid())
->setMetric('boost_impressions')
->increment();
// Pass down impressions update to manager
$this->manager->onImpression($this->campaign);
// Increment entity counters
// NOTE: Campaigns have a _single_ entity, for now. Refactor this when we support multiple
// Ideally, we should use a composite URN, like: urn:campaign-entity:100000321:(urn:activity:100000500)
foreach ($this->campaign->getEntityUrns() as $entityUrn) {
$entity = $this->resolver->setOpts(['ignoreAcl' => true])->single($entityUrn);
$this->counters
->setEntityGuid($entity->guid)
->setMetric('impression')
->increment();
$this->counters
->setEntityGuid($entity->owner_guid)
->setMetric('impression')
->increment();
}
return true;
}
/**
* @return Campaign
* @throws Exception
*/
public function syncImpressionsMet()
{
$count = $this->counters
->setEntityGuid($this->campaign->getGuid())
->setMetric('boost_impressions')
->get(false);
$this->campaign
->setImpressionsMet($count);
return $this->campaign;
}
}
<?php
/**
* CountersProvider
* @author edgebal
*/
namespace Minds\Core\Counters;
use Minds\Core\Di\Provider;
class CountersProvider extends Provider
{
public function register()
{
$this->di->bind('Counters', function ($di) {
return new Manager();
}, ['useFactory' => true]);
}
}
<?php
/**
* Manager
* @author edgebal
*/
namespace Minds\Core\Counters;
use Exception;
use Minds\Core\Data\Cassandra\Client as CassandraClient;
use Minds\Core\Di\Di;
use Minds\Helpers\Counters;
class Manager
{
/** @var CassandraClient */
protected $dbClient;
/** @var int|string */
protected $entityGuid;
/** @var string */
protected $metric;
/**
* Manager constructor.
* @param CassandraClient $dbClient
*/
public function __construct(
$dbClient = null
)
{
$this->dbClient = $dbClient ?: Di::_()->get('Database\Cassandra\Cql');
}
/**
* @param int|string $entityGuid
* @return Manager
*/
public function setEntityGuid($entityGuid)
{
$this->entityGuid = $entityGuid;
return $this;
}
/**
* @param string $metric
* @return Manager
*/
public function setMetric(string $metric)
{
$this->metric = $metric;
return $this;
}
/**
* @param int $value
* @return bool
* @throws Exception
*/
public function increment($value = 1)
{
if (!$this->entityGuid) {
throw new Exception('Invalid counter entity');
}
if (!$this->metric) {
throw new Exception('Invalid counter metric');
}
Counters::increment($this->entityGuid, $this->metric, $value, $this->dbClient);
return true;
}
/**
* @param int $value
* @return bool
* @throws Exception
*/
public function decrement($value = 1)
{
if (!$this->entityGuid) {
throw new Exception('Invalid counter entity');
}
if (!$this->metric) {
throw new Exception('Invalid counter metric');
}
Counters::decrement($this->entityGuid, $this->metric, $value, $this->dbClient);
return true;
}
/**
* @param bool $cache
* @return int
* @throws Exception
*/
public function get($cache = true)
{
if (!$this->entityGuid) {
throw new Exception('Invalid counter entity');
}
if (!$this->metric) {
throw new Exception('Invalid counter metric');
}
return Counters::get($this->entityGuid, $this->metric, $cache, $this->dbClient);
}
/**
* @return bool
* @throws Exception
*/
public function clear()
{
if (!$this->entityGuid) {
throw new Exception('Invalid counter entity');
}
if (!$this->metric) {
throw new Exception('Invalid counter metric');
}
Counters::clear($this->entityGuid, $this->metric, 0, $this->dbClient);
return true;
}
}
......@@ -124,12 +124,14 @@ class Resolver
$sorted = array_filter($sorted, function($entity) { return (bool) $entity; });
// Filter out forbidden entities
// Filter out forbidden entities, if not ignoring ACL
$sorted = array_filter($sorted, function($entity) {
return $this->acl->read($entity, $this->user);
if (!($this->opts['ignoreAcl'] ?? false)) {
$sorted = array_filter($sorted, function($entity) {
return $this->acl->read($entity, $this->user);
//&& !Flags::shouldFail($entity);
});
});
}
//
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment