Commit c966ecce authored by Guy Thouret's avatar Guy Thouret

Boost Analytics and API endpoint - #1201

1 merge request!417WIP: Boost Campaigns
Pipeline #101951875 passed with stages
in 7 minutes and 11 seconds
<?php
namespace Minds\Api;
use Minds\Interfaces;
abstract class AbstractApi implements Interfaces\Api
{
protected $accessControlAllowOrigin = ['*'];
protected $accessControlAllowHeaders = [];
protected $accessControlAllowMethods = [];
protected $defaultResponse = ['status' => 'success'];
const HTTP_CODES = [
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Moved Temporarily',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Time-out',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Large',
415 => 'Unsupported Media Type',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Time-out',
505 => 'HTTP Version not supported',
];
public function __construct()
{
$this->sendAccessControlHeaders();
}
protected function sendAccessControlHeaders(): void
{
$this->sendAccessControlAllowOrigin();
$this->sendAccessControlAllowHeaders();
$this->sendAccessControlAllowMethods();
}
protected function sendAccessControlAllowOrigin(): void
{
if (!empty($this->accessControlAllowOrigin)) {
header("Access-Control-Allow-Origin: " .
$this->parseAccessControlArray($this->accessControlAllowOrigin), false);
}
}
protected function sendAccessControlAllowHeaders(): void
{
if (!empty($this->accessControlAllowHeaders)) {
header("Access-Control-Allow-Headers: " .
$this->parseAccessControlArray($this->accessControlAllowHeaders), false);
}
}
protected function sendAccessControlAllowMethods(): void
{
if (!empty($this->accessControlAllowMethods)) {
header("Access-Control-Allow-Methods: " .
$this->parseAccessControlArray($this->accessControlAllowMethods), false);
}
}
protected function parseAccessControlArray(array $accessControlArray): string
{
$output = "";
$lastHeader = end($accessControlArray);
foreach ($accessControlArray as $header) {
$output .= $header;
if ($header !== $lastHeader) {
$output .= ",";
}
}
return $output;
}
protected function setResponseCode(int $code = 200): int
{
if (!isset(self::HTTP_CODES[$code])) {
exit('Unknown http status code "' . htmlentities($code) . '"');
}
$text = self::HTTP_CODES[$code];
$protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
header("${protocol} ${code} ${text}");
return $code;
}
protected function sendArrayOfObjects($array, int $code = 200): void
{
$this->send(array_values($array), $code);
}
protected function send($responseArray, int $code = 200, $jsonOptions = 0): void
{
$responseArray = array_merge($this->defaultResponse, $responseArray);
$returnString = json_encode($responseArray, $jsonOptions);
$this->sendJsonString($returnString, $code);
}
protected function sendJsonString(string $jsonString, int $code = 200): void
{
header('Content-Type: application/json');
header('Content-Length:' . strlen($jsonString));
$this->setResponseCode($code);
echo $jsonString;
}
protected function sendInternalServerError(): void
{
$this->sendError(500);
}
protected function sendBadRequest(): void
{
$this->sendError(400);
}
protected function sendNotImplemented(): void
{
$this->sendError(501);
}
protected function sendNotModified(): void
{
$this->sendError(304);
}
protected function sendNotAcceptable(): void
{
$this->sendError(406);
}
protected function sendUnauthorised(): void
{
$this->sendError(401);
}
protected function sendSuccess(): void
{
$this->send([]);
}
protected function sendError(int $code = 406, string $message = null): void
{
if (is_null($message)) {
$message = self::HTTP_CODES[$code];
}
$this->send($this->buildError($message), $code);
}
protected function buildError(string $message): array
{
return [
'status' => 'error',
'message' => $message
];
}
public function get($pages): void
{
$this->sendNotImplemented();
}
public function post($pages): void
{
$this->sendNotImplemented();
}
public function put($pages): void
{
$this->sendNotImplemented();
}
public function delete($pages): void
{
$this->sendNotImplemented();
}
}
<?php
namespace Minds\Controllers\api\v2\boost\campaigns;
use Minds\Api\AbstractApi;
use Minds\Core\Analytics\EntityCentric\BoostViewsDaily;
class analytics extends AbstractApi
{
public function get($pages): void
{
switch ($pages[0]) {
case 'rate':
// Get current boost rate
$avgRate = (new BoostViewsDaily())->lastSevenDays()->getAvg();
$this->send(['rate' => $avgRate]);
break;
case 'days':
$days = (new BoostViewsDaily())->lastSevenDays()->getAll();
$this->send(['days' => $days]);
break;
default:
$this->sendBadRequest();
}
}
}
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Data\ElasticSearch\Client;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use Minds\Core\Di\Di;
use Minds\Helpers\Time;
class BoostViewsDaily
{
/** @var Client */
protected $es;
/** @var array */
protected $dailyViews = [];
/** @var int */
protected $totalViews = 0;
/** @var int */
protected $startDayMs;
/** @var int */
protected $endDayMs;
public function __construct(Client $esClient = null)
{
$this->es = $esClient ?: Di::_()->get('Database\ElasticSearch');
$this->lastSevenDays();
}
protected function clearData(): void
{
$this->dailyViews = [];
$this->totalViews = 0;
}
public function lastSevenDays(): self
{
return $this->dateRange(strtotime('yesterday -1 week'), strtotime('yesterday'));
}
public function dateRange(int $start, int $end): self
{
$this->clearData();
$this->startDayMs = Time::toInterval($start, Time::ONE_DAY) * 1000;
$this->endDayMs = Time::toInterval($end, Time::ONE_DAY) * 1000;
return $this;
}
protected function query(): void
{
if (!empty($this->dailyViews)) {
return;
}
$prepared = new Search();
$prepared->query($this->buildQuery());
$response = $this->es->request($prepared);
if (isset($response['aggregations']['boost_views_total'])) {
$this->totalViews = $response['aggregations']['boost_views_total']['value'];
}
if (isset($response['aggregations']['boost_views_daily']['buckets'])) {
foreach ($response['aggregations']['boost_views_daily']['buckets'] as $bucket) {
$this->dailyViews[$bucket['key']] = $bucket['boost_views']['value'];
}
}
}
protected function buildQuery(): array
{
$must = [
'range' => [
'@timestamp' => [
'gte' => $this->startDayMs,
'lte' => $this->endDayMs,
]
]
];
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'boost_views_total' => [
'sum' => [
'field' => 'views::boosted',
],
],
'boost_views_daily' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => '1d'
],
'aggs' => [
'boost_views' => [
'sum' => [
'field' => 'views::boosted'
]
]
]
]
]
]
];
return $query;
}
public function getAll(): array
{
$this->query();
return $this->dailyViews;
}
public function getTotal(): int
{
$this->query();
return $this->totalViews;
}
public function getMax(): int
{
$this->query();
return max($this->dailyViews);
}
public function getAvg(): float
{
$this->query();
return !empty($this->dailyViews) ? array_sum($this->dailyViews) / count($this->dailyViews) : 0;
}
}
<?php
namespace Spec\Minds\Core\Analytics\EntityCentric;
use Minds\Core\Analytics\EntityCentric\BoostViewsDaily;
use Minds\Core\Data\ElasticSearch\Client;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class BoostViewsDailySpec extends ObjectBehavior
{
/** @var Client */
protected $esClient;
/** @var array */
protected $response;
public function let(Client $esClient)
{
$this->beConstructedWith($esClient);
$this->esClient = $esClient;
$this->response = [
'aggregations' => [
'boost_views_total' => [
'value' => 1887
],
'boost_views_daily' => [
'buckets' => [
['key' => '1570060800', 'boost_views' => ['value' => 242]],
['key' => '1570147200', 'boost_views' => ['value' => 256]],
['key' => '1570233600', 'boost_views' => ['value' => 287]],
['key' => '1570320000', 'boost_views' => ['value' => 267]],
['key' => '1570406400', 'boost_views' => ['value' => 249]],
['key' => '1570492800', 'boost_views' => ['value' => 290]],
['key' => '1570579200', 'boost_views' => ['value' => 296]]
]
]
]
];
}
public function it_is_initializable()
{
$this->shouldHaveType(BoostViewsDaily::class);
}
public function it_should_set_last_seven_days_range()
{
$this->lastSevenDays()->shouldReturn($this);
}
public function it_should_set_date_range()
{
$start = strtotime('yesterday -1 day');
$end = strtotime('yesterday');
$this->dateRange($start, $end)->shouldReturn($this);
}
public function it_should_return_array_of_daily_views()
{
$this->esClient->request(Argument::type(Search::class))->shouldBeCalled()->willReturn($this->response);
$this->getAll()->shouldReturn([
'1570060800' => 242,
'1570147200' => 256,
'1570233600' => 287,
'1570320000' => 267,
'1570406400' => 249,
'1570492800' => 290,
'1570579200' => 296
]);
}
public function it_should_return_total_views()
{
$this->esClient->request(Argument::type(Search::class))->shouldBeCalled()->willReturn($this->response);
$this->getTotal()->shouldReturn(1887);
}
public function it_should_return_max_views()
{
$this->esClient->request(Argument::type(Search::class))->shouldBeCalled()->willReturn($this->response);
$this->getMax()->shouldReturn(296);
}
public function it_should_return_avg_views()
{
$this->esClient->request(Argument::type(Search::class))->shouldBeCalled()->willReturn($this->response);
$this->getAvg()->shouldBeApproximately(269.57, 0.01);
}
}
Please register or to comment