Skip to content
GitLab
Menu
Why GitLab
Pricing
Contact Sales
Explore
Why GitLab
Pricing
Contact Sales
Explore
Sign in
Get free trial
Commits on Source (2)
(feat): adds engagement tab to analytics
· 0f228e80
Mark Harding
authored
Nov 04, 2019
0f228e80
Merge branch 'feat/engagement-analytics'
· 50b4f656
Mark Harding
authored
Nov 04, 2019
50b4f656
Show whitespace changes
Inline
Side-by-side
Core/Analytics/Dashboards/EngagementDashboard.php
0 → 100644
View file @
50b4f656
<?php
/**
* Engagement Dashboard
*/
namespace
Minds\Core\Analytics\Dashboards
;
use
Minds\Entities\User
;
use
Minds\Traits\MagicAttributes
;
/**
* @method EngagementDashboard setTimespanId(string $timespanId)
* @method EngagementDashboard setFilterIds(array $filtersIds)
* @method EngagementDashboard setUser(User $user)
*/
class
EngagementDashboard
implements
DashboardInterface
{
use
MagicAttributes
;
/** @var string */
private
$timespanId
=
'30d'
;
/** @var string[] */
private
$filterIds
=
[
'platform::browser'
];
/** @var string */
private
$metricId
=
'votes_up'
;
/** @var Timespans\TimespansCollection */
private
$timespansCollection
;
/** @var Metrics\MetricsCollection */
private
$metricsCollection
;
/** @var Filters\FiltersCollection */
private
$filtersCollection
;
/** @var User */
private
$user
;
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
)
->
setUser
(
$this
->
user
)
->
addFilters
(
new
Filters\ChannelFilter
()
);
$this
->
metricsCollection
->
setTimespansCollection
(
$this
->
timespansCollection
)
->
setFiltersCollection
(
$this
->
filtersCollection
)
->
setSelectedId
(
$this
->
metricId
)
->
setUser
(
$this
->
user
)
->
addMetrics
(
new
Metrics\Engagement\VotesUpMetric
(),
new
Metrics\Engagement\CommentsMetric
(),
new
Metrics\Engagement\RemindsMetric
(),
new
Metrics\Engagement\SubscribersMetric
()
)
->
build
();
return
$this
;
}
/**
* Export
* @param array $extras
* @return array
*/
public
function
export
(
array
$extras
=
[]):
array
{
$this
->
build
();
return
[
'category'
=>
'engagement'
,
'label'
=>
'Engagement'
,
'description'
=>
''
,
'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
(),
];
}
}
Core/Analytics/Dashboards/Manager.php
View file @
50b4f656
...
...
@@ -7,6 +7,7 @@ class Manager
'traffic'
=>
TrafficDashboard
::
class
,
'trending'
=>
TrendingDashboard
::
class
,
'earnings'
=>
EarningsDashboard
::
class
,
'engagement'
=>
EngagementDashboard
::
class
,
];
/**
...
...
Core/Analytics/Dashboards/Metrics/ActiveUsersMetric.php
View file @
50b4f656
...
...
@@ -39,7 +39,7 @@ class ActiveUsersMetric extends AbstractMetric
$timespan
=
$this
->
timespansCollection
->
getSelected
();
$filters
=
$this
->
filtersCollection
->
getSelected
();
$comparisonTsMs
=
strtotime
(
"-
{
$timespan
->
getComparisonInterval
()
}
days"
,
$timespan
->
getFromTsMs
()
/
1000
)
*
1000
;
$comparisonTsMs
=
strtotime
(
"
midnight
-
{
$timespan
->
getComparisonInterval
()
}
days"
,
$timespan
->
getFromTsMs
()
/
1000
)
*
1000
;
$currentTsMs
=
$timespan
->
getFromTsMs
();
// Field name to use for the aggregation
...
...
@@ -76,10 +76,14 @@ class ActiveUsersMetric extends AbstractMetric
],
];
$must
[][
'exists'
]
=
[
'field'
=>
'active::total'
,
];
$must
[][
'range'
]
=
[
'@timestamp'
=>
[
'gte'
=>
$tsMs
,
'lt
e
'
=>
strtotime
(
"midnight +
{
$timespan
->
getComparisonInterval
()
}
days"
,
$tsMs
/
1000
)
*
1000
,
'lt'
=>
strtotime
(
"midnight
tomorrow
+
{
$timespan
->
getComparisonInterval
()
}
days"
,
$tsMs
/
1000
)
*
1000
,
],
];
...
...
@@ -145,6 +149,10 @@ class ActiveUsersMetric extends AbstractMetric
'entity_urn'
=>
'urn:metric:global'
];
$must
[][
'exists'
]
=
[
'field'
=>
'active::total'
,
];
// Specify the resolution to avoid duplicates
$must
[]
=
[
'term'
=>
[
...
...
Core/Analytics/Dashboards/Metrics/Earnings/AbstractEarningsMetric.php
View file @
50b4f656
...
...
@@ -54,7 +54,7 @@ abstract class AbstractEarningsMetric extends AbstractMetric
$must
[][
'range'
]
=
[
'@timestamp'
=>
[
'gte'
=>
$tsMs
,
'lt
e
'
=>
strtotime
(
"midnight +
{
$timespan
->
getComparisonInterval
()
}
days"
,
$tsMs
/
1000
)
*
1000
,
'lt'
=>
strtotime
(
"midnight
tomorrow
+
{
$timespan
->
getComparisonInterval
()
}
days"
,
$tsMs
/
1000
)
*
1000
,
],
];
...
...
@@ -66,6 +66,12 @@ abstract class AbstractEarningsMetric extends AbstractMetric
];
}
$must
[]
=
[
'exists'
=>
[
'field'
=>
$this
->
aggField
,
],
];
$query
=
[
'index'
=>
'minds-entitycentric-*'
,
'size'
=>
0
,
...
...
@@ -127,6 +133,12 @@ abstract class AbstractEarningsMetric extends AbstractMetric
];
}
$must
[]
=
[
'exists'
=>
[
'field'
=>
$this
->
aggField
,
],
];
// Do the query
$query
=
[
'index'
=>
'minds-entitycentric-*'
,
...
...
Core/Analytics/Dashboards/Metrics/Engagement/AbstractEngagementMetric.php
0 → 100644
View file @
50b4f656
<?php
namespace
Minds\Core\Analytics\Dashboards\Metrics\Engagement
;
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
AbstractEngagementMetric
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
=
'number'
;
/** @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
=
[];
$maxTs
=
strtotime
(
"midnight tomorrow +
{
$timespan
->
getComparisonInterval
()
}
days"
,
$tsMs
/
1000
);
$must
[][
'range'
]
=
[
'@timestamp'
=>
[
'gte'
=>
$tsMs
,
'lt'
=>
$maxTs
*
1000
,
],
];
if
(
$userGuid
=
$this
->
getUserGuid
())
{
$must
[]
=
[
'term'
=>
[
'owner_guid'
=>
$userGuid
,
],
];
}
$must
[]
=
[
'exists'
=>
[
'field'
=>
$this
->
aggField
,
],
];
$indexes
=
implode
(
','
,
[
'minds-entitycentric-'
.
date
(
'm-Y'
,
$tsMs
/
1000
),
'minds-entitycentric-'
.
date
(
'm-Y'
,
$maxTs
),
]);
$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
,
],
];
}
$must
[]
=
[
'exists'
=>
[
'field'
=>
$this
->
aggField
,
],
];
// 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'
=>
0
,
'extended_bounds'
=>
[
'min'
=>
$timespan
->
getFromTsMs
(),
'max'
=>
time
()
*
1000
,
],
],
'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
;
}
}
Core/Analytics/Dashboards/Metrics/Engagement/CommentsMetric.php
0 → 100644
View file @
50b4f656
<?php
namespace
Minds\Core\Analytics\Dashboards\Metrics\Engagement
;
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
CommentsMetric
extends
AbstractEngagementMetric
{
/** @var string */
protected
$id
=
'comments'
;
/** @var string */
protected
$label
=
'Comments'
;
/** @var string */
protected
$description
=
"Number of comments you have received on your content"
;
/** @var array */
protected
$permissions
=
[
'user'
,
'admin'
];
/** @var string */
protected
$aggField
=
'comment::total'
;
}
Core/Analytics/Dashboards/Metrics/Engagement/RemindsMetric.php
0 → 100644
View file @
50b4f656
<?php
namespace
Minds\Core\Analytics\Dashboards\Metrics\Engagement
;
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
RemindsMetric
extends
AbstractEngagementMetric
{
/** @var string */
protected
$id
=
'reminds'
;
/** @var string */
protected
$label
=
'Reminds'
;
/** @var string */
protected
$description
=
"Number of reminds you have received on your content"
;
/** @var array */
protected
$permissions
=
[
'user'
,
'admin'
];
/** @var string */
protected
$aggField
=
'remind::total'
;
}
Core/Analytics/Dashboards/Metrics/Engagement/SubscribersMetric.php
0 → 100644
View file @
50b4f656
<?php
namespace
Minds\Core\Analytics\Dashboards\Metrics\Engagement
;
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
SubscribersMetric
extends
AbstractEngagementMetric
{
/** @var string */
protected
$id
=
'subscribers'
;
/** @var string */
protected
$label
=
'Subscribes'
;
/** @var string */
protected
$description
=
"Number of subscribers your channel has gained"
;
/** @var array */
protected
$permissions
=
[
'user'
,
'admin'
];
/** @var string */
protected
$aggField
=
'subscribe::total'
;
}
Core/Analytics/Dashboards/Metrics/Engagement/VotesUpMetric.php
0 → 100644
View file @
50b4f656
<?php
namespace
Minds\Core\Analytics\Dashboards\Metrics\Engagement
;
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
VotesUpMetric
extends
AbstractEngagementMetric
{
/** @var string */
protected
$id
=
'votes_up'
;
/** @var string */
protected
$label
=
'Votes up'
;
/** @var string */
protected
$description
=
"Number of votes up you have received on your content"
;
/** @var array */
protected
$permissions
=
[
'user'
,
'admin'
];
/** @var string */
protected
$aggField
=
'vote:up::total'
;
}
Core/Analytics/Dashboards/Metrics/SignupsMetric.php
View file @
50b4f656
...
...
@@ -54,7 +54,7 @@ class SignupsMetric extends AbstractMetric
$must
[][
'range'
]
=
[
'@timestamp'
=>
[
'gte'
=>
$tsMs
,
'lt
e
'
=>
strtotime
(
"midnight +
{
$timespan
->
getComparisonInterval
()
}
days"
,
$tsMs
/
1000
)
*
1000
,
'lt'
=>
strtotime
(
"midnight
tomorrow
+
{
$timespan
->
getComparisonInterval
()
}
days"
,
$tsMs
/
1000
)
*
1000
,
],
];
...
...
Core/Analytics/Dashboards/Timespans/MtdTimespan.php
View file @
50b4f656
...
...
@@ -16,7 +16,7 @@ class MtdTimespan extends AbstractTimespan
protected
$fromTsMs
;
/** @var int */
protected
$comparisonInterval
=
28
;
protected
$comparisonInterval
=
30
;
public
function
__construct
()
{
...
...
Core/Analytics/Dashboards/Timespans/_30dTimespan.php
View file @
50b4f656
...
...
@@ -16,7 +16,7 @@ class _30dTimespan extends AbstractTimespan
protected
$fromTsMs
;
/** @var int */
protected
$comparisonInterval
=
28
;
protected
$comparisonInterval
=
30
;
public
function
__construct
()
{
...
...
Core/Analytics/EntityCentric/EngagementSynchroniser.php
0 → 100644
View file @
50b4f656
<?php
namespace
Minds\Core\Analytics\EntityCentric
;
use
Minds\Core\Data\ElasticSearch
;
use
Minds\Core\Di\Di
;
use
DateTime
;
use
Exception
;
class
EngagementSynchroniser
{
/** @var array */
private
$records
=
[];
/** @var ElasticSearch\Client */
private
$es
;
public
function
__construct
(
$es
=
null
)
{
$this
->
es
=
$es
??
Di
::
_
()
->
get
(
'Database\ElasticSearch'
);
}
/**
* @param int $from
* @return self
*/
public
function
setFrom
(
$from
):
self
{
$this
->
from
=
$from
;
return
$this
;
}
/**
* Convert to records
* @return iterable
*/
public
function
toRecords
():
iterable
{
$date
=
(
new
DateTime
())
->
setTimestamp
(
$this
->
from
);
$now
=
new
DateTime
();
$days
=
(
int
)
$date
->
diff
(
$now
)
->
format
(
'%a'
);
$months
=
round
(
$days
/
28
);
$i
=
0
;
foreach
(
$this
->
getEntitiesMetrics
()
as
$buckets
)
{
$urn
=
null
;
$ownerGuid
=
null
;
if
(
!
$buckets
[
'type'
][
'buckets'
][
0
][
'key'
]
&&
$buckets
[
'metrics'
][
'buckets'
][
0
][
'key'
]
===
'subscribe'
)
{
$urn
=
"urn:user:
{
$buckets
[
'key'
]
}
"
;
$ownerGuid
=
(
string
)
$buckets
[
'key'
];
}
elseif
(
!
$buckets
[
'type'
][
'buckets'
][
0
][
'key'
])
{
echo
"
\n
Engagement: skipping as no type"
;
continue
;
}
else
{
$urn
=
"urn:
{
$buckets
[
'type'
][
'buckets'
][
0
][
'key'
]
}
:
{
$buckets
[
'key'
]
}
"
;
$ownerGuid
=
(
string
)
$buckets
[
'owner'
][
'buckets'
][
0
][
'key'
];
if
(
$buckets
[
'type'
][
'buckets'
][
0
][
'key'
]
===
'object'
)
{
$urn
=
"urn:
{
$buckets
[
'subtype'
][
'buckets'
][
0
][
'key'
]
}
:
{
$buckets
[
'key'
]
}
"
;
}
}
$record
=
new
EntityCentricRecord
();
$record
->
setEntityUrn
(
$urn
)
->
setOwnerGuid
(
$ownerGuid
)
->
setTimestamp
(
$this
->
from
)
->
setResolution
(
'day'
);
foreach
(
$buckets
[
'metrics'
][
'buckets'
]
as
$metrics
)
{
$record
->
incrementSum
(
$metrics
[
'key'
]
.
'::total'
,
(
int
)
$metrics
[
'doc_count'
]);
}
$this
->
records
[]
=
$record
;
++
$i
;
error_log
(
"Engagement:
$i
"
);
}
foreach
(
$this
->
records
as
$record
)
{
yield
$record
;
}
}
private
function
getEntitiesMetrics
()
{
$opts
=
array_merge
([
'fields'
=>
[],
'from'
=>
time
(),
],
[]);
$must
=
[];
// $must[] = [
// 'term' => [
// 'action.keyword' => 'subscribe',
// ],
//];
$must
[]
=
[
'range'
=>
[
'@timestamp'
=>
[
'gte'
=>
$this
->
from
*
1000
,
'lt'
=>
strtotime
(
'+1 day'
,
$this
->
from
)
*
1000
,
],
],
];
$partition
=
0
;
$partitions
=
50
;
$partitionSize
=
5000
;
// Allows for 250,000 entities
$index
=
'minds-metrics-'
.
date
(
'm-Y'
,
$this
->
from
);
while
(
++
$partition
<
$partitions
)
{
// Do the query
$query
=
[
'index'
=>
$index
,
'size'
=>
0
,
'body'
=>
[
'query'
=>
[
'bool'
=>
[
'must'
=>
$must
,
],
],
'aggs'
=>
[
'1'
=>
[
'terms'
=>
[
'field'
=>
'entity_guid.keyword'
,
'min_doc_count'
=>
1
,
'size'
=>
$partitionSize
,
'include'
=>
[
'partition'
=>
$partition
,
'num_partitions'
=>
$partitions
,
],
],
'aggs'
=>
[
'metrics'
=>
[
'terms'
=>
[
'field'
=>
'action.keyword'
,
'min_doc_count'
=>
1
,
],
],
'owner'
=>
[
'terms'
=>
[
'field'
=>
'entity_owner_guid.keyword'
,
'min_doc_count'
=>
1
,
],
],
'type'
=>
[
'terms'
=>
[
'field'
=>
'entity_type.keyword'
,
],
],
'subtype'
=>
[
'terms'
=>
[
'field'
=>
'entity_subtype.keyword'
,
]
],
],
],
],
],
];
// Query elasticsearch
$prepared
=
new
ElasticSearch\Prepared\Search
();
$prepared
->
query
(
$query
);
$response
=
$this
->
es
->
request
(
$prepared
);
foreach
(
$response
[
'aggregations'
][
'1'
][
'buckets'
]
as
$bucket
)
{
yield
$bucket
;
}
}
}
}
Core/Analytics/EntityCentric/Manager.php
View file @
50b4f656
...
...
@@ -13,6 +13,7 @@ class Manager
{
/** @var array */
const
SYNCHRONISERS
=
[
EngagementSynchroniser
::
class
,
PartnerEarningsSynchroniser
::
class
,
SignupsSynchroniser
::
class
,
ActiveUsersSynchroniser
::
class
,
...
...