Skip to content
Commits on Source (2)
<?php
namespace Minds\Controllers\Cli;
use Minds\Core;
use Minds\Core\Monetization\Partners\Manager;
use Minds\Cli;
use Minds\Interfaces;
use Minds\Exceptions;
use Minds\Entities;
class PartnerEarnings extends Cli\Controller implements Interfaces\CliControllerInterface
{
public function __construct()
{
}
public function help($command = null)
{
$this->out('TBD');
}
public function exec()
{
$this->out('Missing subcommand');
}
public function sync()
{
error_reporting(E_ALL);
ini_set('display_errors', 1);
$daysAgo = $this->getOpt('daysAgo') ?: 0;
$from = $this->getOpt('from') ?: strtotime("midnight $daysAgo days ago");
$manager = new Manager();
$i = 0;
foreach ($manager->issueDeposits([ 'from' => $from ]) as $record) {
$this->out(++$i);
}
}
}
<?php
/**
* Earnings Dashboard
*/
namespace Minds\Core\Analytics\Dashboards;
use Minds\Traits\MagicAttributes;
/**
* @method TrafficDashboard setTimespanId(string $timespanId)
* @method TrafficDashboard setFilterIds(array $filtersIds)
*/
class EarningsDashboard implements DashboardInterface
{
use MagicAttributes;
/** @var string */
private $timespanId = '30d';
/** @var string[] */
private $filterIds = [ 'platform::browser' ];
/** @var string */
private $metricId = 'active_users';
/** @var Timespans\TimespansCollection */
private $timespansCollection;
/** @var Metrics\MetricsCollection */
private $metricsCollection;
/** @var Filters\FiltersCollection */
private $filtersCollection;
/** @var Visualisations\Chart\ChartSegmentsCollection */
private $chartCollection;
public function __construct(
$timespansCollection = null,
$metricsCollection = null,
$filtersCollection = null
) {
$this->timespansCollection = $timespansCollection ?? new Timespans\TimespansCollection();
$this->metricsCollection = $metricsCollection ?? new Metrics\MetricsCollection();
$this->filtersCollection = $filtersCollection ?? new Filters\FiltersCollection();
}
/**
* Build the dashboard
* @return self
*/
public function build(): self
{
$this->timespansCollection
->setSelectedId($this->timespanId)
->addTimespans(
new Timespans\TodayTimespan(),
new Timespans\_30dTimespan(),
new Timespans\_1yTimespan(),
new Timespans\MtdTimespan(),
new Timespans\YtdTimespan()
);
$this->filtersCollection
->setSelectedIds($this->filterIds)
->addFilters(
new Filters\ChannelFilter()
);
$this->metricsCollection
->setTimespansCollection($this->timespansCollection)
->setFiltersCollection($this->filtersCollection)
->setSelectedId($this->metricId)
->addMetrics(
new Metrics\Earnings\TotalEarningsMetric(),
new Metrics\Earnings\ViewsEarningsMetric(),
new Metrics\Earnings\ReferralsEarningsMetric(),
new Metrics\Earnings\SalesEarningsMetric()
)
->build();
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$this->build();
return [
'category' => 'earnings',
'timespan' => $this->timespansCollection->getSelected()->getId(),
'timespans' => $this->timespansCollection->export(),
'metric' => $this->metricsCollection->getSelected()->getId(),
'metrics' => $this->metricsCollection->export(),
'filter' => $this->filtersCollection->getSelectedIds(),
'filters' => $this->filtersCollection->export(),
];
}
}
......@@ -6,6 +6,7 @@ class Manager
const DASHBOARDS = [
'traffic' => TrafficDashboard::class,
'trending' => TrendingDashboard::class,
'earnings' => EarningsDashboard::class,
];
/**
......
......@@ -3,6 +3,7 @@ namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Traits\MagicAttributes;
/**
......@@ -43,6 +44,37 @@ abstract class AbstractMetric
/** @var FiltersCollection */
protected $filtersCollection;
/**
* Return the usd guid for metrics
* @return string
*/
protected function getUserGuid(): ?string
{
$filters = $this->filtersCollection->getSelected();
$channelFilter = $filters['channel'];
if (!$channelFilter) {
if (!Session::getLoggedInUserGuid()) {
throw new \Exception("You must be loggedin");
}
return Session::getLoggedInUserGuid();
}
if ($channelFilter->getSelectedOption() === 'all') {
if (Session::isAdmin()) {
return "";
}
$channelFilter->setSelectedOption('self');
}
if ($channelFilter->getSelectedOption() === 'self') {
return Session::getLoggedInUserGuid();
}
// TODO: check permissions first
return $channelFilter->getSelectedOption();
}
/**
* Export
* @param array $extras
......
......@@ -32,6 +32,10 @@ class ActiveUsersMetric extends AbstractMetric
*/
public function buildSummary(): self
{
if ($this->getUserGuid()) {
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
......@@ -118,6 +122,11 @@ class ActiveUsersMetric extends AbstractMetric
*/
public function buildVisualisation(): self
{
if ($this->getUserGuid()) {
$this->visualisation = (new Visualisations\ChartVisualisation());
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$xValues = [];
$yValues = [];
......
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
abstract class AbstractEarningsMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var string */
protected $id = '';
/** @var string */
protected $label = '';
/** @var string */
protected $description = '';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $unit = 'usd';
/** @var string */
protected $aggField = '';
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("midnight -{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $this->aggField,
],
],
],
],
];
// 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($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval())
->setComparisonPositivity(true);
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 1,
],
'aggs' => [
'2' => [
'sum' => [
'field' => $this->aggField,
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$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);
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class ReferralsEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_referrals';
/** @var string */
protected $label = 'Referrals USD';
/** @var string */
protected $description = 'Referral earnings for PRO users';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::referrals';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class SalesEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_sales';
/** @var string */
protected $label = 'Sales USD';
/** @var string */
protected $description = 'Sales earnings for PRO users';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::sales';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class TotalEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_total';
/** @var string */
protected $label = 'Total Earnings';
/** @var string */
protected $description = 'Total earnings for PRO users';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::total';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class ViewsEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_views';
/** @var string */
protected $label = 'Views USD';
/** @var string */
protected $description = 'Views earnings for PRO users';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::views';
}
......@@ -70,6 +70,9 @@ class MetricsCollection implements DashboardCollectionInterface
*/
public function getSelected(): AbstractMetric
{
if (!isset($this->metrics[$this->selectedId])) {
$this->selectedId = key($this->metrics);
}
return $this->metrics[$this->selectedId];
}
......
......@@ -32,6 +32,10 @@ class SignupsMetric extends AbstractMetric
*/
public function buildSummary(): self
{
if ($this->getUserGuid()) {
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$comparisonTsMs = strtotime("-{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
......@@ -95,6 +99,11 @@ class SignupsMetric extends AbstractMetric
*/
public function buildVisualisation(): self
{
if ($this->getUserGuid()) {
$this->visualisation = (new Visualisations\ChartVisualisation());
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$xValues = [];
$yValues = [];
......
......@@ -3,7 +3,7 @@ namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\Elasticsearch;
use Minds\Core\Data\ElasticSearch;
class ViewsMetric extends AbstractMetric
{
......@@ -48,14 +48,7 @@ class ViewsMetric extends AbstractMetric
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
......@@ -133,13 +126,6 @@ class ViewsMetric extends AbstractMetric
],
];
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
......@@ -203,24 +189,4 @@ class ViewsMetric extends AbstractMetric
return $this;
}
private function getUserGuid(): ?string
{
$filters = $this->filtersCollection->getSelected();
$channelFilter = $filters['channel'];
if (!$channelFilter) {
return "";
}
if ($channelFilter->getSelectedOption() === 'self') {
return Session::getLoggedInUserGuid();
}
if ($channelFilter->getSelectedOption() === 'all') {
return "";
}
// TODO: check permissions first
return $channelFilter->getSelectedOption();
}
}
......@@ -2,14 +2,18 @@
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Entities\Resolver;
use Minds\Common\Urn;
class ViewsTableMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var Resolver */
private $entitiesResolver;
/** @var string */
protected $id = 'views_table';
......@@ -25,6 +29,7 @@ class ViewsTableMetric extends AbstractMetric
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
$this->entitiesResolver = $entitiesResolver ?? new Resolver();
}
/**
......@@ -67,11 +72,11 @@ class ViewsTableMetric extends AbstractMetric
];
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
];
// $must[] = [
// 'term' => [
// 'resolution' => $timespan->getInterval(),
// ],
// ];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
......@@ -107,10 +112,10 @@ class ViewsTableMetric extends AbstractMetric
],
],
'views::organic' => [
'sum' => [
'sum' => [
'field' => 'views::organic',
],
],
],
'views::single' => [
'sum' => [
'field' => 'views::single',
......@@ -126,45 +131,47 @@ class ViewsTableMetric extends AbstractMetric
$prepared = new ElasticSearch\Prepared\Search();
$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);
$subBuckets = [];
$entity = $this->entitiesResolver->single(new Urn($bucket['key']));
$buckets[] = [
'key' => $bucket['key'],
'values' => [
'views::total' => $bucket['views::total']['value'],
'views::organic' => $bucket['views::organic']['value'],
'views::single' => $bucket['views::single']['value'],
],
'entity' => $entity ? $entity->export() : null,
'views::total' => $bucket['views::total']['value'],
'views::organic' => $bucket['views::organic']['value'],
'views::single' => $bucket['views::single']['value'],
],
];
}
$this->visualisation = (new Visualisations\TableVisualisation())
->setBuckets($buckets)
->setColumns([ 'views::total', 'views::organic', 'views::single']);
->setColumns([
[
'id' => 'entity',
'label' => '',
'order' => 0,
],
[
'id' => 'views::total',
'label' => 'Total Views',
'order' => 1,
],
[
'id' => 'views::organic',
'label' => 'Organic',
'order' => 2,
],
[
'id' => 'views::single',
'label' => 'Single',
'order' => 3,
]
]);
return $this;
}
private function getUserGuid(): ?string
{
$filters = $this->filtersCollection->getSelected();
$channelFilter = $filters['channel'];
if (!$channelFilter) {
return "";
}
if ($channelFilter->getSelectedOption() === 'self') {
return Session::getLoggedInUserGuid();
}
if ($channelFilter->getSelectedOption() === 'all') {
return "";
}
// TODO: check permissions first
return $channelFilter->getSelectedOption();
}
}
......@@ -63,7 +63,7 @@ class TrafficDashboard implements DashboardInterface
$this->filtersCollection
->setSelectedIds($this->filterIds)
->addFilters(
new Filters\PlatformFilter(),
// new Filters\PlatformFilter(),
new Filters\ViewTypeFilter(),
new Filters\ChannelFilter()
);
......
......@@ -60,7 +60,7 @@ class TrendingDashboard implements DashboardInterface
$this->filtersCollection
->setSelectedIds($this->filterIds)
->addFilters(
new Filters\PlatformFilter(),
// new Filters\PlatformFilter(),
new Filters\ViewTypeFilter(),
new Filters\ChannelFilter()
);
......
......@@ -60,4 +60,12 @@ class EntityCentricRecord
$this->sums[$metric] = $this->sums[$metric] + $value;
return $this;
}
/**
* @return string
*/
public function getUrn(): string
{
return (string) implode('-', [ $this->getEntityUrn(), $this->getResolution(), $this->getTimestamp() ]);
}
}
......@@ -13,6 +13,7 @@ class Manager
{
/** @var array */
const SYNCHRONISERS = [
PartnerEarningsSynchroniser::class,
SignupsSynchroniser::class,
ActiveUsersSynchroniser::class,
ViewsSynchroniser::class,
......@@ -21,6 +22,9 @@ class Manager
/** @var Repository */
protected $repository;
/** @var Sums */
protected $sums;
/** @var int */
private $from;
......@@ -28,9 +32,11 @@ class Manager
private $to;
public function __construct(
$repository = null
$repository = null,
$sums = null
) {
$this->repository = $repository ?: new Repository();
$this->repository = $repository ?? new Repository();
$this->sums = $sums ?? new Sums();
}
/**
......@@ -81,4 +87,13 @@ class Manager
public function getAggregateByQuery(array $query): array
{
}
/**
* @param array $opts
* @retun iterable
*/
public function getListAggregatedByOwner(array $opts = []): iterable
{
return $this->sums->getByOwner($opts);
}
}
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Monetization\Partners\Manager as PartnersManager;
use DateTime;
use Exception;
class PartnerEarningsSynchroniser
{
/** @var PartnersManager */
private $partnersManager;
public function __construct($partnersManager = null)
{
$this->partnersManager = $partnersManager ?? new PartnersManager;
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Convert to records
* @return iterable
*/
public function toRecords(): iterable
{
$opts = [];
$opts['from'] = $this->from;
$records = [];
$i = 0;
while (true) {
$result = $this->partnersManager->getList($opts);
$opts['offset'] = $result->getPagingToken();
foreach ($result as $deposit) {
$urn = "urn:user:{$deposit->getUserGuid()}";
$record = new EntityCentricRecord();
$record->setEntityUrn($urn)
->setOwnerGuid($deposit->getUserGuid())
->setTimestamp($deposit->getTimestamp()) // TODO: confirm if this should be rounded to midnight
->setResolution('day');
// In order to increment sums, replace with what has already been seen
if (isset($records[$record->getUrn()])) {
$record = $records[$record->getUrn()];
}
$record->incrementSum('usd_earnings::total', $deposit->getAmountCents());
$record->incrementSum("usd_earnings::{$deposit->getItem()}", $deposit->getAmountCents());
$records[$record->getUrn()] = $record;
}
if ($result->isLastPage()) {
break;
}
}
foreach ($records as $record) {
var_dump($record);
yield $record;
}
}
}
<?php
/**
* EntityCentric Sums
* @author Mark
*/
namespace Minds\Core\Analytics\EntityCentric;
use DateTime;
use DateTimeZone;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Data\ElasticSearch\Client as ElasticClient;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Di\Di;
class Sums
{
/** @var ElasticClient */
protected $es;
/**
* Repository constructor.
* @param ElasticClient $es
*/
public function __construct(
$es = null
) {
$this->es = $es ?: Di::_()->get('Database\ElasticSearch');
}
public function getByOwner(array $opts = []): iterable
{
$opts = array_merge([
'fields' => [],
'from' => time(),
], $opts);
$must = [];
$must[] = [
'range' => [
'@timestamp' => [
'gte' => $opts['from'] * 1000,
'lt' => strtotime('+1 day', $opts['from']) * 1000,
],
],
];
$termsAgg = [];
foreach ($opts['fields'] as $field) {
$termsAgg[$field] = [
'sum' => [
'field' => $field,
],
];
$must[] = [
'exists' => [
'field' => $field,
],
];
}
$partition = 0;
$partitions = 100;
$partitionSize = 5000; // Allows for 500,000 users
while (++$partition < $partitions) {
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'terms' => [
'field' => 'owner_guid',
'min_doc_count' => 1,
'size' => $partitionSize,
'include' => [
'partition' => $partition,
'num_partitions' => $partitions,
],
],
'aggs' => $termsAgg,
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
yield $bucket;
}
}
}
}