From 6981c18a029d52bec94bcae9fc41fa8d8a78001b Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 17 Jan 2019 17:16:08 +0900 Subject: [PATCH] Implement active users chart Resolve #3904 --- locales/ja-JP.yml | 1 + src/chart/active-users.ts | 48 +++++++++++++++++++ src/client/app/admin/views/charts.vue | 36 +++++++++++--- .../api/endpoints/charts/active-users.ts | 34 +++++++++++++ .../api/endpoints/notes/hybrid-timeline.ts | 3 ++ .../api/endpoints/notes/local-timeline.ts | 5 ++ src/server/api/endpoints/notes/timeline.ts | 3 ++ src/services/note/create.ts | 3 ++ 8 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/chart/active-users.ts create mode 100644 src/server/api/endpoints/charts/active-users.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8c75fe6eb..a7796c4d1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1209,6 +1209,7 @@ admin/views/charts.vue: notes-total: "投稿の積算" users: "ユーザーの増減" users-total: "ユーザーの積算" + active-users: "アクティブユーザー数" drive: "ドライブ使用量の増減" drive-total: "ドライブ使用量の積算" drive-files: "ドライブのファイル数の増減" diff --git a/src/chart/active-users.ts b/src/chart/active-users.ts new file mode 100644 index 000000000..06d9b8aa9 --- /dev/null +++ b/src/chart/active-users.ts @@ -0,0 +1,48 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj } from '.'; +import { IUser, isLocalUser } from '../models/user'; + +/** + * アクティブユーザーに関するチャート + */ +type ActiveUsersLog = { + local: { + /** + * アクティブユーザー数 + */ + count: number; + }; + + remote: ActiveUsersLog['local']; +}; + +class ActiveUsersChart extends Chart { + constructor() { + super('activeUsers'); + } + + @autobind + protected async getTemplate(init: boolean, latest?: ActiveUsersLog): Promise { + return { + local: { + count: 0 + }, + remote: { + count: 0 + } + }; + } + + @autobind + public async update(user: IUser) { + const update: Obj = { + count: 1 + }; + + await this.incIfUnique({ + [isLocalUser(user) ? 'local' : 'remote']: update + }, 'users', user._id.toHexString()); + } +} + +export default new ActiveUsersChart(); diff --git a/src/client/app/admin/views/charts.vue b/src/client/app/admin/views/charts.vue index 13e8b3671..04cf9512c 100644 --- a/src/client/app/admin/views/charts.vue +++ b/src/client/app/admin/views/charts.vue @@ -10,6 +10,7 @@ + @@ -67,6 +68,7 @@ export default Vue.extend({ case 'federation-instances-total': return this.federationInstancesChart(true); case 'users': return this.usersChart(false); case 'users-total': return this.usersChart(true); + case 'active-users': return this.activeUsersChart(); case 'notes': return this.notesChart('combined'); case 'local-notes': return this.notesChart('local'); case 'remote-notes': return this.notesChart('remote'); @@ -107,12 +109,14 @@ export default Vue.extend({ const [perHour, perDay] = await Promise.all([Promise.all([ this.$root.api('charts/federation', { limit: limit, span: 'hour' }), this.$root.api('charts/users', { limit: limit, span: 'hour' }), + this.$root.api('charts/active-users', { limit: limit, span: 'hour' }), this.$root.api('charts/notes', { limit: limit, span: 'hour' }), this.$root.api('charts/drive', { limit: limit, span: 'hour' }), this.$root.api('charts/network', { limit: limit, span: 'hour' }) ]), Promise.all([ this.$root.api('charts/federation', { limit: limit, span: 'day' }), this.$root.api('charts/users', { limit: limit, span: 'day' }), + this.$root.api('charts/active-users', { limit: limit, span: 'day' }), this.$root.api('charts/notes', { limit: limit, span: 'day' }), this.$root.api('charts/drive', { limit: limit, span: 'day' }), this.$root.api('charts/network', { limit: limit, span: 'day' }) @@ -122,16 +126,18 @@ export default Vue.extend({ perHour: { federation: perHour[0], users: perHour[1], - notes: perHour[2], - drive: perHour[3], - network: perHour[4] + activeUsers: perHour[2], + notes: perHour[3], + drive: perHour[4], + network: perHour[5] }, perDay: { federation: perDay[0], users: perDay[1], - notes: perDay[2], - drive: perDay[3], - network: perDay[4] + activeUsers: perDay[2], + notes: perDay[3], + drive: perDay[4], + network: perDay[5] } }; @@ -321,6 +327,24 @@ export default Vue.extend({ }; }, + activeUsersChart(): any { + return { + series: [{ + name: 'Combined', + type: 'line', + data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count)) + }, { + name: 'Local', + type: 'area', + data: this.format(this.stats.activeUsers.local.count) + }, { + name: 'Remote', + type: 'area', + data: this.format(this.stats.activeUsers.remote.count) + }] + }; + }, + driveChart(): any { return { bytes: true, diff --git a/src/server/api/endpoints/charts/active-users.ts b/src/server/api/endpoints/charts/active-users.ts new file mode 100644 index 000000000..5187e5b35 --- /dev/null +++ b/src/server/api/endpoints/charts/active-users.ts @@ -0,0 +1,34 @@ +import $ from 'cafy'; +import define from '../../define'; +import activeUsersChart from '../../../../chart/active-users'; + +export const meta = { + stability: 'stable', + + desc: { + 'ja-JP': 'アクティブユーザーのチャートを取得します。' + }, + + params: { + span: { + validator: $.str.or(['day', 'hour']), + desc: { + 'ja-JP': '集計のスパン (day または hour)' + } + }, + + limit: { + validator: $.num.optional.range(1, 500), + default: 30, + desc: { + 'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。' + } + }, + } +}; + +export default define(meta, (ps) => new Promise(async (res, rej) => { + const stats = await activeUsersChart.getChart(ps.span as any, ps.limit); + + res(stats); +})); diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index 20855e513..919b0662a 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -6,6 +6,7 @@ import { packMany } from '../../../../models/note'; import define from '../../define'; import { countIf } from '../../../../prelude/array'; import fetchMeta from '../../../../misc/fetch-meta'; +import activeUsersChart from '../../../../chart/active-users'; export const meta = { desc: { @@ -272,4 +273,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }); res(await packMany(timeline, user)); + + activeUsersChart.update(user); })); diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index 30d2762ad..fd624c10e 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -5,6 +5,7 @@ import { packMany } from '../../../../models/note'; import define from '../../define'; import { countIf } from '../../../../prelude/array'; import fetchMeta from '../../../../misc/fetch-meta'; +import activeUsersChart from '../../../../chart/active-users'; export const meta = { desc: { @@ -161,4 +162,8 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { }); res(await packMany(timeline, user)); + + if (user) { + activeUsersChart.update(user); + } })); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 5604cf291..24ed222e9 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -5,6 +5,7 @@ import { getFriends } from '../../common/get-friends'; import { packMany } from '../../../../models/note'; import define from '../../define'; import { countIf } from '../../../../prelude/array'; +import activeUsersChart from '../../../../chart/active-users'; export const meta = { desc: { @@ -266,4 +267,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { // Serialize res(await packMany(timeline, user)); + + activeUsersChart.update(user); })); diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 18073f3aa..8031746c8 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -23,6 +23,7 @@ import registerHashtag from '../register-hashtag'; import isQuote from '../../misc/is-quote'; import notesChart from '../../chart/notes'; import perUserNotesChart from '../../chart/per-user-notes'; +import activeUsersChart from '../../chart/active-users'; import { erase } from '../../prelude/array'; import insertNoteUnread from './unread'; @@ -196,6 +197,8 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< // 統計を更新 notesChart.update(note, true); perUserNotesChart.update(user, note, true); + // ローカルユーザーのチャートはタイムライン取得時に更新しているのでリモートユーザーの場合だけでよい + if (isRemoteUser(user)) activeUsersChart.update(user); // Register host if (isRemoteUser(user)) {