diff options
Diffstat (limited to 'packages/tests/src/api/views')
-rw-r--r-- | packages/tests/src/api/views/index.ts | 5 | ||||
-rw-r--r-- | packages/tests/src/api/views/video-views-counter.ts | 153 | ||||
-rw-r--r-- | packages/tests/src/api/views/video-views-overall-stats.ts | 368 | ||||
-rw-r--r-- | packages/tests/src/api/views/video-views-retention-stats.ts | 53 | ||||
-rw-r--r-- | packages/tests/src/api/views/video-views-timeserie-stats.ts | 253 | ||||
-rw-r--r-- | packages/tests/src/api/views/videos-views-cleaner.ts | 98 |
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 @@ | |||
1 | export * from './video-views-counter.js' | ||
2 | export * from './video-views-overall-stats.js' | ||
3 | export * from './video-views-retention-stats.js' | ||
4 | export * from './video-views-timeserie-stats.js' | ||
5 | export * 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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewsBuffer } from '@tests/shared/views.js' | ||
6 | import { wait } from '@peertube/peertube-core-utils' | ||
7 | import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js' | ||
6 | import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands' | ||
7 | import { wait } from '@peertube/peertube-core-utils' | ||
8 | import { 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 | */ | ||
19 | async 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 | |||
58 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js' | ||
5 | import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
6 | |||
7 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js' | ||
6 | import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@peertube/peertube-models' | ||
7 | import { cleanupTests, PeerTubeServer, stopFfmpeg } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | function 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 | |||
18 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('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 | }) | ||