PeerTube/packages/tests/src/api/views/video-views-timeserie-stats.ts
Chocobozzz 5cb3e6a0b8
Use sessionId instead of IP to identify viewer
Breaking: YAML config `ip_view_expiration` is renamed `view_expiration`
Breaking: Views are taken into account after 10 seconds instead of 30
seconds (can be changed in YAML config)

Purpose of this commit is to get closer to other video platforms where
some platforms count views on play (mux, vimeo) or others use a very low
delay (instagram, tiktok)

We also want to improve the viewer identification, where we no longer
use the IP but the `sessionId` generated by the web browser. Multiple
viewers behind a NAT can now be able to be identified as independent
viewers (this method is also used by vimeo or mux)
2024-04-04 16:27:40 +02:00

272 lines
8.6 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@peertube/peertube-models'
import { buildUUID } from '@peertube/peertube-node-utils'
import { PeerTubeServer, cleanupTests, stopFfmpeg } from '@peertube/peertube-server-commands'
import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
import { expect } from 'chai'
import { FfmpegCommand } from 'fluent-ffmpeg'
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' ]
let servers: PeerTubeServer[]
before(async function () {
this.timeout(120000)
servers = await prepareViewsServers()
})
describe('Common metric tests', function () {
let vodVideoId: string
before(async function () {
this.timeout(240000);
({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true }))
})
it('Should display empty metric stats', async function () {
for (const metric of availableMetrics) {
const { data } = await servers[0].videoStats.getTimeserieStats({ videoId: vodVideoId, metric })
expect(data).to.have.length.at.least(1)
for (const d of data) {
expect(d.value).to.equal(0)
}
}
})
})
describe('Test viewer and watch time metrics on live and VOD', function () {
let vodVideoId: string
let liveVideoId: string
let command: FfmpegCommand
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.length.at.least(25)
expectTodayLastValue(result, lastValue)
for (let i = 0; i < data.length - 2; i++) {
expect(data[i].value).to.equal(0)
}
}
function expectInterval (result: VideoStatsTimeserie, intervalMs: number) {
const first = result.data[0]
const second = result.data[1]
expect(new Date(second.date).getTime() - new Date(first.date).getTime()).to.equal(intervalMs)
}
function runTests (options: { useSessionId: boolean }) {
const generateSession = () => {
if (!options.useSessionId) return undefined
return buildUUID()
}
before(async function () {
this.timeout(240000);
({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
})
it('Should display appropriate viewers metrics', async function () {
for (const videoId of [ vodVideoId, liveVideoId ]) {
await servers[0].views.simulateViewer({ id: videoId, sessionId: generateSession(), currentTimes: [ 0, 3 ] })
await servers[1].views.simulateViewer({ id: videoId, sessionId: generateSession(), currentTimes: [ 0, 5 ] })
}
await processViewersStats(servers)
for (const videoId of [ vodVideoId, liveVideoId ]) {
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,
startDate: buildOneMonthAgo(),
endDate: new Date(),
metric: 'aggregateWatchTime'
})
expectTimeserieData(result, 8)
await servers[1].views.simulateViewer({ id: videoId, sessionId: generateSession(), currentTimes: [ 0, 1 ] })
}
await processViewersStats(servers)
for (const videoId of [ vodVideoId, liveVideoId ]) {
const result = await servers[0].videoStats.getTimeserieStats({
videoId,
startDate: buildOneMonthAgo(),
endDate: new Date(),
metric: 'aggregateWatchTime'
})
expectTimeserieData(result, 9)
}
})
it('Should use a custom start/end date', async function () {
const now = new Date()
const twentyDaysAgo = new Date()
twentyDaysAgo.setDate(twentyDaysAgo.getDate() - 19)
const result = await servers[0].videoStats.getTimeserieStats({
videoId: vodVideoId,
metric: 'aggregateWatchTime',
startDate: twentyDaysAgo,
endDate: now
})
expect(result.groupInterval).to.equal('1 day')
expect(result.data).to.have.lengthOf(20)
const first = result.data[0]
expect(new Date(first.date).toLocaleDateString()).to.equal(twentyDaysAgo.toLocaleDateString())
expectInterval(result, 24 * 3600 * 1000)
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()
twoDaysAgo.setDate(twoDaysAgo.getDate() - 1)
const result = await servers[0].videoStats.getTimeserieStats({
videoId: vodVideoId,
metric: 'aggregateWatchTime',
startDate: twoDaysAgo,
endDate: now
})
expect(result.groupInterval).to.equal('1 hour')
expect(result.data).to.have.length.above(24).and.below(50)
expectInterval(result, 3600 * 1000)
expectTodayLastValue(result, 9)
})
it('Should automatically group by ten minutes', async function () {
const now = new Date()
const twoHoursAgo = new Date()
twoHoursAgo.setHours(twoHoursAgo.getHours() - 4)
const result = await servers[0].videoStats.getTimeserieStats({
videoId: vodVideoId,
metric: 'aggregateWatchTime',
startDate: twoHoursAgo,
endDate: now
})
expect(result.groupInterval).to.equal('10 minutes')
expect(result.data).to.have.length.above(20).and.below(30)
expectInterval(result, 60 * 10 * 1000)
expectTodayLastValue(result)
})
it('Should automatically group by one minute', async function () {
const now = new Date()
const thirtyAgo = new Date()
thirtyAgo.setMinutes(thirtyAgo.getMinutes() - 30)
const result = await servers[0].videoStats.getTimeserieStats({
videoId: vodVideoId,
metric: 'aggregateWatchTime',
startDate: thirtyAgo,
endDate: now
})
expect(result.groupInterval).to.equal('1 minute')
expect(result.data).to.have.length.above(20).and.below(40)
expectInterval(result, 60 * 1000)
expectTodayLastValue(result)
})
after(async function () {
await stopFfmpeg(command)
})
}
describe('Not using session id', function () {
runTests({ useSessionId: false })
})
describe('Using session id', function () {
runTests({ useSessionId: true })
})
})
after(async function () {
await cleanupTests(servers)
})
})