PeerTube/packages/tests/src/api/views/video-views-overall-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

456 lines
17 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { wait } from '@peertube/peertube-core-utils'
import { VideoStatsOverall } from '@peertube/peertube-models'
import { buildUUID } from '@peertube/peertube-node-utils'
import { PeerTubeServer, cleanupTests, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
import { expect } from 'chai'
import { FfmpegCommand } from 'fluent-ffmpeg'
/**
*
* Simulate 5 sections of viewers
* * user0 started and ended before start date
* * user1 started before start date and ended in the interval
* * user2 started started in the interval and ended after end date
* * user3 started and ended in the interval
* * user4 started and ended after end date
*/
async function simulateComplexViewers (servers: PeerTubeServer[], videoUUID: string, useSessionId: boolean) {
const user0 = '8.8.8.8,127.0.0.1'
const user1 = '8.8.8.8,127.0.0.1'
const user2 = '8.8.8.9,127.0.0.1'
const user3 = '8.8.8.10,127.0.0.1'
const user4 = '8.8.8.11,127.0.0.1'
const sessionIdField = useSessionId
? 'sessionId'
: 'xForwardedFor'
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user0 }) // User 0 starts
await wait(500)
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user1 }) // User 1 starts
await servers[0].views.view({ id: videoUUID, currentTime: 2, [sessionIdField]: user0 }) // User 0 ends
await wait(500)
const startDate = new Date().toISOString()
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user2 }) // User 2 starts
await wait(500)
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user3 }) // User 3 starts
await wait(500)
await servers[0].views.view({ id: videoUUID, currentTime: 4, [sessionIdField]: user1 }) // User 1 ends
await wait(500)
await servers[0].views.view({ id: videoUUID, currentTime: 3, [sessionIdField]: user3 }) // User 3 ends
await wait(500)
const endDate = new Date().toISOString()
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user4 }) // User 4 starts
await servers[0].views.view({ id: videoUUID, currentTime: 5, [sessionIdField]: user2 }) // User 2 ends
await wait(500)
await servers[0].views.view({ id: videoUUID, currentTime: 1, [sessionIdField]: user4 }) // User 4 ends
await processViewersStats(servers)
return { startDate, endDate }
}
describe('Test views overall stats', function () {
let servers: PeerTubeServer[]
function runTests (options: { useSessionId: boolean }) {
const { useSessionId } = options
const generateSessionId = () => {
if (!options.useSessionId) return undefined
return buildUUID()
}
before(async function () {
this.timeout(120000)
servers = await prepareViewsServers()
})
describe('Test watch time stats of local videos on live and VOD', function () {
let vodVideoId: string
let liveVideoId: string
let command: FfmpegCommand
before(async function () {
this.timeout(240000);
({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
})
it('Should display overall stats of a video with no viewers', async function () {
for (const videoId of [ liveVideoId, vodVideoId ]) {
const stats = await servers[0].videoStats.getOverallStats({ videoId })
const video = await servers[0].videos.get({ id: videoId })
expect(video.views).to.equal(0)
expect(stats.averageWatchTime).to.equal(0)
expect(stats.totalWatchTime).to.equal(0)
expect(stats.totalViewers).to.equal(0)
}
})
it('Should display overall stats with 1 viewer below the watch time limit', async function () {
this.timeout(60000)
for (const videoId of [ liveVideoId, vodVideoId ]) {
await servers[0].views.simulateViewer({ id: videoId, sessionId: generateSessionId(), currentTimes: [ 0, 1 ] })
}
await processViewersStats(servers)
for (const videoId of [ liveVideoId, vodVideoId ]) {
const stats = await servers[0].videoStats.getOverallStats({ videoId })
const video = await servers[0].videos.get({ id: videoId })
expect(video.views).to.equal(0)
expect(stats.averageWatchTime).to.equal(1)
expect(stats.totalWatchTime).to.equal(1)
expect(stats.totalViewers).to.equal(1)
}
})
it('Should display overall stats with 2 viewers', async function () {
this.timeout(60000)
{
await servers[0].views.simulateViewer({ id: vodVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 3 ] })
await servers[0].views.simulateViewer({ id: liveVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 35, 40 ] })
await processViewersStats(servers)
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
const video = await servers[0].videos.get({ id: vodVideoId })
expect(video.views).to.equal(1)
expect(stats.averageWatchTime).to.equal(2)
expect(stats.totalWatchTime).to.equal(4)
expect(stats.totalViewers).to.equal(2)
}
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
const video = await servers[0].videos.get({ id: liveVideoId })
expect(video.views).to.equal(1)
expect(stats.averageWatchTime).to.equal(21)
expect(stats.totalWatchTime).to.equal(41)
expect(stats.totalViewers).to.equal(2)
}
}
})
it('Should display overall stats with a remote viewer below the watch time limit', async function () {
this.timeout(60000)
for (const videoId of [ liveVideoId, vodVideoId ]) {
await servers[1].views.simulateViewer({ id: videoId, sessionId: generateSessionId(), currentTimes: [ 0, 2 ] })
}
await processViewersStats(servers)
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
const video = await servers[0].videos.get({ id: vodVideoId })
expect(video.views).to.equal(1)
expect(stats.averageWatchTime).to.equal(2)
expect(stats.totalWatchTime).to.equal(6)
expect(stats.totalViewers).to.equal(3)
}
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
const video = await servers[0].videos.get({ id: liveVideoId })
expect(video.views).to.equal(1)
expect(stats.averageWatchTime).to.equal(14)
expect(stats.totalWatchTime).to.equal(43)
expect(stats.totalViewers).to.equal(3)
}
})
it('Should display overall stats with a remote viewer above the watch time limit', async function () {
this.timeout(60000)
await servers[1].views.simulateViewer({ id: vodVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 5 ] })
await servers[1].views.simulateViewer({ id: liveVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 45 ] })
await processViewersStats(servers)
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
const video = await servers[0].videos.get({ id: vodVideoId })
expect(video.views).to.equal(2)
expect(stats.averageWatchTime).to.equal(3)
expect(stats.totalWatchTime).to.equal(11)
expect(stats.totalViewers).to.equal(4)
}
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
const video = await servers[0].videos.get({ id: liveVideoId })
expect(video.views).to.equal(2)
expect(stats.averageWatchTime).to.equal(22)
expect(stats.totalWatchTime).to.equal(88)
expect(stats.totalViewers).to.equal(4)
}
})
it('Should filter overall stats by date', async function () {
this.timeout(60000)
const beforeView = new Date()
await servers[0].views.simulateViewer({ id: vodVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 3 ] })
await processViewersStats(servers)
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId, startDate: beforeView.toISOString() })
expect(stats.averageWatchTime).to.equal(3)
expect(stats.totalWatchTime).to.equal(3)
expect(stats.totalViewers).to.equal(1)
}
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId, endDate: beforeView.toISOString() })
expect(stats.averageWatchTime).to.equal(22)
expect(stats.totalWatchTime).to.equal(88)
expect(stats.totalViewers).to.equal(4)
}
})
after(async function () {
await stopFfmpeg(command)
})
})
describe('Test watchers peak stats of local videos on VOD', function () {
let videoUUID: string
let before2Watchers: Date
before(async function () {
this.timeout(240000);
({ vodVideoId: videoUUID } = await prepareViewsVideos({ servers, live: true, vod: true }))
})
it('Should not have watchers peak', async function () {
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
expect(stats.viewersPeak).to.equal(0)
expect(stats.viewersPeakDate).to.be.null
})
it('Should have watcher peak with 1 watcher', async function () {
this.timeout(60000)
const before = new Date()
await servers[0].views.simulateViewer({ id: videoUUID, sessionId: generateSessionId(), currentTimes: [ 0, 2 ] })
const after = new Date()
await processViewersStats(servers)
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
expect(stats.viewersPeak).to.equal(1)
expect(new Date(stats.viewersPeakDate)).to.be.above(before).and.below(after)
})
it('Should have watcher peak with 2 watchers', async function () {
this.timeout(60000)
const sessionId = generateSessionId()
before2Watchers = new Date()
await servers[0].views.view({ id: videoUUID, sessionId, currentTime: 0 })
await servers[1].views.view({ id: videoUUID, sessionId, currentTime: 0 })
await servers[0].views.view({ id: videoUUID, sessionId, currentTime: 2 })
await servers[1].views.view({ id: videoUUID, sessionId, currentTime: 2 })
const after = new Date()
await processViewersStats(servers)
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
expect(stats.viewersPeak).to.equal(2)
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)
}
})
it('Should complex filter peak viewers by date', async function () {
this.timeout(60000)
const { startDate, endDate } = await simulateComplexViewers(servers, videoUUID, useSessionId)
const expectCorrect = (stats: VideoStatsOverall) => {
expect(stats.viewersPeak).to.equal(3)
expect(new Date(stats.viewersPeakDate)).to.be.above(new Date(startDate)).and.below(new Date(endDate))
}
expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate, endDate }))
expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate }))
expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate }))
expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID }))
})
})
describe('Test countries/subdivisions', function () {
let videoUUID: string
it('Should not report countries/subdivisions if geoip is disabled', async function () {
this.timeout(120000)
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
await waitJobs(servers)
await servers[1].views.view({ id: uuid, sessionId: generateSessionId(), xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
await processViewersStats(servers)
const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
expect(stats.countries).to.have.lengthOf(0)
expect(stats.subdivisions).to.have.lengthOf(0)
})
it('Should not report subdivisions if database URL is not provided in the configuration', async function () {
this.timeout(240000)
const { uuid } = await servers[0].videos.quickUpload({ name: 'video without subdivisions' })
await waitJobs(servers)
await Promise.all([ servers[0].kill(), servers[1].kill() ])
const config = { geo_ip: { enabled: true, city: { database_url: '' } } }
await Promise.all([ servers[0].run(config), servers[1].run(config) ])
await servers[0].views.simulateViewer({
id: uuid,
sessionId: generateSessionId(),
xForwardedFor: '8.8.8.8,127.0.0.1',
currentTimes: [ 1, 2 ]
})
await servers[1].views.simulateViewer({
id: uuid,
sessionId: generateSessionId(),
xForwardedFor: '8.8.8.4,127.0.0.1',
currentTimes: [ 3, 4 ]
})
await servers[1].views.simulateViewer({
id: uuid,
sessionId: generateSessionId(),
xForwardedFor: '80.67.169.12,127.0.0.1',
currentTimes: [ 2, 3 ]
})
await processViewersStats(servers)
const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
expect(stats.countries).to.have.lengthOf(2)
expect(stats.subdivisions).to.have.lengthOf(0)
})
it('Should report countries/subdivisions if geoip is enabled', async function () {
this.timeout(240000)
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
videoUUID = uuid
await waitJobs(servers)
await Promise.all([
servers[0].kill(),
servers[1].kill()
])
const config = { geo_ip: { enabled: true } }
await Promise.all([
servers[0].run(config),
servers[1].run(config)
])
await servers[0].views.simulateViewer({
id: uuid,
sessionId: generateSessionId(),
xForwardedFor: '8.8.8.8,127.0.0.1',
currentTimes: [ 1, 2 ]
})
await servers[1].views.simulateViewer({
id: uuid,
sessionId: generateSessionId(),
xForwardedFor: '8.8.8.4,127.0.0.1',
currentTimes: [ 3, 4 ]
})
await servers[1].views.simulateViewer({
id: uuid,
sessionId: generateSessionId(),
xForwardedFor: '80.67.169.12,127.0.0.1',
currentTimes: [ 2, 3 ]
})
await processViewersStats(servers)
const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
expect(stats.countries).to.have.lengthOf(2)
expect(stats.countries[0].isoCode).to.equal('US')
expect(stats.countries[0].viewers).to.equal(2)
expect(stats.countries[1].isoCode).to.equal('FR')
expect(stats.countries[1].viewers).to.equal(1)
expect(stats.subdivisions[0].name).to.equal('California')
expect(stats.subdivisions[0].viewers).to.equal(2)
expect(stats.subdivisions[1].name).to.equal('Brittany')
expect(stats.subdivisions[1].viewers).to.equal(1)
})
it('Should filter countries/subdivisions 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)
expect(stats.subdivisions).to.have.lengthOf(0)
})
})
after(async function () {
await cleanupTests(servers)
})
}
describe('Not using session id', function () {
runTests({ useSessionId: false })
})
describe('Using session id', function () {
runTests({ useSessionId: true })
})
})