Commit fffb5e90 authored by Mark Harding's avatar Mark Harding

(feat): various changes when working with production datasets

1 merge request!343WIP: Entity Centric Metrics & Dashboard
Pipeline #84977250 failed with stages
in 4 minutes and 13 seconds
......@@ -11,7 +11,7 @@ use Minds\Entities;
use Minds\Helpers\Counters;
use Minds\Interfaces;
class pageview implements Interfaces\Api, Interfaces\ApiIgnorePam
class dashboards implements Interfaces\Api, Interfaces\ApiIgnorePam
{
public function get($pages)
{
......
......@@ -16,5 +16,9 @@ class AnalyticsProvider extends Provider
$this->di->bind('Analytics\Graphs\Repository', function ($di) {
return new Graphs\Repository();
}, ['useFactory' => true]);
$this->di->bind('Analytics\Dashboards\Manager', function ($di) {
return new Dashboards\Manager();
}, ['useFactory' => true]);
}
}
......@@ -22,6 +22,15 @@ class FiltersCollection implements DashboardCollectionInterface
return $this;
}
/**
* Selected ids
* @return string[]
*/
public function getSelectedIds(): array
{
return $this->selectedIds;
}
public function getSelected(): array
{
// Filters have scoped key pairs like
......@@ -34,7 +43,7 @@ class FiltersCollection implements DashboardCollectionInterface
continue;
}
$selected[$key] = $this->filters[$key];
$selected[$key]->selectOption($value);
$selected[$key]->setSelectedOption($value);
}
return $selected;
}
......
......@@ -13,6 +13,7 @@ class Manager
*/
public function getDashboardById(string $id): DashboardInterface
{
return self::DASHBOARDS[$id];
$class = self::DASHBOARDS[$id];
return new $class;
}
}
......@@ -49,7 +49,7 @@ abstract class AbstractMetric
'label' => (string) $this->label,
'permissions' => (array) $this->permissions,
'summary' => $this->summary ? (array) $this->summary->export() : null,
'visualisation' => $this->visualisation ? (array) $this->visualisation->export : null,
'visualisation' => $this->visualisation ? (array) $this->visualisation->export() : null,
];
}
}
......@@ -2,11 +2,11 @@
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Data\Elasticsearch;
use Minds\Core\Data\ElasticSearch;
class ActiveUsersMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
/** @var ElasticSearch\Client */
private $es;
/** @var string */
......@@ -35,20 +35,6 @@ class ActiveUsersMetric extends AbstractMetric
$comparisonTsMs = strtotime("-{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $comparisonTsMs,
],
];
// Use our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global'
];
// Field name to use for the aggregation
$aggField = "active::total";
// The aggregation type, this differs by resolution
......@@ -60,59 +46,65 @@ class ActiveUsersMetric extends AbstractMetric
$resolution = 'day';
$aggType = "sum";
break;
case '30d':
case 'mtd':
$resolution = 'month';
$aggType = "max";
break;
case '1y':
case 'ytd':
$resolution = 'month';
$aggType = "avg";
break;
}
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $resolution,
],
];
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $resolution,
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'fixed_interval' => $timespan->getComparisonInterval(),
'min_doc_count' => 1,
];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
'aggs' => [
'2' => [
$aggType => [
'field' => $aggField,
],
],
'aggs' => [
'1' => [
$aggType => [
'field' => $aggField,
],
],
],
],
],
];
];
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary->setValue($response['aggregations']['1']['buckets'][1]['2']['value'])
->setComparisonValue($response['aggregations']['1']['buckets'][0]['2']['value'])
$this->summary->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval());
return $this;
}
......@@ -147,7 +139,7 @@ class ActiveUsersMetric extends AbstractMetric
'resolution' => $timespan->getInterval(),
],
];
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
......@@ -182,18 +174,25 @@ class ActiveUsersMetric extends AbstractMetric
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$xValues[] = $date;
$yValues[] = $bucket['2']['value'];
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXValues($xValues)
->setYValues($yValues)
->setXLabel('Date')
->setYLabel('Count');
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
......
......@@ -21,6 +21,9 @@ class MetricSummary
/** @var int */
private $comparisonInterval = 1;
/** @var bool */
private $comparisonPositivity = true;
/**
* Export
* @param array $extras
......@@ -32,6 +35,7 @@ class MetricSummary
'current_value' => (int) $this->value,
'comparison_value' => (int) $this->comparisonValue,
'comparison_interval' => (int) $this->comparisonInterval,
'comparison_positive_inclination' => (bool) $this->comparisonPositivity,
];
}
}
......@@ -93,6 +93,8 @@ class MetricsCollection implements DashboardCollectionInterface
// Build current visualisation
$this->getSelected()->buildVisualisation();
return $this;
}
public function buildSummaries(): self
......
......@@ -33,68 +33,55 @@ class SignupsMetric extends AbstractMetric
$comparisonTsMs = strtotime("-{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $comparisonTsMs,
],
];
// Return our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global',
];
// Daily resolution
// TODO: implement this to avoid duplicated
// $must[] = [
// 'term' => [
// 'resolution' => 'day',
// ],
// ];
$aggField = "signups::total";
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
// Return our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global',
];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'fixed_interval' => $timespan->getComparisonInterval(),
'min_doc_count' => 1,
];
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
'aggs' => [
'2' => [
'sum' => [
'field' => $aggField,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $aggField,
],
],
],
],
],
];
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($response['aggregations']['1']['buckets'][1]['2']['value'])
->setComparisonValue($response['aggregations']['1']['buckets'][0]['2']['value'])
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval());
return $this;
}
......@@ -127,11 +114,11 @@ class SignupsMetric extends AbstractMetric
];
// Specify the resolution to avoid duplicates
$must[] = [
/*$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
];
];*/
// Do the query
$query = [
......@@ -167,17 +154,25 @@ class SignupsMetric extends AbstractMetric
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$xValues[] = $date;
$yValues[] = $bucket['2']['value'];
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXValues($xValues)
->setYValues($yValues)
->setXLabel('Date')
->setYLabel('Count');
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
......
......@@ -30,64 +30,67 @@ class ViewsMetric extends AbstractMetric
public function buildSummary(): self
{
$timespan = $this->timespansCollection->getSelected();
$comparisonTsMs = strtotime("-{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("midnight -{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
$must = [];
$must[]['range'] = [
'@timestamp' => [
'gte' => $comparisonTsMs,
],
];
// TODO: Allow this to be changed based on supplied filters
$aggField = "views::total";
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
];
if ($filters['view_type']) {
$aggField = "views::" . $filters['view_type']->getSelectedOption();
}
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'fixed_interval' => $timespan->getComparisonInterval(),
'min_doc_count' => 1,
];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
'aggs' => [
'2' => [
'sum' => [
'field' => $aggField,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $aggField,
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($response['aggregations']['1']['buckets'][1]['2']['value'])
->setComparisonValue($response['aggregations']['1']['buckets'][0]['2']['value'])
->setComparisonInterval($timespan->getComparisonInterval());
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval())
->setComparisonPositivity(true);
return $this;
}
......@@ -98,12 +101,17 @@ class ViewsMetric extends AbstractMetric
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$xValues = [];
$yValues = [];
// TODO: make this respect the filters
$field = "views::total";
if ($filters['view_type']) {
$field = "views::" . $filters['view_type']->getSelectedOption();
}
$must = [];
// Range must be from previous period
......@@ -113,11 +121,6 @@ class ViewsMetric extends AbstractMetric
],
];
// Use our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global'
];
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
......@@ -159,17 +162,24 @@ class ViewsMetric extends AbstractMetric
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$xValues[] = $date;
$yValues[] = $bucket['2']['value'];
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXValues($xValues)
->setYValues($yValues)
->setXLabel('Date')
->setYLabel('Count');
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
......
......@@ -24,6 +24,9 @@ class ChartVisualisation extends AbstractVisualisation
/** @var array */
private $yValues;
/** @var array */
private $buckets = [];
/**
* Export
* @param array $extras
......@@ -33,14 +36,7 @@ class ChartVisualisation extends AbstractVisualisation
{
return [
'type' => $this->type,
'x' => [
'values' => (array) $this->xValues,
'label' => $this->xLabel,
],
'y' => [
'values' => (array) $this->yValues,
'label' => $this->yLabel,
],
'buckets' => (array) $this->buckets,
];
}
}
......@@ -40,8 +40,9 @@ abstract class AbstractTimespan
'id' => (string) $this->id,
'label' => (string) $this->label,
'interval' => (string) $this->interval,
'comparison_interval' => (string) $this->comparisonInterval,
'comparison_interval' => (int) $this->comparisonInterval,
'from_ts_ms' => (int) $this->fromTsMs,
'from_ts_iso' => date('c', $this->fromTsMs / 1000),
];
}
}
......@@ -15,8 +15,8 @@ class MtdTimespan extends AbstractTimespan
/** @var int */
protected $fromTsMs;
/** @var string */
protected $comparisonInterval = 'month';
/** @var int */
protected $comparisonInterval = 28;
public function __construct()
{
......
......@@ -20,8 +20,8 @@ class TodayTimespan extends AbstractTimespan
/** @var int */
protected $fromTsMs;
/** @var string */
protected $comparisonInterval = 'day';
/** @var int */
protected $comparisonInterval = 1;
public function __construct()
{
......
......@@ -16,10 +16,10 @@ class YtdTimespan extends AbstractTimespan
protected $fromTsMs;
/** @var string */
protected $comparisonInterval = 'year';
protected $comparisonInterval = 365;
public function __construct()
{
$this->fromTsMs = strtotime('midnight first day of this year') * 1000;
$this->fromTsMs = strtotime('midnight first day of January') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class _1yTimespan extends AbstractTimespan
{
/** @var string */
protected $id = '1y';
/** @var string */
protected $label = '1 year ago';
/** @var string */
protected $interval = 'month';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 365;
public function __construct()
{
$this->fromTsMs = strtotime('midnight 365 days ago') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class _30dTimespan extends AbstractTimespan
{
/** @var string */
protected $id = '30d';
/** @var string */
protected $label = 'Last 30 days';
/** @var string */
protected $interval = 'day';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 28;
public function __construct()
{
$this->fromTsMs = strtotime('midnight 30 days ago') * 1000;
}
}
......@@ -15,7 +15,7 @@ class TrafficDashboard implements DashboardInterface
use MagicAttributes;
/** @var string */
private $timespanId = 'mtd';
private $timespanId = '30d';
/** @var string[] */
private $filterIds = [ 'platform::browser' ];
......@@ -55,6 +55,8 @@ class TrafficDashboard implements DashboardInterface
->setSelectedId($this->timespanId)
->addTimespans(
new Timespans\TodayTimespan(),
new Timespans\_30dTimespan(),
new Timespans\_1yTimespan(),
new Timespans\MtdTimespan(),
new Timespans\YtdTimespan()
);
......@@ -85,12 +87,14 @@ class TrafficDashboard implements DashboardInterface
*/
public function export(array $extras = []): array
{
$this->build();
return [
'category' => 'traffic',
'timespan' => $this->timespansCollection->getSelected()->getId(),
'timespans' => $this->timespansCollection->export(),
'metric' => $this->metricsCollection->getSelected()->getId(),
'metrics' => $this->metricsCollection->export(),
'filter' => $this->filtersCollection->getSelected()->getId(),
'filter' => $this->filtersCollection->getSelectedIds(),
'filters' => $this->filtersCollection->export(),
];
}
......
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Analytics\Metrics\Active;
use DateTime;
use Exception;
class ActiveUsersSynchroniser
{
/** @var array */
private $records = [];
/** @var Active */
private $activeMetric;
public function __construct($activeMetric = null)
{
$this->activeMetric = $activeMetric ?? new Active();
}
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
public function toRecords()
{
$date = (new DateTime())->setTimestamp($this->from);
$now = new DateTime();
$days = (int) $date->diff($now)->format('%a');
$months = round($days / 28);
// Daily resolution
foreach ($this->activeMetric->get($days) as $bucket) {
$record = new EntityCentricRecord();
$record->setEntityUrn("urn:metric:global")
->setOwnerGuid((string) 0) // Site is owner
->setTimestamp($bucket['timestamp'])
->setResolution('day')
->incrementSum('active::total', $bucket['total']);
$this->records[] = $record;
}
// Monthly resolution
foreach ($this->activeMetric->get($months, 'month') as $bucket) {
$record = new EntityCentricRecord();
$record->setEntityUrn("urn:metric:global")
->setOwnerGuid((string) 0) // Site is owner
->setTimestamp($bucket['timestamp'])
->setResolution('month')
->incrementSum('active::total', $bucket['total']);
$this->records[] = $record;
}
foreach ($this->records as $record) {
yield $record;
}
}
}
......@@ -43,14 +43,16 @@ class EntityCentricRecord
/**
* Increment views
* @param string $metric
* @param int $value
* @return DownsampledView
*/
public function incrementSum($metric): EntityCentricRecord
public function incrementSum($metric, $value = 1): EntityCentricRecord
{
if (!isset($this->sums[$metric])) {
$this->sums[$metric] = 0;
}
++$this->sums[$metric];
$this->sums[$metric] = $this->sums[$metric] + $value;
return $this;
}
}
......@@ -13,7 +13,9 @@ class Manager
{
/** @var array */
const SYNCHRONISERS = [
ViewsSynchroniser::class,
SignupsSynchroniser::class,
ActiveUsersSynchroniser::class,
// ViewsSynchroniser::class,
];
/** @var Repository */
......@@ -51,9 +53,9 @@ class Manager
$this->add($record);
yield $record;
}
// Call again incase any leftover
$this->repository->bulk();
}
// Call again incase any leftover
$this->repository->bulk();
echo "done";
}
......
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Analytics\Metrics\Signup;
use DateTime;
use Exception;
class SignupsSynchroniser
{
/** @var array */
private $records = [];
/** @var Signup */
private $signupMetric;
public function __construct($signupMetric = null)
{
$this->signupMetric = $signupMetric ?? new Signup;
}
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
public function toRecords()
{
$date = (new DateTime())->setTimestamp($this->from);
$now = new DateTime();
$days = (int) $date->diff($now)->format('%a');
foreach ($this->signupMetric->get($days) as $bucket) {
error_log($bucket['date']);
$record = new EntityCentricRecord();
$record->setEntityUrn("urn:metric:global")
->setOwnerGuid((string) 0) // Site is owner
->setTimestamp($bucket['timestamp'])
->setResolution('day')
->incrementSum('signups::total', $bucket['total']);
$this->records[] = $record;
}
foreach ($this->records as $record) {
yield $record;
}
}
}
......@@ -68,10 +68,12 @@ class ViewsSynchroniser
$entityUrn = $view->getEntityUrn();
if (!isset($this->records[$view->getEntityUrn()])) {
$timestamp = (new \DateTime())->setTimestamp($view->getTimestamp())->setTime(0, 0, 0);
$record = new EntityCentricRecord();
$record->setEntityUrn($view->getEntityUrn())
->setOwnerGuid($view->getOwnerGuid())
->setTimestamp($view->getTimestamp());
->setTimestamp($timestamp->getTimestamp())
->setResolution('day');
$this->records[$view->getEntityUrn()] = $record;
}
......
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