aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/stats.ts11
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/timeserie.ts14
-rw-r--r--server/models/view/local-video-viewer.ts12
-rw-r--r--server/tests/api/check-params/views.ts2
-rw-r--r--server/tests/api/views/video-views-overall-stats.ts26
-rw-r--r--server/tests/api/views/video-views-timeserie-stats.ts74
7 files changed, 113 insertions, 28 deletions
diff --git a/server/controllers/api/videos/stats.ts b/server/controllers/api/videos/stats.ts
index 30e2bb06c..e79f01888 100644
--- a/server/controllers/api/videos/stats.ts
+++ b/server/controllers/api/videos/stats.ts
@@ -67,18 +67,9 @@ async function getTimeserieStats (req: express.Request, res: express.Response) {
67 const stats = await LocalVideoViewerModel.getTimeserieStats({ 67 const stats = await LocalVideoViewerModel.getTimeserieStats({
68 video, 68 video,
69 metric, 69 metric,
70 startDate: query.startDate ?? buildOneMonthAgo().toISOString(), 70 startDate: query.startDate ?? video.createdAt.toISOString(),
71 endDate: query.endDate ?? new Date().toISOString() 71 endDate: query.endDate ?? new Date().toISOString()
72 }) 72 })
73 73
74 return res.json(stats) 74 return res.json(stats)
75} 75}
76
77function buildOneMonthAgo () {
78 const monthAgo = new Date()
79 monthAgo.setHours(0, 0, 0, 0)
80
81 monthAgo.setDate(monthAgo.getDate() - 29)
82
83 return monthAgo
84}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index fa0fbc19d..dca792b1b 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -813,7 +813,7 @@ const SEARCH_INDEX = {
813// --------------------------------------------------------------------------- 813// ---------------------------------------------------------------------------
814 814
815const STATS_TIMESERIE = { 815const STATS_TIMESERIE = {
816 MAX_DAYS: 30 816 MAX_DAYS: 365 * 10 // Around 10 years
817} 817}
818 818
819// --------------------------------------------------------------------------- 819// ---------------------------------------------------------------------------
diff --git a/server/lib/timeserie.ts b/server/lib/timeserie.ts
index bd3d1c1ca..08b12129a 100644
--- a/server/lib/timeserie.ts
+++ b/server/lib/timeserie.ts
@@ -9,7 +9,10 @@ function buildGroupByAndBoundaries (startDateString: string, endDateString: stri
9 logger.debug('Found "%s" group interval.', groupInterval, { startDate, endDate }) 9 logger.debug('Found "%s" group interval.', groupInterval, { startDate, endDate })
10 10
11 // Remove parts of the date we don't need 11 // Remove parts of the date we don't need
12 if (groupInterval.endsWith(' day') || groupInterval.endsWith(' days')) { 12 if (groupInterval.endsWith(' month') || groupInterval.endsWith(' months')) {
13 startDate.setDate(1)
14 startDate.setHours(0, 0, 0, 0)
15 } else if (groupInterval.endsWith(' day') || groupInterval.endsWith(' days')) {
13 startDate.setHours(0, 0, 0, 0) 16 startDate.setHours(0, 0, 0, 0)
14 } else if (groupInterval.endsWith(' hour') || groupInterval.endsWith(' hours')) { 17 } else if (groupInterval.endsWith(' hour') || groupInterval.endsWith(' hours')) {
15 startDate.setMinutes(0, 0, 0) 18 startDate.setMinutes(0, 0, 0)
@@ -33,16 +36,25 @@ export {
33// --------------------------------------------------------------------------- 36// ---------------------------------------------------------------------------
34 37
35function buildGroupInterval (startDate: Date, endDate: Date): string { 38function buildGroupInterval (startDate: Date, endDate: Date): string {
39 const aYear = 31536000
40 const aMonth = 2678400
36 const aDay = 86400 41 const aDay = 86400
37 const anHour = 3600 42 const anHour = 3600
38 const aMinute = 60 43 const aMinute = 60
39 44
40 const diffSeconds = (endDate.getTime() - startDate.getTime()) / 1000 45 const diffSeconds = (endDate.getTime() - startDate.getTime()) / 1000
41 46
47 if (diffSeconds >= 6 * aYear) return '6 months'
48 if (diffSeconds >= 2 * aYear) return '1 month'
49 if (diffSeconds >= 6 * aMonth) return '7 days'
50 if (diffSeconds >= 2 * aMonth) return '2 days'
51
42 if (diffSeconds >= 15 * aDay) return '1 day' 52 if (diffSeconds >= 15 * aDay) return '1 day'
43 if (diffSeconds >= 8 * aDay) return '12 hours' 53 if (diffSeconds >= 8 * aDay) return '12 hours'
44 if (diffSeconds >= 4 * aDay) return '6 hours' 54 if (diffSeconds >= 4 * aDay) return '6 hours'
55
45 if (diffSeconds >= 15 * anHour) return '1 hour' 56 if (diffSeconds >= 15 * anHour) return '1 hour'
57
46 if (diffSeconds >= 180 * aMinute) return '10 minutes' 58 if (diffSeconds >= 180 * aMinute) return '10 minutes'
47 59
48 return '1 minute' 60 return '1 minute'
diff --git a/server/models/view/local-video-viewer.ts b/server/models/view/local-video-viewer.ts
index 2862f8b96..2305c7262 100644
--- a/server/models/view/local-video-viewer.ts
+++ b/server/models/view/local-video-viewer.ts
@@ -136,7 +136,7 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
136 const watchPeakQuery = `WITH "watchPeakValues" AS ( 136 const watchPeakQuery = `WITH "watchPeakValues" AS (
137 SELECT "startDate" AS "dateBreakpoint", 1 AS "inc" 137 SELECT "startDate" AS "dateBreakpoint", 1 AS "inc"
138 FROM "localVideoViewer" 138 FROM "localVideoViewer"
139 WHERE "videoId" = :videoId 139 WHERE "videoId" = :videoId ${dateWhere}
140 UNION ALL 140 UNION ALL
141 SELECT "endDate" AS "dateBreakpoint", -1 AS "inc" 141 SELECT "endDate" AS "dateBreakpoint", -1 AS "inc"
142 FROM "localVideoViewer" 142 FROM "localVideoViewer"
@@ -165,6 +165,10 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
165 countriesPromise 165 countriesPromise
166 ]) 166 ])
167 167
168 const viewersPeak = rowsWatchPeak.length !== 0
169 ? parseInt(rowsWatchPeak[0].concurrent) || 0
170 : 0
171
168 return { 172 return {
169 totalWatchTime: rowsWatchTime.length !== 0 173 totalWatchTime: rowsWatchTime.length !== 0
170 ? Math.round(rowsWatchTime[0].totalWatchTime) || 0 174 ? Math.round(rowsWatchTime[0].totalWatchTime) || 0
@@ -173,10 +177,8 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
173 ? Math.round(rowsWatchTime[0].averageWatchTime) || 0 177 ? Math.round(rowsWatchTime[0].averageWatchTime) || 0
174 : 0, 178 : 0,
175 179
176 viewersPeak: rowsWatchPeak.length !== 0 180 viewersPeak,
177 ? parseInt(rowsWatchPeak[0].concurrent) || 0 181 viewersPeakDate: rowsWatchPeak.length !== 0 && viewersPeak !== 0
178 : 0,
179 viewersPeakDate: rowsWatchPeak.length !== 0
180 ? rowsWatchPeak[0].dateBreakpoint || null 182 ? rowsWatchPeak[0].dateBreakpoint || null
181 : null, 183 : null,
182 184
diff --git a/server/tests/api/check-params/views.ts b/server/tests/api/check-params/views.ts
index fe037b145..8f1fa796b 100644
--- a/server/tests/api/check-params/views.ts
+++ b/server/tests/api/check-params/views.ts
@@ -176,7 +176,7 @@ describe('Test videos views', function () {
176 await servers[0].videoStats.getTimeserieStats({ 176 await servers[0].videoStats.getTimeserieStats({
177 videoId, 177 videoId,
178 metric: 'viewers', 178 metric: 'viewers',
179 startDate: new Date('2021-04-07T08:31:57.126Z'), 179 startDate: new Date('2000-04-07T08:31:57.126Z'),
180 endDate: new Date(), 180 endDate: new Date(),
181 expectedStatus: HttpStatusCode.BAD_REQUEST_400 181 expectedStatus: HttpStatusCode.BAD_REQUEST_400
182 }) 182 })
diff --git a/server/tests/api/views/video-views-overall-stats.ts b/server/tests/api/views/video-views-overall-stats.ts
index 53b8f0d4b..02012388d 100644
--- a/server/tests/api/views/video-views-overall-stats.ts
+++ b/server/tests/api/views/video-views-overall-stats.ts
@@ -169,6 +169,7 @@ describe('Test views overall stats', function () {
169 169
170 describe('Test watchers peak stats of local videos on VOD', function () { 170 describe('Test watchers peak stats of local videos on VOD', function () {
171 let videoUUID: string 171 let videoUUID: string
172 let before2Watchers: Date
172 173
173 before(async function () { 174 before(async function () {
174 this.timeout(120000); 175 this.timeout(120000);
@@ -201,7 +202,7 @@ describe('Test views overall stats', function () {
201 it('Should have watcher peak with 2 watchers', async function () { 202 it('Should have watcher peak with 2 watchers', async function () {
202 this.timeout(60000) 203 this.timeout(60000)
203 204
204 const before = new Date() 205 before2Watchers = new Date()
205 await servers[0].views.view({ id: videoUUID, currentTime: 0 }) 206 await servers[0].views.view({ id: videoUUID, currentTime: 0 })
206 await servers[1].views.view({ id: videoUUID, currentTime: 0 }) 207 await servers[1].views.view({ id: videoUUID, currentTime: 0 })
207 await servers[0].views.view({ id: videoUUID, currentTime: 2 }) 208 await servers[0].views.view({ id: videoUUID, currentTime: 2 })
@@ -213,11 +214,26 @@ describe('Test views overall stats', function () {
213 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID }) 214 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
214 215
215 expect(stats.viewersPeak).to.equal(2) 216 expect(stats.viewersPeak).to.equal(2)
216 expect(new Date(stats.viewersPeakDate)).to.be.above(before).and.below(after) 217 expect(new Date(stats.viewersPeakDate)).to.be.above(before2Watchers).and.below(after)
218 })
219
220 it('Should filter peak viewers stats by date', async function () {
221 {
222 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
223 expect(stats.viewersPeak).to.equal(0)
224 expect(stats.viewersPeakDate).to.not.exist
225 }
226
227 {
228 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate: before2Watchers.toISOString() })
229 expect(stats.viewersPeak).to.equal(1)
230 expect(new Date(stats.viewersPeakDate)).to.be.below(before2Watchers)
231 }
217 }) 232 })
218 }) 233 })
219 234
220 describe('Test countries', function () { 235 describe('Test countries', function () {
236 let videoUUID: string
221 237
222 it('Should not report countries if geoip is disabled', async function () { 238 it('Should not report countries if geoip is disabled', async function () {
223 this.timeout(120000) 239 this.timeout(120000)
@@ -237,6 +253,7 @@ describe('Test views overall stats', function () {
237 this.timeout(240000) 253 this.timeout(240000)
238 254
239 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) 255 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
256 videoUUID = uuid
240 await waitJobs(servers) 257 await waitJobs(servers)
241 258
242 await Promise.all([ 259 await Promise.all([
@@ -265,6 +282,11 @@ describe('Test views overall stats', function () {
265 expect(stats.countries[1].isoCode).to.equal('FR') 282 expect(stats.countries[1].isoCode).to.equal('FR')
266 expect(stats.countries[1].viewers).to.equal(1) 283 expect(stats.countries[1].viewers).to.equal(1)
267 }) 284 })
285
286 it('Should filter countries stats by date', async function () {
287 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
288 expect(stats.countries).to.have.lengthOf(0)
289 })
268 }) 290 })
269 291
270 after(async function () { 292 after(async function () {
diff --git a/server/tests/api/views/video-views-timeserie-stats.ts b/server/tests/api/views/video-views-timeserie-stats.ts
index fd3aba188..6f03b0e82 100644
--- a/server/tests/api/views/video-views-timeserie-stats.ts
+++ b/server/tests/api/views/video-views-timeserie-stats.ts
@@ -9,6 +9,15 @@ import { cleanupTests, PeerTubeServer, stopFfmpeg } from '@shared/server-command
9 9
10const expect = chai.expect 10const expect = chai.expect
11 11
12function buildOneMonthAgo () {
13 const monthAgo = new Date()
14 monthAgo.setHours(0, 0, 0, 0)
15
16 monthAgo.setDate(monthAgo.getDate() - 29)
17
18 return monthAgo
19}
20
12describe('Test views timeserie stats', function () { 21describe('Test views timeserie stats', function () {
13 const availableMetrics: VideoStatsTimeserieMetric[] = [ 'viewers' ] 22 const availableMetrics: VideoStatsTimeserieMetric[] = [ 'viewers' ]
14 23
@@ -33,7 +42,7 @@ describe('Test views timeserie stats', function () {
33 for (const metric of availableMetrics) { 42 for (const metric of availableMetrics) {
34 const { data } = await servers[0].videoStats.getTimeserieStats({ videoId: vodVideoId, metric }) 43 const { data } = await servers[0].videoStats.getTimeserieStats({ videoId: vodVideoId, metric })
35 44
36 expect(data).to.have.lengthOf(30) 45 expect(data).to.have.length.at.least(1)
37 46
38 for (const d of data) { 47 for (const d of data) {
39 expect(d.value).to.equal(0) 48 expect(d.value).to.equal(0)
@@ -47,17 +56,19 @@ describe('Test views timeserie stats', function () {
47 let liveVideoId: string 56 let liveVideoId: string
48 let command: FfmpegCommand 57 let command: FfmpegCommand
49 58
50 function expectTodayLastValue (result: VideoStatsTimeserie, lastValue: number) { 59 function expectTodayLastValue (result: VideoStatsTimeserie, lastValue?: number) {
51 const { data } = result 60 const { data } = result
52 61
53 const last = data[data.length - 1] 62 const last = data[data.length - 1]
54 const today = new Date().getDate() 63 const today = new Date().getDate()
55 expect(new Date(last.date).getDate()).to.equal(today) 64 expect(new Date(last.date).getDate()).to.equal(today)
65
66 if (lastValue) expect(last.value).to.equal(lastValue)
56 } 67 }
57 68
58 function expectTimeserieData (result: VideoStatsTimeserie, lastValue: number) { 69 function expectTimeserieData (result: VideoStatsTimeserie, lastValue: number) {
59 const { data } = result 70 const { data } = result
60 expect(data).to.have.lengthOf(30) 71 expect(data).to.have.length.at.least(25)
61 72
62 expectTodayLastValue(result, lastValue) 73 expectTodayLastValue(result, lastValue)
63 74
@@ -87,14 +98,24 @@ describe('Test views timeserie stats', function () {
87 await processViewersStats(servers) 98 await processViewersStats(servers)
88 99
89 for (const videoId of [ vodVideoId, liveVideoId ]) { 100 for (const videoId of [ vodVideoId, liveVideoId ]) {
90 const result = await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'viewers' }) 101 const result = await servers[0].videoStats.getTimeserieStats({
102 videoId,
103 startDate: buildOneMonthAgo(),
104 endDate: new Date(),
105 metric: 'viewers'
106 })
91 expectTimeserieData(result, 2) 107 expectTimeserieData(result, 2)
92 } 108 }
93 }) 109 })
94 110
95 it('Should display appropriate watch time metrics', async function () { 111 it('Should display appropriate watch time metrics', async function () {
96 for (const videoId of [ vodVideoId, liveVideoId ]) { 112 for (const videoId of [ vodVideoId, liveVideoId ]) {
97 const result = await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'aggregateWatchTime' }) 113 const result = await servers[0].videoStats.getTimeserieStats({
114 videoId,
115 startDate: buildOneMonthAgo(),
116 endDate: new Date(),
117 metric: 'aggregateWatchTime'
118 })
98 expectTimeserieData(result, 8) 119 expectTimeserieData(result, 8)
99 120
100 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] }) 121 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
@@ -103,7 +124,12 @@ describe('Test views timeserie stats', function () {
103 await processViewersStats(servers) 124 await processViewersStats(servers)
104 125
105 for (const videoId of [ vodVideoId, liveVideoId ]) { 126 for (const videoId of [ vodVideoId, liveVideoId ]) {
106 const result = await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'aggregateWatchTime' }) 127 const result = await servers[0].videoStats.getTimeserieStats({
128 videoId,
129 startDate: buildOneMonthAgo(),
130 endDate: new Date(),
131 metric: 'aggregateWatchTime'
132 })
107 expectTimeserieData(result, 9) 133 expectTimeserieData(result, 9)
108 } 134 }
109 }) 135 })
@@ -130,6 +156,38 @@ describe('Test views timeserie stats', function () {
130 expectTodayLastValue(result, 9) 156 expectTodayLastValue(result, 9)
131 }) 157 })
132 158
159 it('Should automatically group by months', async function () {
160 const now = new Date()
161 const heightYearsAgo = new Date()
162 heightYearsAgo.setFullYear(heightYearsAgo.getFullYear() - 7)
163
164 const result = await servers[0].videoStats.getTimeserieStats({
165 videoId: vodVideoId,
166 metric: 'aggregateWatchTime',
167 startDate: heightYearsAgo,
168 endDate: now
169 })
170
171 expect(result.groupInterval).to.equal('6 months')
172 expect(result.data).to.have.length.above(10).and.below(200)
173 })
174
175 it('Should automatically group by days', async function () {
176 const now = new Date()
177 const threeMonthsAgo = new Date()
178 threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
179
180 const result = await servers[0].videoStats.getTimeserieStats({
181 videoId: vodVideoId,
182 metric: 'aggregateWatchTime',
183 startDate: threeMonthsAgo,
184 endDate: now
185 })
186
187 expect(result.groupInterval).to.equal('2 days')
188 expect(result.data).to.have.length.above(10).and.below(200)
189 })
190
133 it('Should automatically group by hours', async function () { 191 it('Should automatically group by hours', async function () {
134 const now = new Date() 192 const now = new Date()
135 const twoDaysAgo = new Date() 193 const twoDaysAgo = new Date()
@@ -165,7 +223,7 @@ describe('Test views timeserie stats', function () {
165 expect(result.data).to.have.length.above(20).and.below(30) 223 expect(result.data).to.have.length.above(20).and.below(30)
166 224
167 expectInterval(result, 60 * 10 * 1000) 225 expectInterval(result, 60 * 10 * 1000)
168 expectTodayLastValue(result, 9) 226 expectTodayLastValue(result)
169 }) 227 })
170 228
171 it('Should automatically group by one minute', async function () { 229 it('Should automatically group by one minute', async function () {
@@ -184,7 +242,7 @@ describe('Test views timeserie stats', function () {
184 expect(result.data).to.have.length.above(20).and.below(40) 242 expect(result.data).to.have.length.above(20).and.below(40)
185 243
186 expectInterval(result, 60 * 1000) 244 expectInterval(result, 60 * 1000)
187 expectTodayLastValue(result, 9) 245 expectTodayLastValue(result)
188 }) 246 })
189 247
190 after(async function () { 248 after(async function () {