aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages/tests/src/api/views
diff options
context:
space:
mode:
Diffstat (limited to 'packages/tests/src/api/views')
-rw-r--r--packages/tests/src/api/views/index.ts5
-rw-r--r--packages/tests/src/api/views/video-views-counter.ts153
-rw-r--r--packages/tests/src/api/views/video-views-overall-stats.ts368
-rw-r--r--packages/tests/src/api/views/video-views-retention-stats.ts53
-rw-r--r--packages/tests/src/api/views/video-views-timeserie-stats.ts253
-rw-r--r--packages/tests/src/api/views/videos-views-cleaner.ts98
6 files changed, 930 insertions, 0 deletions
diff --git a/packages/tests/src/api/views/index.ts b/packages/tests/src/api/views/index.ts
new file mode 100644
index 000000000..2b7334d1a
--- /dev/null
+++ b/packages/tests/src/api/views/index.ts
@@ -0,0 +1,5 @@
1export * from './video-views-counter.js'
2export * from './video-views-overall-stats.js'
3export * from './video-views-retention-stats.js'
4export * from './video-views-timeserie-stats.js'
5export * from './videos-views-cleaner.js'
diff --git a/packages/tests/src/api/views/video-views-counter.ts b/packages/tests/src/api/views/video-views-counter.ts
new file mode 100644
index 000000000..d9afb0f18
--- /dev/null
+++ b/packages/tests/src/api/views/video-views-counter.ts
@@ -0,0 +1,153 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { prepareViewsServers, prepareViewsVideos, processViewsBuffer } from '@tests/shared/views.js'
6import { wait } from '@peertube/peertube-core-utils'
7import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
8
9describe('Test video views/viewers counters', function () {
10 let servers: PeerTubeServer[]
11
12 async function checkCounter (field: 'views' | 'viewers', id: string, expected: number) {
13 for (const server of servers) {
14 const video = await server.videos.get({ id })
15
16 const messageSuffix = video.isLive
17 ? 'live video'
18 : 'vod video'
19
20 expect(video[field]).to.equal(expected, `${field} not valid on server ${server.serverNumber} for ${messageSuffix} ${video.uuid}`)
21 }
22 }
23
24 before(async function () {
25 this.timeout(120000)
26
27 servers = await prepareViewsServers()
28 })
29
30 describe('Test views counter on VOD', function () {
31 let videoUUID: string
32
33 before(async function () {
34 this.timeout(120000)
35
36 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
37 videoUUID = uuid
38
39 await waitJobs(servers)
40 })
41
42 it('Should not view a video if watch time is below the threshold', async function () {
43 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 2 ] })
44 await processViewsBuffer(servers)
45
46 await checkCounter('views', videoUUID, 0)
47 })
48
49 it('Should view a video if watch time is above the threshold', async function () {
50 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] })
51 await processViewsBuffer(servers)
52
53 await checkCounter('views', videoUUID, 1)
54 })
55
56 it('Should not view again this video with the same IP', async function () {
57 await servers[0].views.simulateViewer({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 1, 4 ] })
58 await servers[0].views.simulateViewer({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 1, 4 ] })
59 await processViewsBuffer(servers)
60
61 await checkCounter('views', videoUUID, 2)
62 })
63
64 it('Should view the video from server 2 and send the event', async function () {
65 await servers[1].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] })
66 await waitJobs(servers)
67 await processViewsBuffer(servers)
68
69 await checkCounter('views', videoUUID, 3)
70 })
71 })
72
73 describe('Test views and viewers counters on live and VOD', function () {
74 let liveVideoId: string
75 let vodVideoId: string
76 let command: FfmpegCommand
77
78 before(async function () {
79 this.timeout(240000);
80
81 ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
82 })
83
84 it('Should display no views and viewers', async function () {
85 await checkCounter('views', liveVideoId, 0)
86 await checkCounter('viewers', liveVideoId, 0)
87
88 await checkCounter('views', vodVideoId, 0)
89 await checkCounter('viewers', vodVideoId, 0)
90 })
91
92 it('Should view twice and display 1 view/viewer', async function () {
93 this.timeout(30000)
94
95 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
96 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
97 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
98 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
99
100 await waitJobs(servers)
101 await checkCounter('viewers', liveVideoId, 1)
102 await checkCounter('viewers', vodVideoId, 1)
103
104 await processViewsBuffer(servers)
105
106 await checkCounter('views', liveVideoId, 1)
107 await checkCounter('views', vodVideoId, 1)
108 })
109
110 it('Should wait and display 0 viewers but still have 1 view', async function () {
111 this.timeout(30000)
112
113 await wait(12000)
114 await waitJobs(servers)
115
116 await checkCounter('views', liveVideoId, 1)
117 await checkCounter('viewers', liveVideoId, 0)
118
119 await checkCounter('views', vodVideoId, 1)
120 await checkCounter('viewers', vodVideoId, 0)
121 })
122
123 it('Should view on a remote and on local and display 2 viewers and 3 views', async function () {
124 this.timeout(30000)
125
126 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
127 await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
128 await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
129
130 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
131 await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
132 await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
133
134 await waitJobs(servers)
135
136 await checkCounter('viewers', liveVideoId, 2)
137 await checkCounter('viewers', vodVideoId, 2)
138
139 await processViewsBuffer(servers)
140
141 await checkCounter('views', liveVideoId, 3)
142 await checkCounter('views', vodVideoId, 3)
143 })
144
145 after(async function () {
146 await stopFfmpeg(command)
147 })
148 })
149
150 after(async function () {
151 await cleanupTests(servers)
152 })
153})
diff --git a/packages/tests/src/api/views/video-views-overall-stats.ts b/packages/tests/src/api/views/video-views-overall-stats.ts
new file mode 100644
index 000000000..6ea0da2d9
--- /dev/null
+++ b/packages/tests/src/api/views/video-views-overall-stats.ts
@@ -0,0 +1,368 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
6import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
7import { wait } from '@peertube/peertube-core-utils'
8import { VideoStatsOverall } from '@peertube/peertube-models'
9
10/**
11 *
12 * Simulate 5 sections of viewers
13 * * user0 started and ended before start date
14 * * user1 started before start date and ended in the interval
15 * * user2 started started in the interval and ended after end date
16 * * user3 started and ended in the interval
17 * * user4 started and ended after end date
18 */
19async function simulateComplexViewers (servers: PeerTubeServer[], videoUUID: string) {
20 const user0 = '8.8.8.8,127.0.0.1'
21 const user1 = '8.8.8.8,127.0.0.1'
22 const user2 = '8.8.8.9,127.0.0.1'
23 const user3 = '8.8.8.10,127.0.0.1'
24 const user4 = '8.8.8.11,127.0.0.1'
25
26 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user0 }) // User 0 starts
27 await wait(500)
28
29 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user1 }) // User 1 starts
30 await servers[0].views.view({ id: videoUUID, currentTime: 2, xForwardedFor: user0 }) // User 0 ends
31 await wait(500)
32
33 const startDate = new Date().toISOString()
34 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user2 }) // User 2 starts
35 await wait(500)
36
37 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user3 }) // User 3 starts
38 await wait(500)
39
40 await servers[0].views.view({ id: videoUUID, currentTime: 4, xForwardedFor: user1 }) // User 1 ends
41 await wait(500)
42
43 await servers[0].views.view({ id: videoUUID, currentTime: 3, xForwardedFor: user3 }) // User 3 ends
44 await wait(500)
45
46 const endDate = new Date().toISOString()
47 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user4 }) // User 4 starts
48 await servers[0].views.view({ id: videoUUID, currentTime: 5, xForwardedFor: user2 }) // User 2 ends
49 await wait(500)
50
51 await servers[0].views.view({ id: videoUUID, currentTime: 1, xForwardedFor: user4 }) // User 4 ends
52
53 await processViewersStats(servers)
54
55 return { startDate, endDate }
56}
57
58describe('Test views overall stats', function () {
59 let servers: PeerTubeServer[]
60
61 before(async function () {
62 this.timeout(120000)
63
64 servers = await prepareViewsServers()
65 })
66
67 describe('Test watch time stats of local videos on live and VOD', function () {
68 let vodVideoId: string
69 let liveVideoId: string
70 let command: FfmpegCommand
71
72 before(async function () {
73 this.timeout(240000);
74
75 ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
76 })
77
78 it('Should display overall stats of a video with no viewers', async function () {
79 for (const videoId of [ liveVideoId, vodVideoId ]) {
80 const stats = await servers[0].videoStats.getOverallStats({ videoId })
81 const video = await servers[0].videos.get({ id: videoId })
82
83 expect(video.views).to.equal(0)
84 expect(stats.averageWatchTime).to.equal(0)
85 expect(stats.totalWatchTime).to.equal(0)
86 expect(stats.totalViewers).to.equal(0)
87 }
88 })
89
90 it('Should display overall stats with 1 viewer below the watch time limit', async function () {
91 this.timeout(60000)
92
93 for (const videoId of [ liveVideoId, vodVideoId ]) {
94 await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
95 }
96
97 await processViewersStats(servers)
98
99 for (const videoId of [ liveVideoId, vodVideoId ]) {
100 const stats = await servers[0].videoStats.getOverallStats({ videoId })
101 const video = await servers[0].videos.get({ id: videoId })
102
103 expect(video.views).to.equal(0)
104 expect(stats.averageWatchTime).to.equal(1)
105 expect(stats.totalWatchTime).to.equal(1)
106 expect(stats.totalViewers).to.equal(1)
107 }
108 })
109
110 it('Should display overall stats with 2 viewers', async function () {
111 this.timeout(60000)
112
113 {
114 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] })
115 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35, 40 ] })
116
117 await processViewersStats(servers)
118
119 {
120 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
121 const video = await servers[0].videos.get({ id: vodVideoId })
122
123 expect(video.views).to.equal(1)
124 expect(stats.averageWatchTime).to.equal(2)
125 expect(stats.totalWatchTime).to.equal(4)
126 expect(stats.totalViewers).to.equal(2)
127 }
128
129 {
130 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
131 const video = await servers[0].videos.get({ id: liveVideoId })
132
133 expect(video.views).to.equal(1)
134 expect(stats.averageWatchTime).to.equal(21)
135 expect(stats.totalWatchTime).to.equal(41)
136 expect(stats.totalViewers).to.equal(2)
137 }
138 }
139 })
140
141 it('Should display overall stats with a remote viewer below the watch time limit', async function () {
142 this.timeout(60000)
143
144 for (const videoId of [ liveVideoId, vodVideoId ]) {
145 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 2 ] })
146 }
147
148 await processViewersStats(servers)
149
150 {
151 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
152 const video = await servers[0].videos.get({ id: vodVideoId })
153
154 expect(video.views).to.equal(1)
155 expect(stats.averageWatchTime).to.equal(2)
156 expect(stats.totalWatchTime).to.equal(6)
157 expect(stats.totalViewers).to.equal(3)
158 }
159
160 {
161 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
162 const video = await servers[0].videos.get({ id: liveVideoId })
163
164 expect(video.views).to.equal(1)
165 expect(stats.averageWatchTime).to.equal(14)
166 expect(stats.totalWatchTime).to.equal(43)
167 expect(stats.totalViewers).to.equal(3)
168 }
169 })
170
171 it('Should display overall stats with a remote viewer above the watch time limit', async function () {
172 this.timeout(60000)
173
174 await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
175 await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 45 ] })
176 await processViewersStats(servers)
177
178 {
179 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
180 const video = await servers[0].videos.get({ id: vodVideoId })
181
182 expect(video.views).to.equal(2)
183 expect(stats.averageWatchTime).to.equal(3)
184 expect(stats.totalWatchTime).to.equal(11)
185 expect(stats.totalViewers).to.equal(4)
186 }
187
188 {
189 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
190 const video = await servers[0].videos.get({ id: liveVideoId })
191
192 expect(video.views).to.equal(2)
193 expect(stats.averageWatchTime).to.equal(22)
194 expect(stats.totalWatchTime).to.equal(88)
195 expect(stats.totalViewers).to.equal(4)
196 }
197 })
198
199 it('Should filter overall stats by date', async function () {
200 this.timeout(60000)
201
202 const beforeView = new Date()
203
204 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] })
205 await processViewersStats(servers)
206
207 {
208 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId, startDate: beforeView.toISOString() })
209 expect(stats.averageWatchTime).to.equal(3)
210 expect(stats.totalWatchTime).to.equal(3)
211 expect(stats.totalViewers).to.equal(1)
212 }
213
214 {
215 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId, endDate: beforeView.toISOString() })
216 expect(stats.averageWatchTime).to.equal(22)
217 expect(stats.totalWatchTime).to.equal(88)
218 expect(stats.totalViewers).to.equal(4)
219 }
220 })
221
222 after(async function () {
223 await stopFfmpeg(command)
224 })
225 })
226
227 describe('Test watchers peak stats of local videos on VOD', function () {
228 let videoUUID: string
229 let before2Watchers: Date
230
231 before(async function () {
232 this.timeout(240000);
233
234 ({ vodVideoId: videoUUID } = await prepareViewsVideos({ servers, live: true, vod: true }))
235 })
236
237 it('Should not have watchers peak', async function () {
238 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
239
240 expect(stats.viewersPeak).to.equal(0)
241 expect(stats.viewersPeakDate).to.be.null
242 })
243
244 it('Should have watcher peak with 1 watcher', async function () {
245 this.timeout(60000)
246
247 const before = new Date()
248 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 0, 2 ] })
249 const after = new Date()
250
251 await processViewersStats(servers)
252
253 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
254
255 expect(stats.viewersPeak).to.equal(1)
256 expect(new Date(stats.viewersPeakDate)).to.be.above(before).and.below(after)
257 })
258
259 it('Should have watcher peak with 2 watchers', async function () {
260 this.timeout(60000)
261
262 before2Watchers = new Date()
263 await servers[0].views.view({ id: videoUUID, currentTime: 0 })
264 await servers[1].views.view({ id: videoUUID, currentTime: 0 })
265 await servers[0].views.view({ id: videoUUID, currentTime: 2 })
266 await servers[1].views.view({ id: videoUUID, currentTime: 2 })
267 const after = new Date()
268
269 await processViewersStats(servers)
270
271 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
272
273 expect(stats.viewersPeak).to.equal(2)
274 expect(new Date(stats.viewersPeakDate)).to.be.above(before2Watchers).and.below(after)
275 })
276
277 it('Should filter peak viewers stats by date', async function () {
278 {
279 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
280 expect(stats.viewersPeak).to.equal(0)
281 expect(stats.viewersPeakDate).to.not.exist
282 }
283
284 {
285 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate: before2Watchers.toISOString() })
286 expect(stats.viewersPeak).to.equal(1)
287 expect(new Date(stats.viewersPeakDate)).to.be.below(before2Watchers)
288 }
289 })
290
291 it('Should complex filter peak viewers by date', async function () {
292 this.timeout(60000)
293
294 const { startDate, endDate } = await simulateComplexViewers(servers, videoUUID)
295
296 const expectCorrect = (stats: VideoStatsOverall) => {
297 expect(stats.viewersPeak).to.equal(3)
298 expect(new Date(stats.viewersPeakDate)).to.be.above(new Date(startDate)).and.below(new Date(endDate))
299 }
300
301 expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate, endDate }))
302 expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate }))
303 expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate }))
304 expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID }))
305 })
306 })
307
308 describe('Test countries', function () {
309 let videoUUID: string
310
311 it('Should not report countries if geoip is disabled', async function () {
312 this.timeout(120000)
313
314 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
315 await waitJobs(servers)
316
317 await servers[1].views.view({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
318
319 await processViewersStats(servers)
320
321 const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
322 expect(stats.countries).to.have.lengthOf(0)
323 })
324
325 it('Should report countries if geoip is enabled', async function () {
326 this.timeout(240000)
327
328 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
329 videoUUID = uuid
330 await waitJobs(servers)
331
332 await Promise.all([
333 servers[0].kill(),
334 servers[1].kill()
335 ])
336
337 const config = { geo_ip: { enabled: true } }
338 await Promise.all([
339 servers[0].run(config),
340 servers[1].run(config)
341 ])
342
343 await servers[0].views.view({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
344 await servers[1].views.view({ id: uuid, xForwardedFor: '8.8.8.4,127.0.0.1', currentTime: 3 })
345 await servers[1].views.view({ id: uuid, xForwardedFor: '80.67.169.12,127.0.0.1', currentTime: 2 })
346
347 await processViewersStats(servers)
348
349 const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
350 expect(stats.countries).to.have.lengthOf(2)
351
352 expect(stats.countries[0].isoCode).to.equal('US')
353 expect(stats.countries[0].viewers).to.equal(2)
354
355 expect(stats.countries[1].isoCode).to.equal('FR')
356 expect(stats.countries[1].viewers).to.equal(1)
357 })
358
359 it('Should filter countries stats by date', async function () {
360 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
361 expect(stats.countries).to.have.lengthOf(0)
362 })
363 })
364
365 after(async function () {
366 await cleanupTests(servers)
367 })
368})
diff --git a/packages/tests/src/api/views/video-views-retention-stats.ts b/packages/tests/src/api/views/video-views-retention-stats.ts
new file mode 100644
index 000000000..4cd0c7da9
--- /dev/null
+++ b/packages/tests/src/api/views/video-views-retention-stats.ts
@@ -0,0 +1,53 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
5import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands'
6
7describe('Test views retention stats', function () {
8 let servers: PeerTubeServer[]
9
10 before(async function () {
11 this.timeout(120000)
12
13 servers = await prepareViewsServers()
14 })
15
16 describe('Test retention stats on VOD', function () {
17 let vodVideoId: string
18
19 before(async function () {
20 this.timeout(240000);
21
22 ({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true }))
23 })
24
25 it('Should display empty retention', async function () {
26 const { data } = await servers[0].videoStats.getRetentionStats({ videoId: vodVideoId })
27 expect(data).to.have.lengthOf(6)
28
29 for (let i = 0; i < 6; i++) {
30 expect(data[i].second).to.equal(i)
31 expect(data[i].retentionPercent).to.equal(0)
32 }
33 })
34
35 it('Should display appropriate retention metrics', async function () {
36 await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
37 await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 3 ] })
38 await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 4 ] })
39 await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
40
41 await processViewersStats(servers)
42
43 const { data } = await servers[0].videoStats.getRetentionStats({ videoId: vodVideoId })
44 expect(data).to.have.lengthOf(6)
45
46 expect(data.map(d => d.retentionPercent)).to.deep.equal([ 50, 75, 25, 25, 25, 0 ])
47 })
48 })
49
50 after(async function () {
51 await cleanupTests(servers)
52 })
53})
diff --git a/packages/tests/src/api/views/video-views-timeserie-stats.ts b/packages/tests/src/api/views/video-views-timeserie-stats.ts
new file mode 100644
index 000000000..44fccb644
--- /dev/null
+++ b/packages/tests/src/api/views/video-views-timeserie-stats.ts
@@ -0,0 +1,253 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
6import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@peertube/peertube-models'
7import { cleanupTests, PeerTubeServer, stopFfmpeg } from '@peertube/peertube-server-commands'
8
9function buildOneMonthAgo () {
10 const monthAgo = new Date()
11 monthAgo.setHours(0, 0, 0, 0)
12
13 monthAgo.setDate(monthAgo.getDate() - 29)
14
15 return monthAgo
16}
17
18describe('Test views timeserie stats', function () {
19 const availableMetrics: VideoStatsTimeserieMetric[] = [ 'viewers' ]
20
21 let servers: PeerTubeServer[]
22
23 before(async function () {
24 this.timeout(120000)
25
26 servers = await prepareViewsServers()
27 })
28
29 describe('Common metric tests', function () {
30 let vodVideoId: string
31
32 before(async function () {
33 this.timeout(240000);
34
35 ({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true }))
36 })
37
38 it('Should display empty metric stats', async function () {
39 for (const metric of availableMetrics) {
40 const { data } = await servers[0].videoStats.getTimeserieStats({ videoId: vodVideoId, metric })
41
42 expect(data).to.have.length.at.least(1)
43
44 for (const d of data) {
45 expect(d.value).to.equal(0)
46 }
47 }
48 })
49 })
50
51 describe('Test viewer and watch time metrics on live and VOD', function () {
52 let vodVideoId: string
53 let liveVideoId: string
54 let command: FfmpegCommand
55
56 function expectTodayLastValue (result: VideoStatsTimeserie, lastValue?: number) {
57 const { data } = result
58
59 const last = data[data.length - 1]
60 const today = new Date().getDate()
61 expect(new Date(last.date).getDate()).to.equal(today)
62
63 if (lastValue) expect(last.value).to.equal(lastValue)
64 }
65
66 function expectTimeserieData (result: VideoStatsTimeserie, lastValue: number) {
67 const { data } = result
68 expect(data).to.have.length.at.least(25)
69
70 expectTodayLastValue(result, lastValue)
71
72 for (let i = 0; i < data.length - 2; i++) {
73 expect(data[i].value).to.equal(0)
74 }
75 }
76
77 function expectInterval (result: VideoStatsTimeserie, intervalMs: number) {
78 const first = result.data[0]
79 const second = result.data[1]
80 expect(new Date(second.date).getTime() - new Date(first.date).getTime()).to.equal(intervalMs)
81 }
82
83 before(async function () {
84 this.timeout(240000);
85
86 ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
87 })
88
89 it('Should display appropriate viewers metrics', async function () {
90 for (const videoId of [ vodVideoId, liveVideoId ]) {
91 await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 3 ] })
92 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 5 ] })
93 }
94
95 await processViewersStats(servers)
96
97 for (const videoId of [ vodVideoId, liveVideoId ]) {
98 const result = await servers[0].videoStats.getTimeserieStats({
99 videoId,
100 startDate: buildOneMonthAgo(),
101 endDate: new Date(),
102 metric: 'viewers'
103 })
104 expectTimeserieData(result, 2)
105 }
106 })
107
108 it('Should display appropriate watch time metrics', async function () {
109 for (const videoId of [ vodVideoId, liveVideoId ]) {
110 const result = await servers[0].videoStats.getTimeserieStats({
111 videoId,
112 startDate: buildOneMonthAgo(),
113 endDate: new Date(),
114 metric: 'aggregateWatchTime'
115 })
116 expectTimeserieData(result, 8)
117
118 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
119 }
120
121 await processViewersStats(servers)
122
123 for (const videoId of [ vodVideoId, liveVideoId ]) {
124 const result = await servers[0].videoStats.getTimeserieStats({
125 videoId,
126 startDate: buildOneMonthAgo(),
127 endDate: new Date(),
128 metric: 'aggregateWatchTime'
129 })
130 expectTimeserieData(result, 9)
131 }
132 })
133
134 it('Should use a custom start/end date', async function () {
135 const now = new Date()
136 const twentyDaysAgo = new Date()
137 twentyDaysAgo.setDate(twentyDaysAgo.getDate() - 19)
138
139 const result = await servers[0].videoStats.getTimeserieStats({
140 videoId: vodVideoId,
141 metric: 'aggregateWatchTime',
142 startDate: twentyDaysAgo,
143 endDate: now
144 })
145
146 expect(result.groupInterval).to.equal('1 day')
147 expect(result.data).to.have.lengthOf(20)
148
149 const first = result.data[0]
150 expect(new Date(first.date).toLocaleDateString()).to.equal(twentyDaysAgo.toLocaleDateString())
151
152 expectInterval(result, 24 * 3600 * 1000)
153 expectTodayLastValue(result, 9)
154 })
155
156 it('Should automatically group by months', async function () {
157 const now = new Date()
158 const heightYearsAgo = new Date()
159 heightYearsAgo.setFullYear(heightYearsAgo.getFullYear() - 7)
160
161 const result = await servers[0].videoStats.getTimeserieStats({
162 videoId: vodVideoId,
163 metric: 'aggregateWatchTime',
164 startDate: heightYearsAgo,
165 endDate: now
166 })
167
168 expect(result.groupInterval).to.equal('6 months')
169 expect(result.data).to.have.length.above(10).and.below(200)
170 })
171
172 it('Should automatically group by days', async function () {
173 const now = new Date()
174 const threeMonthsAgo = new Date()
175 threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
176
177 const result = await servers[0].videoStats.getTimeserieStats({
178 videoId: vodVideoId,
179 metric: 'aggregateWatchTime',
180 startDate: threeMonthsAgo,
181 endDate: now
182 })
183
184 expect(result.groupInterval).to.equal('2 days')
185 expect(result.data).to.have.length.above(10).and.below(200)
186 })
187
188 it('Should automatically group by hours', async function () {
189 const now = new Date()
190 const twoDaysAgo = new Date()
191 twoDaysAgo.setDate(twoDaysAgo.getDate() - 1)
192
193 const result = await servers[0].videoStats.getTimeserieStats({
194 videoId: vodVideoId,
195 metric: 'aggregateWatchTime',
196 startDate: twoDaysAgo,
197 endDate: now
198 })
199
200 expect(result.groupInterval).to.equal('1 hour')
201 expect(result.data).to.have.length.above(24).and.below(50)
202
203 expectInterval(result, 3600 * 1000)
204 expectTodayLastValue(result, 9)
205 })
206
207 it('Should automatically group by ten minutes', async function () {
208 const now = new Date()
209 const twoHoursAgo = new Date()
210 twoHoursAgo.setHours(twoHoursAgo.getHours() - 4)
211
212 const result = await servers[0].videoStats.getTimeserieStats({
213 videoId: vodVideoId,
214 metric: 'aggregateWatchTime',
215 startDate: twoHoursAgo,
216 endDate: now
217 })
218
219 expect(result.groupInterval).to.equal('10 minutes')
220 expect(result.data).to.have.length.above(20).and.below(30)
221
222 expectInterval(result, 60 * 10 * 1000)
223 expectTodayLastValue(result)
224 })
225
226 it('Should automatically group by one minute', async function () {
227 const now = new Date()
228 const thirtyAgo = new Date()
229 thirtyAgo.setMinutes(thirtyAgo.getMinutes() - 30)
230
231 const result = await servers[0].videoStats.getTimeserieStats({
232 videoId: vodVideoId,
233 metric: 'aggregateWatchTime',
234 startDate: thirtyAgo,
235 endDate: now
236 })
237
238 expect(result.groupInterval).to.equal('1 minute')
239 expect(result.data).to.have.length.above(20).and.below(40)
240
241 expectInterval(result, 60 * 1000)
242 expectTodayLastValue(result)
243 })
244
245 after(async function () {
246 await stopFfmpeg(command)
247 })
248 })
249
250 after(async function () {
251 await cleanupTests(servers)
252 })
253})
diff --git a/packages/tests/src/api/views/videos-views-cleaner.ts b/packages/tests/src/api/views/videos-views-cleaner.ts
new file mode 100644
index 000000000..521dd9b5e
--- /dev/null
+++ b/packages/tests/src/api/views/videos-views-cleaner.ts
@@ -0,0 +1,98 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { SQLCommand } from '@tests/shared/sql-command.js'
5import { wait } from '@peertube/peertube-core-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 killallServers,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15
16describe('Test video views cleaner', function () {
17 let servers: PeerTubeServer[]
18 let sqlCommands: SQLCommand[] = []
19
20 let videoIdServer1: string
21 let videoIdServer2: string
22
23 before(async function () {
24 this.timeout(240000)
25
26 servers = await createMultipleServers(2)
27 await setAccessTokensToServers(servers)
28
29 await doubleFollow(servers[0], servers[1])
30
31 videoIdServer1 = (await servers[0].videos.quickUpload({ name: 'video server 1' })).uuid
32 videoIdServer2 = (await servers[1].videos.quickUpload({ name: 'video server 2' })).uuid
33
34 await waitJobs(servers)
35
36 await servers[0].views.simulateView({ id: videoIdServer1 })
37 await servers[1].views.simulateView({ id: videoIdServer1 })
38 await servers[0].views.simulateView({ id: videoIdServer2 })
39 await servers[1].views.simulateView({ id: videoIdServer2 })
40
41 await waitJobs(servers)
42
43 sqlCommands = servers.map(s => new SQLCommand(s))
44 })
45
46 it('Should not clean old video views', async function () {
47 this.timeout(50000)
48
49 await killallServers([ servers[0] ])
50
51 await servers[0].run({ views: { videos: { remote: { max_age: '10 days' } } } })
52
53 await wait(6000)
54
55 // Should still have views
56
57 for (let i = 0; i < servers.length; i++) {
58 const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
59 expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
60 }
61
62 for (let i = 0; i < servers.length; i++) {
63 const total = await sqlCommands[i].countVideoViewsOf(videoIdServer2)
64 expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
65 }
66 })
67
68 it('Should clean old video views', async function () {
69 this.timeout(50000)
70
71 await killallServers([ servers[0] ])
72
73 await servers[0].run({ views: { videos: { remote: { max_age: '5 seconds' } } } })
74
75 await wait(6000)
76
77 // Should still have views
78
79 for (let i = 0; i < servers.length; i++) {
80 const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
81 expect(total).to.equal(2)
82 }
83
84 const totalServer1 = await sqlCommands[0].countVideoViewsOf(videoIdServer2)
85 expect(totalServer1).to.equal(0)
86
87 const totalServer2 = await sqlCommands[1].countVideoViewsOf(videoIdServer2)
88 expect(totalServer2).to.equal(2)
89 })
90
91 after(async function () {
92 for (const sqlCommand of sqlCommands) {
93 await sqlCommand.cleanup()
94 }
95
96 await cleanupTests(servers)
97 })
98})