import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
+import { LoginGuard } from '@app/core'
import { VideoResolver } from '@app/shared/shared-main'
import { VideoStatsComponent } from './video'
const statsRoutes: Routes = [
{
path: 'videos/:videoId',
+ canActivate: [ LoginGuard ],
component: VideoStatsComponent,
data: {
meta: {
import { ChartModule } from 'primeng/chart'
import { NgModule } from '@angular/core'
+import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main'
+import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
import { StatsRoutingModule } from './stats-routing.module'
import { VideoStatsComponent, VideoStatsService } from './video'
StatsRoutingModule,
SharedMainModule,
+ SharedFormModule,
SharedGlobalIconModule,
+ SharedVideoLiveModule,
ChartModule
],
<div class="margin-content">
- <h1 class="title-page title-page-single" i18n>Stats for {{ video.name }}</h1>
+ <h1 class="title-page title-page-single" i18n>{{ video.name }}</h1>
- <div class="overall-stats-embed">
- <div class="overall-stats">
- <div *ngFor="let card of overallStatCards" class="card overall-stats-card">
+ <div class="stats-embed">
+ <div class="global-stats">
+ <div *ngFor="let card of globalStatsCards" class="card stats-card">
<div class="label">{{ card.label }}</div>
<div class="value">{{ card.value }}</div>
<div *ngIf="card.moreInfo" class="more-info">{{ card.moreInfo }}</div>
<my-embed [video]="video"></my-embed>
</div>
- <div class="timeserie">
- <div ngbNav #nav="ngbNav" [activeId]="activeGraphId" (activeIdChange)="onChartChange($event)" class="nav-tabs">
-
- <ng-container *ngFor="let availableChart of availableCharts" [ngbNavItem]="availableChart.id">
- <a ngbNavLink i18n>
- <span>{{ availableChart.label }}</span>
- </a>
-
- <ng-template ngbNavContent>
- <div class="chart-container" [ngStyle]="{ 'min-height': chartHeight }">
- <p-chart
- *ngIf="chartOptions[availableChart.id]"
- [height]="chartHeight" [width]="chartWidth"
- [type]="chartOptions[availableChart.id].type" [options]="chartOptions[availableChart.id].options" [data]="chartOptions[availableChart.id].data"
- [plugins]="chartPlugins"
- ></p-chart>
- </div>
-
- <div class="zoom-container">
- <span *ngIf="!hasZoom() && availableChart.zoomEnabled" i18n class="description">You can select a part of the graph to zoom in</span>
-
- <my-button i18n *ngIf="hasZoom()" (click)="resetZoom()">Reset zoom</my-button>
- </div>
- </ng-template>
- </ng-container>
+ <div class="stats-with-date">
+ <div class="overall-stats">
+ <div class="date-filter-wrapper">
+ <h2>{{ getViewersStatsTitle() }}</h2>
+
+ <my-select-options [(ngModel)]="currentDateFilter" (ngModelChange)="onDateFilterChange()" [items]="dateFilters"></my-select-options>
+ </div>
+
+ <div class="cards">
+ <div *ngFor="let card of overallStatCards" class="card stats-card">
+ <div class="label">{{ card.label }}</div>
+ <div class="value">{{ card.value }}</div>
+ <div *ngIf="card.moreInfo" class="more-info">{{ card.moreInfo }}</div>
+ </div>
+ </div>
</div>
- <div [ngbNavOutlet]="nav"></div>
+ <div class="timeserie">
+ <div ngbNav #nav="ngbNav" [activeId]="activeGraphId" (activeIdChange)="onChartChange($event)" class="nav-tabs">
+
+ <ng-container *ngFor="let availableChart of availableCharts" [ngbNavItem]="availableChart.id">
+ <a ngbNavLink i18n>
+ <span>{{ availableChart.label }}</span>
+ </a>
+
+ <ng-template ngbNavContent>
+ <div class="chart-container" [ngStyle]="{ 'min-height': chartHeight }">
+ <p-chart
+ *ngIf="chartOptions[availableChart.id]"
+ [height]="chartHeight" [width]="chartWidth"
+ [type]="chartOptions[availableChart.id].type" [options]="chartOptions[availableChart.id].options" [data]="chartOptions[availableChart.id].data"
+ [plugins]="chartPlugins"
+ ></p-chart>
+ </div>
+
+ <div class="zoom-container">
+ <span *ngIf="!hasZoom() && availableChart.zoomEnabled" i18n class="description">You can select a part of the graph to zoom in</span>
+
+ <my-button i18n *ngIf="hasZoom()" (click)="resetZoom()">Reset zoom</my-button>
+ </div>
+ </ng-template>
+ </ng-container>
+ </div>
+
+ <div [ngbNavOutlet]="nav"></div>
+ </div>
</div>
</div>
@use '_mixins' as *;
@use '_nav' as *;
-.overall-stats-embed {
+.stats-embed {
display: flex;
justify-content: space-between;
}
-.overall-stats {
+.overall-stats,
+.global-stats {
display: flex;
flex-wrap: wrap;
+
+ h2 {
+ font-size: 16px;
+ width: 100%;
+ }
+}
+
+.overall-stats {
+ justify-content: space-between;
+
+ .cards {
+ display: flex;
+ }
}
-.overall-stats-card {
+.stats-card {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
}
- .label {
- color: pvar(--greyForegroundColor);
- font-weight: $font-semibold;
- opacity: 0.8;
- }
-
.value {
font-size: 24px;
font-weight: $font-semibold;
width: 100%;
}
+.stats-with-date {
+ margin-top: 30px;
+ padding-top: 30px;
+ border-top: 1px solid $separator-border-color;
+}
+
@include on-small-main-col {
my-embed {
display: none;
}
.tab-content {
- margin-top: 15px;
+ margin-top: 5px;
}
.nav-tabs {
import { ChartConfiguration, ChartData, PluginOptionsByType, Scale, TooltipItem } from 'chart.js'
import zoomPlugin from 'chartjs-plugin-zoom'
import { Observable, of } from 'rxjs'
+import { SelectOptionsItem } from 'src/types'
import { Component, OnInit } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { Notifier, PeerTubeRouterService } from '@app/core'
import { NumberFormatterPipe, VideoDetails } from '@app/shared/shared-main'
+import { LiveVideoService } from '@app/shared/shared-video-live'
import { secondsToTime } from '@shared/core-utils'
-import { VideoStatsOverall, VideoStatsRetention, VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@shared/models/videos'
+import { HttpStatusCode } from '@shared/models/http'
+import {
+ LiveVideoSession,
+ VideoStatsOverall,
+ VideoStatsRetention,
+ VideoStatsTimeserie,
+ VideoStatsTimeserieMetric
+} from '@shared/models/videos'
import { VideoStatsService } from './video-stats.service'
type ActiveGraphId = VideoStatsTimeserieMetric | 'retention' | 'countries'
displayLegend: boolean
}
+type Card = { label: string, value: string | number, moreInfo?: string }
+
@Component({
templateUrl: './video-stats.component.html',
styleUrls: [ './video-stats.component.scss' ],
providers: [ NumberFormatterPipe ]
})
export class VideoStatsComponent implements OnInit {
- overallStatCards: { label: string, value: string | number, moreInfo?: string }[] = []
+ // Cannot handle date filters
+ globalStatsCards: Card[] = []
+ // Can handle date filters
+ overallStatCards: Card[] = []
chartOptions: { [ id in ActiveGraphId ]?: ChartConfiguration<'line' | 'bar'> } = {}
chartHeight = '300px'
chartWidth: string = null
- availableCharts = [
- {
- id: 'viewers',
- label: $localize`Viewers`,
- zoomEnabled: true
- },
- {
- id: 'aggregateWatchTime',
- label: $localize`Watch time`,
- zoomEnabled: true
- },
- {
- id: 'retention',
- label: $localize`Retention`,
- zoomEnabled: false
- },
- {
- id: 'countries',
- label: $localize`Countries`,
- zoomEnabled: false
- }
- ]
-
+ availableCharts: { id: string, label: string, zoomEnabled: boolean }[] = []
activeGraphId: ActiveGraphId = 'viewers'
video: VideoDetails
chartPlugins = [ zoomPlugin ]
- private timeseriesStartDate: Date
- private timeseriesEndDate: Date
+ currentDateFilter = 'all'
+ dateFilters: SelectOptionsItem[] = [
+ {
+ id: 'all',
+ label: $localize`Since the video publication`
+ }
+ ]
+
+ private statsStartDate: Date
+ private statsEndDate: Date
private chartIngestData: { [ id in ActiveGraphId ]?: ChartIngestData } = {}
private notifier: Notifier,
private statsService: VideoStatsService,
private peertubeRouter: PeerTubeRouterService,
- private numberFormatter: NumberFormatterPipe
+ private numberFormatter: NumberFormatterPipe,
+ private liveService: LiveVideoService
) {}
ngOnInit () {
this.video = this.route.snapshot.data.video
+ this.availableCharts = [
+ {
+ id: 'viewers',
+ label: $localize`Viewers`,
+ zoomEnabled: true
+ },
+ {
+ id: 'aggregateWatchTime',
+ label: $localize`Watch time`,
+ zoomEnabled: true
+ },
+ {
+ id: 'countries',
+ label: $localize`Countries`,
+ zoomEnabled: false
+ }
+ ]
+
+ if (!this.video.isLive) {
+ this.availableCharts.push({
+ id: 'retention',
+ label: $localize`Retention`,
+ zoomEnabled: false
+ })
+ }
+
+ const snapshotQuery = this.route.snapshot.queryParams
+ if (snapshotQuery.startDate || snapshotQuery.endDate) {
+ this.addAndSelectCustomDateFilter()
+ }
+
this.route.queryParams.subscribe(params => {
- this.timeseriesStartDate = params.startDate
+ this.statsStartDate = params.startDate
? new Date(params.startDate)
: undefined
- this.timeseriesEndDate = params.endDate
+ this.statsEndDate = params.endDate
? new Date(params.endDate)
: undefined
this.loadChart()
+ this.loadOverallStats()
})
- this.loadOverallStats()
+ this.loadDateFilters()
}
hasCountries () {
resetZoom () {
this.peertubeRouter.silentNavigate([], {})
+ this.removeAndResetCustomDateFilter()
}
hasZoom () {
- return !!this.timeseriesStartDate && this.isTimeserieGraph(this.activeGraphId)
+ return !!this.statsStartDate && this.isTimeserieGraph(this.activeGraphId)
+ }
+
+ getViewersStatsTitle () {
+ if (this.statsStartDate && this.statsEndDate) {
+ return $localize`Viewers stats between ${this.statsStartDate.toLocaleString()} and ${this.statsEndDate.toLocaleString()}`
+ }
+
+ return $localize`Viewers stats`
+ }
+
+ onDateFilterChange () {
+ if (this.currentDateFilter === 'all') {
+ return this.resetZoom()
+ }
+
+ const idParts = this.currentDateFilter.split('|')
+ if (idParts.length === 2) {
+ return this.peertubeRouter.silentNavigate([], { startDate: idParts[0], endDate: idParts[1] })
+ }
}
private isTimeserieGraph (graphId: ActiveGraphId) {
}
private loadOverallStats () {
- this.statsService.getOverallStats(this.video.uuid)
+ this.statsService.getOverallStats({ videoId: this.video.uuid, startDate: this.statsStartDate, endDate: this.statsEndDate })
.subscribe({
next: res => {
this.countries = res.countries.slice(0, 10).map(c => ({
})
}
+ private loadDateFilters () {
+ if (this.video.isLive) return this.loadLiveDateFilters()
+
+ return this.loadVODDateFilters()
+ }
+
+ private loadLiveDateFilters () {
+ this.liveService.listSessions(this.video.id)
+ .subscribe({
+ next: ({ data }) => {
+ const newFilters = data.map(session => this.buildLiveFilter(session))
+
+ this.dateFilters = this.dateFilters.concat(newFilters)
+ },
+
+ error: err => this.notifier.error(err.message)
+ })
+ }
+
+ private loadVODDateFilters () {
+ this.liveService.findLiveSessionFromVOD(this.video.id)
+ .subscribe({
+ next: session => {
+ this.dateFilters = this.dateFilters.concat([ this.buildLiveFilter(session) ])
+ },
+
+ error: err => {
+ if (err.status === HttpStatusCode.NOT_FOUND_404) return
+
+ this.notifier.error(err.message)
+ }
+ })
+ }
+
+ private buildLiveFilter (session: LiveVideoSession) {
+ return {
+ id: session.startDate + '|' + session.endDate,
+ label: $localize`Of live of ${new Date(session.startDate).toLocaleString()}`
+ }
+ }
+
+ private addAndSelectCustomDateFilter () {
+ const exists = this.dateFilters.some(d => d.id === 'custom')
+
+ if (!exists) {
+ this.dateFilters = this.dateFilters.concat([
+ {
+ id: 'custom',
+ label: $localize`Custom dates`
+ }
+ ])
+ }
+
+ this.currentDateFilter = 'custom'
+ }
+
+ private removeAndResetCustomDateFilter () {
+ this.dateFilters = this.dateFilters.filter(d => d.id !== 'custom')
+
+ this.currentDateFilter = 'all'
+ }
+
private buildOverallStatCard (overallStats: VideoStatsOverall) {
- this.overallStatCards = [
+ this.globalStatsCards = [
{
label: $localize`Views`,
value: this.numberFormatter.transform(this.video.views)
{
label: $localize`Likes`,
value: this.numberFormatter.transform(this.video.likes)
- },
+ }
+ ]
+
+ this.overallStatCards = [
{
label: $localize`Average watch time`,
value: secondsToTime(overallStats.averageWatchTime)
},
+ {
+ label: $localize`Total watch time`,
+ value: secondsToTime(overallStats.totalWatchTime)
+ },
{
label: $localize`Peak viewers`,
value: this.numberFormatter.transform(overallStats.viewersPeak),
: undefined
}
]
+
+ if (overallStats.countries.length !== 0) {
+ this.overallStatCards.push({
+ label: $localize`Countries`,
+ value: this.numberFormatter.transform(overallStats.countries.length)
+ })
+ }
}
private loadChart () {
aggregateWatchTime: this.statsService.getTimeserieStats({
videoId: this.video.uuid,
- startDate: this.timeseriesStartDate,
- endDate: this.timeseriesEndDate,
+ startDate: this.statsStartDate,
+ endDate: this.statsEndDate,
metric: 'aggregateWatchTime'
}),
viewers: this.statsService.getTimeserieStats({
videoId: this.video.uuid,
- startDate: this.timeseriesStartDate,
- endDate: this.timeseriesEndDate,
+ startDate: this.statsStartDate,
+ endDate: this.statsEndDate,
metric: 'viewers'
}),
const endDate = this.buildZoomEndDate(rawData.groupInterval, rawData.data[max].date)
this.peertubeRouter.silentNavigate([], { startDate, endDate })
+ this.addAndSelectCustomDateFilter()
}
}
}
const date = new Date(label)
+ if (data.groupInterval.match(/ month?$/)) {
+ return date.toLocaleDateString([], { month: 'numeric' })
+ }
+
if (data.groupInterval.match(/ days?$/)) {
return date.toLocaleDateString([], { month: 'numeric', day: 'numeric' })
}
private restExtractor: RestExtractor
) { }
- getOverallStats (videoId: string) {
- return this.authHttp.get<VideoStatsOverall>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/stats/overall')
+ getOverallStats (options: {
+ videoId: string
+ startDate?: Date
+ endDate?: Date
+ }) {
+ const { videoId, startDate, endDate } = options
+
+ let params = new HttpParams()
+ if (startDate) params = params.append('startDate', startDate.toISOString())
+ if (endDate) params = params.append('endDate', endDate.toISOString())
+
+ return this.authHttp.get<VideoStatsOverall>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/stats/overall', { params })
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
+import { LoginGuard } from '@app/core'
import { VideoResolver } from '@app/shared/shared-main'
import { VideoStudioEditComponent } from './edit'
const videoStudioRoutes: Routes = [
{
path: '',
+ canActivateChild: [ LoginGuard ],
children: [
{
path: 'edit/:videoId',
import { RestExtractor } from '@app/core'
import { LiveVideo, LiveVideoCreate, LiveVideoSession, LiveVideoUpdate, ResultList, VideoCreateResult } from '@shared/models'
import { environment } from '../../../environments/environment'
+import { VideoService } from '../shared-main'
@Injectable()
export class LiveVideoService {
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
+ findLiveSessionFromVOD (videoId: number | string) {
+ return this.authHttp
+ .get<LiveVideoSession>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/live-session')
+ .pipe(catchError(err => this.restExtractor.handleError(err)))
+ }
+
updateLive (videoId: number | string, liveUpdate: LiveVideoUpdate) {
return this.authHttp
.put(LiveVideoService.BASE_VIDEO_LIVE_URL + videoId, liveUpdate)
const stats = await LocalVideoViewerModel.getTimeserieStats({
video,
metric,
- startDate: query.startDate ?? buildOneMonthAgo().toISOString(),
+ startDate: query.startDate ?? video.createdAt.toISOString(),
endDate: query.endDate ?? new Date().toISOString()
})
return res.json(stats)
}
-
-function buildOneMonthAgo () {
- const monthAgo = new Date()
- monthAgo.setHours(0, 0, 0, 0)
-
- monthAgo.setDate(monthAgo.getDate() - 29)
-
- return monthAgo
-}
// ---------------------------------------------------------------------------
const STATS_TIMESERIE = {
- MAX_DAYS: 30
+ MAX_DAYS: 365 * 10 // Around 10 years
}
// ---------------------------------------------------------------------------
logger.debug('Found "%s" group interval.', groupInterval, { startDate, endDate })
// Remove parts of the date we don't need
- if (groupInterval.endsWith(' day') || groupInterval.endsWith(' days')) {
+ if (groupInterval.endsWith(' month') || groupInterval.endsWith(' months')) {
+ startDate.setDate(1)
+ startDate.setHours(0, 0, 0, 0)
+ } else if (groupInterval.endsWith(' day') || groupInterval.endsWith(' days')) {
startDate.setHours(0, 0, 0, 0)
} else if (groupInterval.endsWith(' hour') || groupInterval.endsWith(' hours')) {
startDate.setMinutes(0, 0, 0)
// ---------------------------------------------------------------------------
function buildGroupInterval (startDate: Date, endDate: Date): string {
+ const aYear = 31536000
+ const aMonth = 2678400
const aDay = 86400
const anHour = 3600
const aMinute = 60
const diffSeconds = (endDate.getTime() - startDate.getTime()) / 1000
+ if (diffSeconds >= 6 * aYear) return '6 months'
+ if (diffSeconds >= 2 * aYear) return '1 month'
+ if (diffSeconds >= 6 * aMonth) return '7 days'
+ if (diffSeconds >= 2 * aMonth) return '2 days'
+
if (diffSeconds >= 15 * aDay) return '1 day'
if (diffSeconds >= 8 * aDay) return '12 hours'
if (diffSeconds >= 4 * aDay) return '6 hours'
+
if (diffSeconds >= 15 * anHour) return '1 hour'
+
if (diffSeconds >= 180 * aMinute) return '10 minutes'
return '1 minute'
const watchPeakQuery = `WITH "watchPeakValues" AS (
SELECT "startDate" AS "dateBreakpoint", 1 AS "inc"
FROM "localVideoViewer"
- WHERE "videoId" = :videoId
+ WHERE "videoId" = :videoId ${dateWhere}
UNION ALL
SELECT "endDate" AS "dateBreakpoint", -1 AS "inc"
FROM "localVideoViewer"
countriesPromise
])
+ const viewersPeak = rowsWatchPeak.length !== 0
+ ? parseInt(rowsWatchPeak[0].concurrent) || 0
+ : 0
+
return {
totalWatchTime: rowsWatchTime.length !== 0
? Math.round(rowsWatchTime[0].totalWatchTime) || 0
? Math.round(rowsWatchTime[0].averageWatchTime) || 0
: 0,
- viewersPeak: rowsWatchPeak.length !== 0
- ? parseInt(rowsWatchPeak[0].concurrent) || 0
- : 0,
- viewersPeakDate: rowsWatchPeak.length !== 0
+ viewersPeak,
+ viewersPeakDate: rowsWatchPeak.length !== 0 && viewersPeak !== 0
? rowsWatchPeak[0].dateBreakpoint || null
: null,
await servers[0].videoStats.getTimeserieStats({
videoId,
metric: 'viewers',
- startDate: new Date('2021-04-07T08:31:57.126Z'),
+ startDate: new Date('2000-04-07T08:31:57.126Z'),
endDate: new Date(),
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
describe('Test watchers peak stats of local videos on VOD', function () {
let videoUUID: string
+ let before2Watchers: Date
before(async function () {
this.timeout(120000);
it('Should have watcher peak with 2 watchers', async function () {
this.timeout(60000)
- const before = new Date()
+ before2Watchers = new Date()
await servers[0].views.view({ id: videoUUID, currentTime: 0 })
await servers[1].views.view({ id: videoUUID, currentTime: 0 })
await servers[0].views.view({ id: videoUUID, currentTime: 2 })
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
expect(stats.viewersPeak).to.equal(2)
- expect(new Date(stats.viewersPeakDate)).to.be.above(before).and.below(after)
+ expect(new Date(stats.viewersPeakDate)).to.be.above(before2Watchers).and.below(after)
+ })
+
+ it('Should filter peak viewers stats by date', async function () {
+ {
+ const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
+ expect(stats.viewersPeak).to.equal(0)
+ expect(stats.viewersPeakDate).to.not.exist
+ }
+
+ {
+ const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate: before2Watchers.toISOString() })
+ expect(stats.viewersPeak).to.equal(1)
+ expect(new Date(stats.viewersPeakDate)).to.be.below(before2Watchers)
+ }
})
})
describe('Test countries', function () {
+ let videoUUID: string
it('Should not report countries if geoip is disabled', async function () {
this.timeout(120000)
this.timeout(240000)
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
+ videoUUID = uuid
await waitJobs(servers)
await Promise.all([
expect(stats.countries[1].isoCode).to.equal('FR')
expect(stats.countries[1].viewers).to.equal(1)
})
+
+ it('Should filter countries stats by date', async function () {
+ const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
+ expect(stats.countries).to.have.lengthOf(0)
+ })
})
after(async function () {
const expect = chai.expect
+function buildOneMonthAgo () {
+ const monthAgo = new Date()
+ monthAgo.setHours(0, 0, 0, 0)
+
+ monthAgo.setDate(monthAgo.getDate() - 29)
+
+ return monthAgo
+}
+
describe('Test views timeserie stats', function () {
const availableMetrics: VideoStatsTimeserieMetric[] = [ 'viewers' ]
for (const metric of availableMetrics) {
const { data } = await servers[0].videoStats.getTimeserieStats({ videoId: vodVideoId, metric })
- expect(data).to.have.lengthOf(30)
+ expect(data).to.have.length.at.least(1)
for (const d of data) {
expect(d.value).to.equal(0)
let liveVideoId: string
let command: FfmpegCommand
- function expectTodayLastValue (result: VideoStatsTimeserie, lastValue: number) {
+ function expectTodayLastValue (result: VideoStatsTimeserie, lastValue?: number) {
const { data } = result
const last = data[data.length - 1]
const today = new Date().getDate()
expect(new Date(last.date).getDate()).to.equal(today)
+
+ if (lastValue) expect(last.value).to.equal(lastValue)
}
function expectTimeserieData (result: VideoStatsTimeserie, lastValue: number) {
const { data } = result
- expect(data).to.have.lengthOf(30)
+ expect(data).to.have.length.at.least(25)
expectTodayLastValue(result, lastValue)
await processViewersStats(servers)
for (const videoId of [ vodVideoId, liveVideoId ]) {
- const result = await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'viewers' })
+ const result = await servers[0].videoStats.getTimeserieStats({
+ videoId,
+ startDate: buildOneMonthAgo(),
+ endDate: new Date(),
+ metric: 'viewers'
+ })
expectTimeserieData(result, 2)
}
})
it('Should display appropriate watch time metrics', async function () {
for (const videoId of [ vodVideoId, liveVideoId ]) {
- const result = await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'aggregateWatchTime' })
+ const result = await servers[0].videoStats.getTimeserieStats({
+ videoId,
+ startDate: buildOneMonthAgo(),
+ endDate: new Date(),
+ metric: 'aggregateWatchTime'
+ })
expectTimeserieData(result, 8)
await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
await processViewersStats(servers)
for (const videoId of [ vodVideoId, liveVideoId ]) {
- const result = await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'aggregateWatchTime' })
+ const result = await servers[0].videoStats.getTimeserieStats({
+ videoId,
+ startDate: buildOneMonthAgo(),
+ endDate: new Date(),
+ metric: 'aggregateWatchTime'
+ })
expectTimeserieData(result, 9)
}
})
expectTodayLastValue(result, 9)
})
+ it('Should automatically group by months', async function () {
+ const now = new Date()
+ const heightYearsAgo = new Date()
+ heightYearsAgo.setFullYear(heightYearsAgo.getFullYear() - 7)
+
+ const result = await servers[0].videoStats.getTimeserieStats({
+ videoId: vodVideoId,
+ metric: 'aggregateWatchTime',
+ startDate: heightYearsAgo,
+ endDate: now
+ })
+
+ expect(result.groupInterval).to.equal('6 months')
+ expect(result.data).to.have.length.above(10).and.below(200)
+ })
+
+ it('Should automatically group by days', async function () {
+ const now = new Date()
+ const threeMonthsAgo = new Date()
+ threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
+
+ const result = await servers[0].videoStats.getTimeserieStats({
+ videoId: vodVideoId,
+ metric: 'aggregateWatchTime',
+ startDate: threeMonthsAgo,
+ endDate: now
+ })
+
+ expect(result.groupInterval).to.equal('2 days')
+ expect(result.data).to.have.length.above(10).and.below(200)
+ })
+
it('Should automatically group by hours', async function () {
const now = new Date()
const twoDaysAgo = new Date()
expect(result.data).to.have.length.above(20).and.below(30)
expectInterval(result, 60 * 10 * 1000)
- expectTodayLastValue(result, 9)
+ expectTodayLastValue(result)
})
it('Should automatically group by one minute', async function () {
expect(result.data).to.have.length.above(20).and.below(40)
expectInterval(result, 60 * 1000)
- expectTodayLastValue(result, 9)
+ expectTodayLastValue(result)
})
after(async function () {