@zumito-team/analytics-module 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +4 -1
- package/dist/models/CommandExecuted.d.ts +9 -0
- package/dist/models/CommandExecuted.js +34 -0
- package/dist/models/GuildAnalyticsConfig.d.ts +1 -0
- package/dist/models/GuildAnalyticsConfig.js +3 -0
- package/dist/routes/AdminAnalyticsCommands.js +3 -1
- package/dist/routes/UserPanelAnalyticsCommands.js +3 -3
- package/dist/services/AnalyticsCollector.d.ts +19 -4
- package/dist/services/AnalyticsCollector.js +116 -118
- package/dist/views/admin-analytics-commands.ejs +38 -3
- package/dist/views/user-analytics-commands.ejs +24 -20
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -8,7 +8,34 @@
|
|
|
8
8
|
npm install @zumito-team/analytics-module
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Add to your `zumito.config.ts
|
|
11
|
+
Add to your `zumito.config.ts`:
|
|
12
|
+
|
|
13
|
+
### Modules (recommended)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineConfig } from 'zumito-framework';
|
|
17
|
+
import { analyticsModule } from '@zumito-team/analytics-module';
|
|
18
|
+
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
modules: [
|
|
21
|
+
analyticsModule({
|
|
22
|
+
defaultTrackCommandPerformance: true,
|
|
23
|
+
}),
|
|
24
|
+
]
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or with plain string:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { defineConfig } from 'zumito-framework';
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
modules: ['@zumito-team/analytics-module']
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Legacy bundles
|
|
12
39
|
|
|
13
40
|
```ts
|
|
14
41
|
{
|
package/dist/config.d.ts
CHANGED
|
@@ -7,5 +7,6 @@ export declare class AnalyticsModuleConfig {
|
|
|
7
7
|
static defaultTrackCommands: boolean;
|
|
8
8
|
static defaultTrackCommandPerformance: boolean;
|
|
9
9
|
static defaultTrackPerChannelVoice: boolean;
|
|
10
|
+
static defaultTrackPerChannelMessages: boolean;
|
|
10
11
|
static configure(opts: Partial<typeof AnalyticsModuleConfig>): void;
|
|
11
12
|
}
|
package/dist/config.js
CHANGED
|
@@ -11,3 +11,4 @@ AnalyticsModuleConfig.defaultTrackMembers = true;
|
|
|
11
11
|
AnalyticsModuleConfig.defaultTrackCommands = true;
|
|
12
12
|
AnalyticsModuleConfig.defaultTrackCommandPerformance = false;
|
|
13
13
|
AnalyticsModuleConfig.defaultTrackPerChannelVoice = false;
|
|
14
|
+
AnalyticsModuleConfig.defaultTrackPerChannelMessages = false;
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,18 @@ export declare class AnalyticsModule extends Module {
|
|
|
8
8
|
initialize(): Promise<void>;
|
|
9
9
|
onAllReady(): Promise<void>;
|
|
10
10
|
}
|
|
11
|
+
export interface AnalyticsModuleEntryConfig {
|
|
12
|
+
defaultRetentionDays?: number;
|
|
13
|
+
cleanupIntervalHours?: number;
|
|
14
|
+
defaultTrackMessages?: boolean;
|
|
15
|
+
defaultTrackVoice?: boolean;
|
|
16
|
+
defaultTrackMembers?: boolean;
|
|
17
|
+
defaultTrackCommands?: boolean;
|
|
18
|
+
defaultTrackCommandPerformance?: boolean;
|
|
19
|
+
defaultTrackPerChannelVoice?: boolean;
|
|
20
|
+
defaultTrackPerChannelMessages?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare const analyticsModule: (config?: AnalyticsModuleEntryConfig) => import("zumito-framework").ModuleEntry;
|
|
11
23
|
export { AnalyticsCollector } from './services/AnalyticsCollector.js';
|
|
12
24
|
export type { CommandExecutedPayload } from './services/AnalyticsCollector.js';
|
|
13
25
|
export { AnalyticsModuleConfig } from './config.js';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import { Module, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import { Module, ServiceContainer, createModuleEntry } from 'zumito-framework';
|
|
3
3
|
import { AnalyticsCollector } from './services/AnalyticsCollector.js';
|
|
4
|
+
import { AnalyticsModuleConfig } from './config.js';
|
|
4
5
|
export class AnalyticsModule extends Module {
|
|
5
6
|
constructor(modulePath = import.meta.url) {
|
|
6
7
|
super(modulePath);
|
|
7
8
|
ServiceContainer.addService(AnalyticsCollector, [], true);
|
|
9
|
+
AnalyticsModuleConfig.configure(this.moduleConfig);
|
|
8
10
|
}
|
|
9
11
|
async initialize() {
|
|
10
12
|
await super.initialize();
|
|
@@ -84,6 +86,7 @@ export class AnalyticsModule extends Module {
|
|
|
84
86
|
AnalyticsModule.moduleName = 'analytics-module';
|
|
85
87
|
AnalyticsModule.dependencies = [];
|
|
86
88
|
AnalyticsModule.optionalDependencies = ['admin-module', 'user-panel-module'];
|
|
89
|
+
export const analyticsModule = createModuleEntry(import.meta.url);
|
|
87
90
|
export { AnalyticsCollector } from './services/AnalyticsCollector.js';
|
|
88
91
|
export { AnalyticsModuleConfig } from './config.js';
|
|
89
92
|
export { GuildDailyStats } from './models/GuildDailyStats.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Collection, Field } from 'zumito-framework';
|
|
8
|
+
let CommandExecuted = class CommandExecuted {
|
|
9
|
+
};
|
|
10
|
+
__decorate([
|
|
11
|
+
Field({ type: 'string', primary: true, unique: true })
|
|
12
|
+
], CommandExecuted.prototype, "id", void 0);
|
|
13
|
+
__decorate([
|
|
14
|
+
Field({ type: 'string' })
|
|
15
|
+
], CommandExecuted.prototype, "guild_id", void 0);
|
|
16
|
+
__decorate([
|
|
17
|
+
Field({ type: 'string' })
|
|
18
|
+
], CommandExecuted.prototype, "command_name", void 0);
|
|
19
|
+
__decorate([
|
|
20
|
+
Field({ type: 'string' })
|
|
21
|
+
], CommandExecuted.prototype, "type", void 0);
|
|
22
|
+
__decorate([
|
|
23
|
+
Field({ type: 'number', default: 0 })
|
|
24
|
+
], CommandExecuted.prototype, "execution_time_ms", void 0);
|
|
25
|
+
__decorate([
|
|
26
|
+
Field({ type: 'boolean', default: false })
|
|
27
|
+
], CommandExecuted.prototype, "error", void 0);
|
|
28
|
+
__decorate([
|
|
29
|
+
Field({ type: 'string' })
|
|
30
|
+
], CommandExecuted.prototype, "executed_at", void 0);
|
|
31
|
+
CommandExecuted = __decorate([
|
|
32
|
+
Collection({ name: 'analytics_commands_executed' })
|
|
33
|
+
], CommandExecuted);
|
|
34
|
+
export { CommandExecuted };
|
|
@@ -31,6 +31,9 @@ __decorate([
|
|
|
31
31
|
__decorate([
|
|
32
32
|
Field({ type: 'boolean', default: false })
|
|
33
33
|
], GuildAnalyticsConfig.prototype, "track_per_channel_voice", void 0);
|
|
34
|
+
__decorate([
|
|
35
|
+
Field({ type: 'boolean', default: false })
|
|
36
|
+
], GuildAnalyticsConfig.prototype, "track_per_channel_messages", void 0);
|
|
34
37
|
__decorate([
|
|
35
38
|
Field({ type: 'number' })
|
|
36
39
|
], GuildAnalyticsConfig.prototype, "retention_days", void 0);
|
|
@@ -18,9 +18,11 @@ export class AdminAnalyticsCommands extends Route {
|
|
|
18
18
|
return res.redirect('/admin/login');
|
|
19
19
|
const daysBack = parseInt(req.query.days) || 7;
|
|
20
20
|
const commandsPerDay = await this.collector.getCommandsPerDay(null, daysBack);
|
|
21
|
+
const commandsPerDayByType = await this.collector.getCommandsPerDayByType(null, daysBack);
|
|
22
|
+
const commandsByType = await this.collector.getCommandsByType(null, daysBack);
|
|
21
23
|
const topCommands = await this.collector.getTopCommands(null, daysBack, 15);
|
|
22
24
|
const slowestCommands = await this.collector.getSlowestCommands(null, daysBack, 10);
|
|
23
|
-
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics-commands.ejs'), { commandsPerDay, topCommands, slowestCommands, daysBack });
|
|
25
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics-commands.ejs'), { commandsPerDay, commandsPerDayByType, commandsByType, topCommands, slowestCommands, daysBack });
|
|
24
26
|
const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
|
|
25
27
|
const view = ServiceContainer.getService(AdminViewService);
|
|
26
28
|
const html = await view.render({
|
|
@@ -37,10 +37,10 @@ export class UserPanelAnalyticsCommands extends Route {
|
|
|
37
37
|
const langMgr = ServiceContainer.getService(UserPanelLanguageManager);
|
|
38
38
|
const { t } = langMgr.getLanguageVariables(req, res);
|
|
39
39
|
const commandsPerDay = await this.collector.getCommandsPerDay(guildId, daysBack);
|
|
40
|
+
const commandsPerDayByType = await this.collector.getCommandsPerDayByType(guildId, daysBack);
|
|
41
|
+
const commandsByType = await this.collector.getCommandsByType(guildId, daysBack);
|
|
40
42
|
const topCommands = await this.collector.getTopCommands(guildId, daysBack, 15);
|
|
41
|
-
const
|
|
42
|
-
const config = await this.collector.getConfig(guildId);
|
|
43
|
-
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics-commands.ejs'), { commandsPerDay, topCommands, slowestCommands: config.track_command_performance ? slowestCommands : null, t, daysBack });
|
|
43
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics-commands.ejs'), { commandsPerDay, commandsPerDayByType, commandsByType, topCommands, t, daysBack });
|
|
44
44
|
const view = ServiceContainer.getService(UserPanelViewService);
|
|
45
45
|
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
46
46
|
res.send(html);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { GuildDailyStats } from '../models/GuildDailyStats.js';
|
|
2
|
-
import { CommandDailyStats } from '../models/CommandDailyStats.js';
|
|
3
2
|
import { VoiceChannelDailyStats } from '../models/VoiceChannelDailyStats.js';
|
|
4
3
|
import { GuildAnalyticsConfig } from '../models/GuildAnalyticsConfig.js';
|
|
5
4
|
import { ChannelMessageStats } from '../models/ChannelMessageStats.js';
|
|
@@ -46,14 +45,30 @@ export declare class AnalyticsCollector {
|
|
|
46
45
|
date: string;
|
|
47
46
|
count: number;
|
|
48
47
|
}[]>;
|
|
48
|
+
private getCommandExecutedRows;
|
|
49
|
+
private getCommandDailyStatsRows;
|
|
49
50
|
getCommandsPerDay(guildId: string | null, daysBack: number): Promise<{
|
|
50
51
|
date: string;
|
|
51
52
|
count: number;
|
|
52
53
|
}[]>;
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
getCommandsPerDayByType(guildId: string | null, daysBack: number): Promise<{
|
|
55
|
+
date: string;
|
|
56
|
+
slash: number;
|
|
57
|
+
prefix: number;
|
|
58
|
+
}[]>;
|
|
59
|
+
getCommandsByType(guildId: string | null, daysBack: number): Promise<{
|
|
60
|
+
slash: number;
|
|
61
|
+
prefix: number;
|
|
62
|
+
}>;
|
|
63
|
+
getTopCommands(guildId: string | null, daysBack: number, limit?: number): Promise<{
|
|
64
|
+
command_name: string;
|
|
65
|
+
usage_count: number;
|
|
66
|
+
}[]>;
|
|
67
|
+
getSlowestCommands(guildId: string | null, daysBack: number, limit?: number): Promise<{
|
|
68
|
+
command_name: string;
|
|
55
69
|
avgExecutionTimeMs: number;
|
|
56
|
-
|
|
70
|
+
total_executions: number;
|
|
71
|
+
}[]>;
|
|
57
72
|
getVoiceChannelStats(guildId: string, daysBack: number): Promise<VoiceChannelDailyStats[]>;
|
|
58
73
|
getChannelMessageStats(guildId: string, daysBack: number): Promise<ChannelMessageStats[]>;
|
|
59
74
|
getConfig(guildId: string): Promise<GuildAnalyticsConfig>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DatabaseManager, ServiceContainer } from 'zumito-framework';
|
|
2
2
|
import { GuildDailyStats } from '../models/GuildDailyStats.js';
|
|
3
3
|
import { CommandDailyStats } from '../models/CommandDailyStats.js';
|
|
4
|
+
import { CommandExecuted } from '../models/CommandExecuted.js';
|
|
4
5
|
import { VoiceChannelDailyStats } from '../models/VoiceChannelDailyStats.js';
|
|
5
6
|
import { GuildAnalyticsConfig } from '../models/GuildAnalyticsConfig.js';
|
|
6
7
|
import { ChannelMessageStats } from '../models/ChannelMessageStats.js';
|
|
@@ -35,6 +36,7 @@ export class AnalyticsCollector {
|
|
|
35
36
|
track_commands: AnalyticsModuleConfig.defaultTrackCommands,
|
|
36
37
|
track_command_performance: AnalyticsModuleConfig.defaultTrackCommandPerformance,
|
|
37
38
|
track_per_channel_voice: AnalyticsModuleConfig.defaultTrackPerChannelVoice,
|
|
39
|
+
track_per_channel_messages: AnalyticsModuleConfig.defaultTrackPerChannelMessages,
|
|
38
40
|
retention_days: AnalyticsModuleConfig.defaultRetentionDays,
|
|
39
41
|
public_stats_page: false,
|
|
40
42
|
};
|
|
@@ -55,7 +57,7 @@ export class AnalyticsCollector {
|
|
|
55
57
|
this.dailyMessageAuthors.get(authorKey).add(authorId);
|
|
56
58
|
}
|
|
57
59
|
// Track per-channel stats
|
|
58
|
-
if (channelId && config.
|
|
60
|
+
if (channelId && config.track_per_channel_messages) {
|
|
59
61
|
const date = today();
|
|
60
62
|
const chanKey = `${guildId}_${channelId}_${date}`;
|
|
61
63
|
const repo = this.repo(ChannelMessageStats);
|
|
@@ -163,29 +165,17 @@ export class AnalyticsCollector {
|
|
|
163
165
|
return;
|
|
164
166
|
await this.upsertGuildStats(payload.guildId, { command_count: 1 });
|
|
165
167
|
const date = today();
|
|
166
|
-
const id = `${payload.guildId}_${payload.commandName}_${
|
|
167
|
-
const repo = this.repo(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
await repo.insert({
|
|
180
|
-
id,
|
|
181
|
-
guild_id: payload.guildId,
|
|
182
|
-
command_name: payload.commandName,
|
|
183
|
-
date,
|
|
184
|
-
usage_count: 1,
|
|
185
|
-
total_execution_time_ms: executionTime,
|
|
186
|
-
error_count: errorAdd,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
168
|
+
const id = `${payload.guildId}_${payload.commandName}_${Date.now()}`;
|
|
169
|
+
const repo = this.repo(CommandExecuted);
|
|
170
|
+
await repo.insert({
|
|
171
|
+
id,
|
|
172
|
+
guild_id: payload.guildId,
|
|
173
|
+
command_name: payload.commandName,
|
|
174
|
+
type: payload.type,
|
|
175
|
+
execution_time_ms: payload.executionTimeMs,
|
|
176
|
+
error: !payload.success,
|
|
177
|
+
executed_at: date,
|
|
178
|
+
});
|
|
189
179
|
}
|
|
190
180
|
async recordMemberCount(guildId, memberCount) {
|
|
191
181
|
const config = await this.ensuredConfig(guildId);
|
|
@@ -306,125 +296,125 @@ export class AnalyticsCollector {
|
|
|
306
296
|
.map(([date, count]) => ({ date, count }))
|
|
307
297
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
308
298
|
}
|
|
309
|
-
async
|
|
310
|
-
const repo = this.repo(
|
|
299
|
+
async getCommandExecutedRows(guildId, daysBack) {
|
|
300
|
+
const repo = this.repo(CommandExecuted);
|
|
311
301
|
const dateMin = this.dateDaysAgo(daysBack);
|
|
312
|
-
let rows
|
|
302
|
+
let rows = await repo.query()
|
|
303
|
+
.where('executed_at', 'gte', dateMin)
|
|
304
|
+
.exec();
|
|
313
305
|
if (guildId) {
|
|
314
|
-
rows =
|
|
315
|
-
.where('guild_id', 'eq', guildId)
|
|
316
|
-
.where('date', 'gte', dateMin)
|
|
317
|
-
.sort('date', 'asc')
|
|
318
|
-
.exec();
|
|
306
|
+
rows = rows.filter(r => r.guild_id === guildId);
|
|
319
307
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
308
|
+
return rows;
|
|
309
|
+
}
|
|
310
|
+
async getCommandDailyStatsRows(guildId, daysBack) {
|
|
311
|
+
const repo = this.repo(CommandDailyStats);
|
|
312
|
+
const dateMin = this.dateDaysAgo(daysBack);
|
|
313
|
+
let rows = await repo.query()
|
|
314
|
+
.where('date', 'gte', dateMin)
|
|
315
|
+
.exec();
|
|
316
|
+
if (guildId) {
|
|
317
|
+
rows = rows.filter(r => r.guild_id === guildId);
|
|
325
318
|
}
|
|
319
|
+
return rows;
|
|
320
|
+
}
|
|
321
|
+
async getCommandsPerDay(guildId, daysBack) {
|
|
322
|
+
const newRows = await this.getCommandExecutedRows(guildId, daysBack);
|
|
323
|
+
const oldRows = await this.getCommandDailyStatsRows(guildId, daysBack);
|
|
326
324
|
const byDate = new Map();
|
|
327
|
-
for (const row of
|
|
328
|
-
byDate.set(row.
|
|
325
|
+
for (const row of newRows) {
|
|
326
|
+
byDate.set(row.executed_at, (byDate.get(row.executed_at) || 0) + 1);
|
|
327
|
+
}
|
|
328
|
+
for (const row of oldRows) {
|
|
329
|
+
byDate.set(row.date, (byDate.get(row.date) || 0) + row.usage_count);
|
|
329
330
|
}
|
|
330
331
|
return Array.from(byDate.entries())
|
|
331
332
|
.map(([date, count]) => ({ date, count }))
|
|
332
333
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
333
334
|
}
|
|
334
|
-
async
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
.
|
|
342
|
-
|
|
335
|
+
async getCommandsPerDayByType(guildId, daysBack) {
|
|
336
|
+
const newRows = await this.getCommandExecutedRows(guildId, daysBack);
|
|
337
|
+
const oldRows = await this.getCommandDailyStatsRows(guildId, daysBack);
|
|
338
|
+
const byDate = new Map();
|
|
339
|
+
for (const row of newRows) {
|
|
340
|
+
const entry = byDate.get(row.executed_at) || { slash: 0, prefix: 0 };
|
|
341
|
+
if (row.type === 'slash')
|
|
342
|
+
entry.slash += 1;
|
|
343
|
+
else
|
|
344
|
+
entry.prefix += 1;
|
|
345
|
+
byDate.set(row.executed_at, entry);
|
|
343
346
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
347
|
+
for (const row of oldRows) {
|
|
348
|
+
const entry = byDate.get(row.date) || { slash: 0, prefix: 0 };
|
|
349
|
+
entry.prefix += row.usage_count;
|
|
350
|
+
byDate.set(row.date, entry);
|
|
351
|
+
}
|
|
352
|
+
return Array.from(byDate.entries())
|
|
353
|
+
.map(([date, counts]) => ({ date, ...counts }))
|
|
354
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
355
|
+
}
|
|
356
|
+
async getCommandsByType(guildId, daysBack) {
|
|
357
|
+
const newRows = await this.getCommandExecutedRows(guildId, daysBack);
|
|
358
|
+
const oldRows = await this.getCommandDailyStatsRows(guildId, daysBack);
|
|
359
|
+
let slash = 0;
|
|
360
|
+
let prefix = 0;
|
|
361
|
+
for (const row of newRows) {
|
|
362
|
+
if (row.type === 'slash')
|
|
363
|
+
slash++;
|
|
364
|
+
else
|
|
365
|
+
prefix++;
|
|
348
366
|
}
|
|
367
|
+
for (const row of oldRows) {
|
|
368
|
+
prefix += row.usage_count;
|
|
369
|
+
}
|
|
370
|
+
return { slash, prefix };
|
|
371
|
+
}
|
|
372
|
+
async getTopCommands(guildId, daysBack, limit = 10) {
|
|
373
|
+
const newRows = await this.getCommandExecutedRows(guildId, daysBack);
|
|
374
|
+
const oldRows = await this.getCommandDailyStatsRows(guildId, daysBack);
|
|
349
375
|
const aggregated = new Map();
|
|
350
|
-
for (const row of
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
existing.total_execution_time_ms += row.total_execution_time_ms;
|
|
356
|
-
existing.error_count += row.error_count;
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
aggregated.set(key, {
|
|
360
|
-
id: key,
|
|
361
|
-
guild_id: guildId || 'global',
|
|
362
|
-
command_name: row.command_name,
|
|
363
|
-
date: '',
|
|
364
|
-
usage_count: row.usage_count,
|
|
365
|
-
total_execution_time_ms: row.total_execution_time_ms,
|
|
366
|
-
error_count: row.error_count,
|
|
367
|
-
});
|
|
368
|
-
}
|
|
376
|
+
for (const row of newRows) {
|
|
377
|
+
aggregated.set(row.command_name, (aggregated.get(row.command_name) || 0) + 1);
|
|
378
|
+
}
|
|
379
|
+
for (const row of oldRows) {
|
|
380
|
+
aggregated.set(row.command_name, (aggregated.get(row.command_name) || 0) + row.usage_count);
|
|
369
381
|
}
|
|
370
|
-
return Array.from(aggregated.
|
|
382
|
+
return Array.from(aggregated.entries())
|
|
383
|
+
.map(([command_name, usage_count]) => ({ command_name, usage_count }))
|
|
371
384
|
.sort((a, b) => b.usage_count - a.usage_count)
|
|
372
385
|
.slice(0, limit);
|
|
373
386
|
}
|
|
374
387
|
async getSlowestCommands(guildId, daysBack, limit = 10) {
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
let rows;
|
|
378
|
-
if (guildId) {
|
|
379
|
-
rows = await repo.query()
|
|
380
|
-
.where('guild_id', 'eq', guildId)
|
|
381
|
-
.where('date', 'gte', dateMin)
|
|
382
|
-
.exec();
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
rows = await repo.query()
|
|
386
|
-
.where('date', 'gte', dateMin)
|
|
387
|
-
.exec();
|
|
388
|
-
}
|
|
388
|
+
const newRows = await this.getCommandExecutedRows(guildId, daysBack);
|
|
389
|
+
const oldRows = await this.getCommandDailyStatsRows(guildId, daysBack);
|
|
389
390
|
const aggregated = new Map();
|
|
390
|
-
for (const row of
|
|
391
|
-
const
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
e.totalTime += row.total_execution_time_ms;
|
|
396
|
-
e.errors += row.error_count;
|
|
391
|
+
for (const row of newRows) {
|
|
392
|
+
const existing = aggregated.get(row.command_name);
|
|
393
|
+
if (existing) {
|
|
394
|
+
existing.totalTime += row.execution_time_ms;
|
|
395
|
+
existing.count += 1;
|
|
397
396
|
}
|
|
398
397
|
else {
|
|
399
|
-
aggregated.set(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
398
|
+
aggregated.set(row.command_name, { totalTime: row.execution_time_ms, count: 1 });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
for (const row of oldRows) {
|
|
402
|
+
const existing = aggregated.get(row.command_name);
|
|
403
|
+
if (existing) {
|
|
404
|
+
existing.totalTime += row.total_execution_time_ms;
|
|
405
|
+
existing.count += row.usage_count;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
aggregated.set(row.command_name, { totalTime: row.total_execution_time_ms, count: row.usage_count });
|
|
404
409
|
}
|
|
405
410
|
}
|
|
406
411
|
return Array.from(aggregated.entries())
|
|
407
|
-
.map(([
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
date: '',
|
|
412
|
-
usage_count: data.usage_count,
|
|
413
|
-
total_execution_time_ms: data.totalTime,
|
|
414
|
-
error_count: data.errors,
|
|
415
|
-
avgExecutionTimeMs: data.usage_count > 0 ? Math.round(data.totalTime / data.usage_count) : 0,
|
|
412
|
+
.map(([command_name, data]) => ({
|
|
413
|
+
command_name,
|
|
414
|
+
avgExecutionTimeMs: data.count > 0 ? Math.round(data.totalTime / data.count) : 0,
|
|
415
|
+
total_executions: data.count,
|
|
416
416
|
}))
|
|
417
|
-
.sort((a, b) =>
|
|
418
|
-
const aHasPerf = a.total_execution_time_ms > 0;
|
|
419
|
-
const bHasPerf = b.total_execution_time_ms > 0;
|
|
420
|
-
if (aHasPerf && bHasPerf)
|
|
421
|
-
return b.avgExecutionTimeMs - a.avgExecutionTimeMs;
|
|
422
|
-
if (aHasPerf)
|
|
423
|
-
return -1;
|
|
424
|
-
if (bHasPerf)
|
|
425
|
-
return 1;
|
|
426
|
-
return b.usage_count - a.usage_count;
|
|
427
|
-
})
|
|
417
|
+
.sort((a, b) => b.avgExecutionTimeMs - a.avgExecutionTimeMs)
|
|
428
418
|
.slice(0, limit);
|
|
429
419
|
}
|
|
430
420
|
async getVoiceChannelStats(guildId, daysBack) {
|
|
@@ -492,6 +482,14 @@ export class AnalyticsCollector {
|
|
|
492
482
|
for (const cmd of oldCmds) {
|
|
493
483
|
await cmdRepo.delete({ id: cmd.id });
|
|
494
484
|
}
|
|
485
|
+
const cmdExRepo = this.repo(CommandExecuted);
|
|
486
|
+
const oldExecs = await cmdExRepo.query()
|
|
487
|
+
.where('guild_id', 'eq', guildId)
|
|
488
|
+
.where('executed_at', 'lt', cutoffDate)
|
|
489
|
+
.exec();
|
|
490
|
+
for (const exec of oldExecs) {
|
|
491
|
+
await cmdExRepo.delete({ id: exec.id });
|
|
492
|
+
}
|
|
495
493
|
const voiceRepo = this.repo(VoiceChannelDailyStats);
|
|
496
494
|
const oldVoice = await voiceRepo.query()
|
|
497
495
|
.where('guild_id', 'eq', guildId)
|
|
@@ -7,11 +7,25 @@
|
|
|
7
7
|
<a href="?days=90" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">90d</a>
|
|
8
8
|
</div>
|
|
9
9
|
</div>
|
|
10
|
+
<div class="grid grid-cols-2 gap-4 mb-6">
|
|
11
|
+
<div class="card text-center">
|
|
12
|
+
<div class="text-3xl font-bold text-blue-400"><%= commandsByType.slash %></div>
|
|
13
|
+
<div class="text-sm text-discord-foreground/60">Slash Commands</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card text-center">
|
|
16
|
+
<div class="text-3xl font-bold text-green-400"><%= commandsByType.prefix %></div>
|
|
17
|
+
<div class="text-sm text-discord-foreground/60">Prefix Commands</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
10
20
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
11
21
|
<div class="card">
|
|
12
22
|
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Comandos por Dia</h3>
|
|
13
23
|
<canvas id="chartCommands" width="400" height="200"></canvas>
|
|
14
24
|
</div>
|
|
25
|
+
<div class="card">
|
|
26
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Comandos por Tipo</h3>
|
|
27
|
+
<canvas id="chartCommandsType" width="400" height="200"></canvas>
|
|
28
|
+
</div>
|
|
15
29
|
<div class="card">
|
|
16
30
|
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Top Comandos</h3>
|
|
17
31
|
<canvas id="chartTop" width="400" height="200"></canvas>
|
|
@@ -30,15 +44,36 @@ new Chart(document.getElementById('chartCommands'), {
|
|
|
30
44
|
type: 'bar',
|
|
31
45
|
data: {
|
|
32
46
|
labels: <%- JSON.stringify(commandsPerDay.map(v => v.date)) %>,
|
|
33
|
-
datasets: [{
|
|
47
|
+
datasets: [{
|
|
48
|
+
label: 'Slash',
|
|
49
|
+
data: <%- JSON.stringify(commandsPerDayByType.map(v => v.slash)) %>,
|
|
50
|
+
backgroundColor: '#5865f2',
|
|
51
|
+
borderRadius: 4
|
|
52
|
+
}, {
|
|
53
|
+
label: 'Prefix',
|
|
54
|
+
data: <%- JSON.stringify(commandsPerDayByType.map(v => v.prefix)) %>,
|
|
55
|
+
backgroundColor: '#57f287',
|
|
56
|
+
borderRadius: 4
|
|
57
|
+
}]
|
|
58
|
+
},
|
|
59
|
+
options: { responsive: true, scales: { x: { stacked: true }, y: { stacked: true } } }
|
|
60
|
+
});
|
|
61
|
+
new Chart(document.getElementById('chartCommandsType'), {
|
|
62
|
+
type: 'doughnut',
|
|
63
|
+
data: {
|
|
64
|
+
labels: ['Slash', 'Prefix'],
|
|
65
|
+
datasets: [{
|
|
66
|
+
data: [<%= commandsByType.slash %>, <%= commandsByType.prefix %>],
|
|
67
|
+
backgroundColor: ['#5865f2', '#57f287']
|
|
68
|
+
}]
|
|
34
69
|
},
|
|
35
|
-
options: { responsive: true, plugins: { legend: {
|
|
70
|
+
options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
|
|
36
71
|
});
|
|
37
72
|
new Chart(document.getElementById('chartTop'), {
|
|
38
73
|
type: 'bar',
|
|
39
74
|
data: {
|
|
40
75
|
labels: <%- JSON.stringify(topCommands.map(v => v.command_name)) %>,
|
|
41
|
-
datasets: [{ label: 'Usos', data: <%- JSON.stringify(topCommands.map(v => v.usage_count)) %>, backgroundColor: topCommands.map((_, i) => 'rgba(88,101,242,' + Math.max(0.3, 1 - i * 0.08) + ')')
|
|
76
|
+
datasets: [{ label: 'Usos', data: <%- JSON.stringify(topCommands.map(v => v.usage_count)) %>, backgroundColor: <%- JSON.stringify(topCommands.map((_, i) => 'rgba(88,101,242,' + Math.max(0.3, 1 - i * 0.08) + ')')) %>, borderRadius: 4 }]
|
|
42
77
|
},
|
|
43
78
|
options: { indexAxis: 'y', responsive: true, plugins: { legend: { display: false } } }
|
|
44
79
|
});
|
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
<a href="?days=90" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">90d</a>
|
|
8
8
|
</div>
|
|
9
9
|
</div>
|
|
10
|
+
<div class="grid grid-cols-2 gap-4 mb-6">
|
|
11
|
+
<div class="card text-center">
|
|
12
|
+
<div class="text-3xl font-bold text-blue-400"><%= commandsByType.slash %></div>
|
|
13
|
+
<div class="text-sm text-discord-foreground/60">Slash Commands</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card text-center">
|
|
16
|
+
<div class="text-3xl font-bold text-green-400"><%= commandsByType.prefix %></div>
|
|
17
|
+
<div class="text-sm text-discord-foreground/60">Prefix Commands</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
10
20
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
11
21
|
<div class="card">
|
|
12
22
|
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.commandsPerDay') %></h3>
|
|
@@ -17,39 +27,33 @@
|
|
|
17
27
|
<canvas id="chartTop" width="400" height="200"></canvas>
|
|
18
28
|
</div>
|
|
19
29
|
</div>
|
|
20
|
-
<% if (slowestCommands && slowestCommands.length > 0) { %>
|
|
21
|
-
<div class="card mt-6">
|
|
22
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.slowestCommands') %></h3>
|
|
23
|
-
<canvas id="chartSlow" width="700" height="250"></canvas>
|
|
24
|
-
</div>
|
|
25
|
-
<% } %>
|
|
26
30
|
</div>
|
|
27
31
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
28
32
|
<script>
|
|
29
33
|
new Chart(document.getElementById('chartCommands'), {
|
|
30
34
|
type: 'bar',
|
|
31
35
|
data: {
|
|
32
|
-
labels: <%- JSON.stringify(
|
|
33
|
-
datasets: [{
|
|
36
|
+
labels: <%- JSON.stringify(commandsPerDayByType.map(v => v.date)) %>,
|
|
37
|
+
datasets: [{
|
|
38
|
+
label: 'Slash',
|
|
39
|
+
data: <%- JSON.stringify(commandsPerDayByType.map(v => v.slash)) %>,
|
|
40
|
+
backgroundColor: '#5865f2',
|
|
41
|
+
borderRadius: 4
|
|
42
|
+
}, {
|
|
43
|
+
label: 'Prefix',
|
|
44
|
+
data: <%- JSON.stringify(commandsPerDayByType.map(v => v.prefix)) %>,
|
|
45
|
+
backgroundColor: '#57f287',
|
|
46
|
+
borderRadius: 4
|
|
47
|
+
}]
|
|
34
48
|
},
|
|
35
|
-
options: { responsive: true,
|
|
49
|
+
options: { responsive: true, scales: { x: { stacked: true }, y: { stacked: true } } }
|
|
36
50
|
});
|
|
37
51
|
new Chart(document.getElementById('chartTop'), {
|
|
38
52
|
type: 'bar',
|
|
39
53
|
data: {
|
|
40
54
|
labels: <%- JSON.stringify(topCommands.map(v => v.command_name)) %>,
|
|
41
|
-
datasets: [{ label: '<%= t("analytics.usageCount") %>', data: <%- JSON.stringify(topCommands.map(v => v.usage_count)) %>, backgroundColor: topCommands.map((_, i) => 'rgba(88,101,242,' + Math.max(0.3, 1 - i * 0.08) + ')')
|
|
55
|
+
datasets: [{ label: '<%= t("analytics.usageCount") %>', data: <%- JSON.stringify(topCommands.map(v => v.usage_count)) %>, backgroundColor: <%- JSON.stringify(topCommands.map((_, i) => 'rgba(88,101,242,' + Math.max(0.3, 1 - i * 0.08) + ')')) %>, borderRadius: 4 }]
|
|
42
56
|
},
|
|
43
57
|
options: { indexAxis: 'y', responsive: true, plugins: { legend: { display: false } } }
|
|
44
58
|
});
|
|
45
|
-
<% if (slowestCommands && slowestCommands.length > 0) { %>
|
|
46
|
-
new Chart(document.getElementById('chartSlow'), {
|
|
47
|
-
type: 'bar',
|
|
48
|
-
data: {
|
|
49
|
-
labels: <%- JSON.stringify(slowestCommands.map(v => v.command_name)) %>,
|
|
50
|
-
datasets: [{ label: 'ms', data: <%- JSON.stringify(slowestCommands.map(v => v.avgExecutionTimeMs)) %>, backgroundColor: '#fee75c', borderRadius: 4 }]
|
|
51
|
-
},
|
|
52
|
-
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { title: { display: true, text: 'ms' } } } }
|
|
53
|
-
});
|
|
54
|
-
<% } %>
|
|
55
59
|
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zumito-team/analytics-module",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Server analytics module with configurable tracking and optional panel integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,14 +13,19 @@
|
|
|
13
13
|
"build": "tsc -p tsconfig.json && cp -r translations dist/ && cp -r views dist/"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"ejs": "^3.1.10",
|
|
17
|
+
"reflect-metadata": "^0.2.2"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
16
20
|
"@zumito-team/admin-module": "^1.8.0",
|
|
17
21
|
"@zumito-team/user-panel-module": "^0.10.0",
|
|
18
|
-
"
|
|
19
|
-
"reflect-metadata": "^0.2.2",
|
|
20
|
-
"zumito-framework": "^1.22.2"
|
|
22
|
+
"zumito-framework": "^1.24.0"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
23
25
|
"@types/ejs": "^3.1.5",
|
|
24
|
-
"
|
|
26
|
+
"@zumito-team/admin-module": "^1.8.0",
|
|
27
|
+
"@zumito-team/user-panel-module": "^0.10.0",
|
|
28
|
+
"typescript": "^5.9.3",
|
|
29
|
+
"zumito-framework": "^1.24.0"
|
|
25
30
|
}
|
|
26
31
|
}
|