@zumito-team/analytics-module 0.7.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/events/discord/MessageCreate.js +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +5 -1
- package/dist/models/ChannelMessageStats.d.ts +8 -0
- package/dist/models/ChannelMessageStats.js +31 -0
- 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/routes/UserPanelAnalyticsMessages.js +2 -1
- package/dist/services/AnalyticsCollector.d.ts +24 -5
- package/dist/services/AnalyticsCollector.js +164 -108
- package/dist/views/admin-analytics-commands.ejs +38 -3
- package/dist/views/user-analytics-commands.ejs +24 -20
- package/dist/views/user-analytics-messages.ejs +18 -2
- 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,10 +8,23 @@ 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';
|
|
14
26
|
export { GuildDailyStats } from './models/GuildDailyStats.js';
|
|
15
27
|
export { CommandDailyStats } from './models/CommandDailyStats.js';
|
|
16
28
|
export { VoiceChannelDailyStats } from './models/VoiceChannelDailyStats.js';
|
|
29
|
+
export { ChannelMessageStats } from './models/ChannelMessageStats.js';
|
|
17
30
|
export { GuildAnalyticsConfig } from './models/GuildAnalyticsConfig.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,9 +86,11 @@ 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';
|
|
90
93
|
export { CommandDailyStats } from './models/CommandDailyStats.js';
|
|
91
94
|
export { VoiceChannelDailyStats } from './models/VoiceChannelDailyStats.js';
|
|
95
|
+
export { ChannelMessageStats } from './models/ChannelMessageStats.js';
|
|
92
96
|
export { GuildAnalyticsConfig } from './models/GuildAnalyticsConfig.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
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 ChannelMessageStats = class ChannelMessageStats {
|
|
9
|
+
};
|
|
10
|
+
__decorate([
|
|
11
|
+
Field({ type: 'string', primary: true, unique: true })
|
|
12
|
+
], ChannelMessageStats.prototype, "id", void 0);
|
|
13
|
+
__decorate([
|
|
14
|
+
Field({ type: 'string' })
|
|
15
|
+
], ChannelMessageStats.prototype, "guild_id", void 0);
|
|
16
|
+
__decorate([
|
|
17
|
+
Field({ type: 'string' })
|
|
18
|
+
], ChannelMessageStats.prototype, "channel_id", void 0);
|
|
19
|
+
__decorate([
|
|
20
|
+
Field({ type: 'string' })
|
|
21
|
+
], ChannelMessageStats.prototype, "date", void 0);
|
|
22
|
+
__decorate([
|
|
23
|
+
Field({ type: 'number', default: 0 })
|
|
24
|
+
], ChannelMessageStats.prototype, "message_count", void 0);
|
|
25
|
+
__decorate([
|
|
26
|
+
Field({ type: 'number', default: 0 })
|
|
27
|
+
], ChannelMessageStats.prototype, "unique_authors", void 0);
|
|
28
|
+
ChannelMessageStats = __decorate([
|
|
29
|
+
Collection({ name: 'analytics_channel_message_stats' })
|
|
30
|
+
], ChannelMessageStats);
|
|
31
|
+
export { ChannelMessageStats };
|
|
@@ -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);
|
|
@@ -43,7 +43,8 @@ export class UserPanelAnalyticsMessages extends Route {
|
|
|
43
43
|
totalMessages += s.message_count;
|
|
44
44
|
messagesPerDay.push({ date: s.date, count: s.message_count });
|
|
45
45
|
}
|
|
46
|
-
const
|
|
46
|
+
const channelMessages = await this.collector.getChannelMessageStats(guildId, daysBack);
|
|
47
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics-messages.ejs'), { totalMessages, messagesPerDay, channelMessages, t, daysBack });
|
|
47
48
|
const view = ServiceContainer.getService(UserPanelViewService);
|
|
48
49
|
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
49
50
|
res.send(html);
|
|
@@ -1,7 +1,7 @@
|
|
|
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';
|
|
4
|
+
import { ChannelMessageStats } from '../models/ChannelMessageStats.js';
|
|
5
5
|
export interface CommandExecutedPayload {
|
|
6
6
|
guildId: string;
|
|
7
7
|
commandName: string;
|
|
@@ -13,11 +13,13 @@ export declare class AnalyticsCollector {
|
|
|
13
13
|
private db;
|
|
14
14
|
private voiceSessions;
|
|
15
15
|
private dailyVoiceUsers;
|
|
16
|
+
private dailyMessageAuthors;
|
|
17
|
+
private dailyChannelMessageAuthors;
|
|
16
18
|
private cleanupTimer;
|
|
17
19
|
constructor(db?: any);
|
|
18
20
|
private repo;
|
|
19
21
|
private ensuredConfig;
|
|
20
|
-
recordMessage(guildId: string): Promise<void>;
|
|
22
|
+
recordMessage(guildId: string, channelId?: string, authorId?: string): Promise<void>;
|
|
21
23
|
recordMemberJoin(guildId: string): Promise<void>;
|
|
22
24
|
recordMemberLeave(guildId: string): Promise<void>;
|
|
23
25
|
recordVoiceJoin(guildId: string, channelId: string, userId: string): Promise<void>;
|
|
@@ -43,15 +45,32 @@ export declare class AnalyticsCollector {
|
|
|
43
45
|
date: string;
|
|
44
46
|
count: number;
|
|
45
47
|
}[]>;
|
|
48
|
+
private getCommandExecutedRows;
|
|
49
|
+
private getCommandDailyStatsRows;
|
|
46
50
|
getCommandsPerDay(guildId: string | null, daysBack: number): Promise<{
|
|
47
51
|
date: string;
|
|
48
52
|
count: number;
|
|
49
53
|
}[]>;
|
|
50
|
-
|
|
51
|
-
|
|
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;
|
|
52
69
|
avgExecutionTimeMs: number;
|
|
53
|
-
|
|
70
|
+
total_executions: number;
|
|
71
|
+
}[]>;
|
|
54
72
|
getVoiceChannelStats(guildId: string, daysBack: number): Promise<VoiceChannelDailyStats[]>;
|
|
73
|
+
getChannelMessageStats(guildId: string, daysBack: number): Promise<ChannelMessageStats[]>;
|
|
55
74
|
getConfig(guildId: string): Promise<GuildAnalyticsConfig>;
|
|
56
75
|
updateConfig(guildId: string, partial: Partial<GuildAnalyticsConfig>): Promise<void>;
|
|
57
76
|
runCleanup(): Promise<void>;
|
|
@@ -1,8 +1,10 @@
|
|
|
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';
|
|
7
|
+
import { ChannelMessageStats } from '../models/ChannelMessageStats.js';
|
|
6
8
|
import { AnalyticsModuleConfig } from '../config.js';
|
|
7
9
|
function today() {
|
|
8
10
|
return new Date().toISOString().slice(0, 10);
|
|
@@ -12,6 +14,8 @@ export class AnalyticsCollector {
|
|
|
12
14
|
this.db = db;
|
|
13
15
|
this.voiceSessions = new Map();
|
|
14
16
|
this.dailyVoiceUsers = new Map();
|
|
17
|
+
this.dailyMessageAuthors = new Map();
|
|
18
|
+
this.dailyChannelMessageAuthors = new Map();
|
|
15
19
|
this.cleanupTimer = null;
|
|
16
20
|
}
|
|
17
21
|
repo(name) {
|
|
@@ -32,17 +36,55 @@ export class AnalyticsCollector {
|
|
|
32
36
|
track_commands: AnalyticsModuleConfig.defaultTrackCommands,
|
|
33
37
|
track_command_performance: AnalyticsModuleConfig.defaultTrackCommandPerformance,
|
|
34
38
|
track_per_channel_voice: AnalyticsModuleConfig.defaultTrackPerChannelVoice,
|
|
39
|
+
track_per_channel_messages: AnalyticsModuleConfig.defaultTrackPerChannelMessages,
|
|
35
40
|
retention_days: AnalyticsModuleConfig.defaultRetentionDays,
|
|
36
41
|
public_stats_page: false,
|
|
37
42
|
};
|
|
38
43
|
await repo.insert(defaults);
|
|
39
44
|
return defaults;
|
|
40
45
|
}
|
|
41
|
-
async recordMessage(guildId) {
|
|
46
|
+
async recordMessage(guildId, channelId, authorId) {
|
|
42
47
|
const config = await this.ensuredConfig(guildId);
|
|
43
48
|
if (!config.enabled || !config.track_messages)
|
|
44
49
|
return;
|
|
45
50
|
await this.upsertGuildStats(guildId, { message_count: 1 });
|
|
51
|
+
// Track unique authors per day
|
|
52
|
+
if (authorId) {
|
|
53
|
+
const authorKey = `${guildId}_${today()}`;
|
|
54
|
+
if (!this.dailyMessageAuthors.has(authorKey)) {
|
|
55
|
+
this.dailyMessageAuthors.set(authorKey, new Set());
|
|
56
|
+
}
|
|
57
|
+
this.dailyMessageAuthors.get(authorKey).add(authorId);
|
|
58
|
+
}
|
|
59
|
+
// Track per-channel stats
|
|
60
|
+
if (channelId && config.track_per_channel_messages) {
|
|
61
|
+
const date = today();
|
|
62
|
+
const chanKey = `${guildId}_${channelId}_${date}`;
|
|
63
|
+
const repo = this.repo(ChannelMessageStats);
|
|
64
|
+
const existing = await repo.findOne({ id: chanKey });
|
|
65
|
+
if (authorId) {
|
|
66
|
+
if (!this.dailyChannelMessageAuthors.has(chanKey)) {
|
|
67
|
+
this.dailyChannelMessageAuthors.set(chanKey, new Set());
|
|
68
|
+
}
|
|
69
|
+
this.dailyChannelMessageAuthors.get(chanKey).add(authorId);
|
|
70
|
+
}
|
|
71
|
+
if (existing) {
|
|
72
|
+
await repo.update({ id: chanKey }, {
|
|
73
|
+
message_count: existing.message_count + 1,
|
|
74
|
+
unique_authors: this.dailyChannelMessageAuthors.get(chanKey)?.size || 0,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
await repo.insert({
|
|
79
|
+
id: chanKey,
|
|
80
|
+
guild_id: guildId,
|
|
81
|
+
channel_id: channelId,
|
|
82
|
+
date,
|
|
83
|
+
message_count: 1,
|
|
84
|
+
unique_authors: 0,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
46
88
|
}
|
|
47
89
|
async recordMemberJoin(guildId) {
|
|
48
90
|
const config = await this.ensuredConfig(guildId);
|
|
@@ -123,29 +165,17 @@ export class AnalyticsCollector {
|
|
|
123
165
|
return;
|
|
124
166
|
await this.upsertGuildStats(payload.guildId, { command_count: 1 });
|
|
125
167
|
const date = today();
|
|
126
|
-
const id = `${payload.guildId}_${payload.commandName}_${
|
|
127
|
-
const repo = this.repo(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
await repo.insert({
|
|
140
|
-
id,
|
|
141
|
-
guild_id: payload.guildId,
|
|
142
|
-
command_name: payload.commandName,
|
|
143
|
-
date,
|
|
144
|
-
usage_count: 1,
|
|
145
|
-
total_execution_time_ms: executionTime,
|
|
146
|
-
error_count: errorAdd,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
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
|
+
});
|
|
149
179
|
}
|
|
150
180
|
async recordMemberCount(guildId, memberCount) {
|
|
151
181
|
const config = await this.ensuredConfig(guildId);
|
|
@@ -266,115 +296,124 @@ export class AnalyticsCollector {
|
|
|
266
296
|
.map(([date, count]) => ({ date, count }))
|
|
267
297
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
268
298
|
}
|
|
269
|
-
async
|
|
270
|
-
const repo = this.repo(
|
|
299
|
+
async getCommandExecutedRows(guildId, daysBack) {
|
|
300
|
+
const repo = this.repo(CommandExecuted);
|
|
271
301
|
const dateMin = this.dateDaysAgo(daysBack);
|
|
272
|
-
let rows
|
|
302
|
+
let rows = await repo.query()
|
|
303
|
+
.where('executed_at', 'gte', dateMin)
|
|
304
|
+
.exec();
|
|
273
305
|
if (guildId) {
|
|
274
|
-
rows =
|
|
275
|
-
.where('guild_id', 'eq', guildId)
|
|
276
|
-
.where('date', 'gte', dateMin)
|
|
277
|
-
.sort('date', 'asc')
|
|
278
|
-
.exec();
|
|
306
|
+
rows = rows.filter(r => r.guild_id === guildId);
|
|
279
307
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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);
|
|
285
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);
|
|
286
324
|
const byDate = new Map();
|
|
287
|
-
for (const row of
|
|
288
|
-
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);
|
|
289
330
|
}
|
|
290
331
|
return Array.from(byDate.entries())
|
|
291
332
|
.map(([date, count]) => ({ date, count }))
|
|
292
333
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
293
334
|
}
|
|
294
|
-
async
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
.
|
|
302
|
-
|
|
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);
|
|
303
346
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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++;
|
|
308
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);
|
|
309
375
|
const aggregated = new Map();
|
|
310
|
-
for (const row of
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
existing.total_execution_time_ms += row.total_execution_time_ms;
|
|
316
|
-
existing.error_count += row.error_count;
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
aggregated.set(key, {
|
|
320
|
-
id: key,
|
|
321
|
-
guild_id: guildId || 'global',
|
|
322
|
-
command_name: row.command_name,
|
|
323
|
-
date: '',
|
|
324
|
-
usage_count: row.usage_count,
|
|
325
|
-
total_execution_time_ms: row.total_execution_time_ms,
|
|
326
|
-
error_count: row.error_count,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
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);
|
|
329
381
|
}
|
|
330
|
-
return Array.from(aggregated.
|
|
382
|
+
return Array.from(aggregated.entries())
|
|
383
|
+
.map(([command_name, usage_count]) => ({ command_name, usage_count }))
|
|
331
384
|
.sort((a, b) => b.usage_count - a.usage_count)
|
|
332
385
|
.slice(0, limit);
|
|
333
386
|
}
|
|
334
387
|
async getSlowestCommands(guildId, daysBack, limit = 10) {
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
let rows;
|
|
338
|
-
if (guildId) {
|
|
339
|
-
rows = await repo.query()
|
|
340
|
-
.where('guild_id', 'eq', guildId)
|
|
341
|
-
.where('date', 'gte', dateMin)
|
|
342
|
-
.exec();
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
rows = await repo.query()
|
|
346
|
-
.where('date', 'gte', dateMin)
|
|
347
|
-
.exec();
|
|
348
|
-
}
|
|
388
|
+
const newRows = await this.getCommandExecutedRows(guildId, daysBack);
|
|
389
|
+
const oldRows = await this.getCommandDailyStatsRows(guildId, daysBack);
|
|
349
390
|
const aggregated = new Map();
|
|
350
|
-
for (const row of
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
e.totalTime += row.total_execution_time_ms;
|
|
356
|
-
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;
|
|
357
396
|
}
|
|
358
397
|
else {
|
|
359
|
-
aggregated.set(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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 });
|
|
364
409
|
}
|
|
365
410
|
}
|
|
366
411
|
return Array.from(aggregated.entries())
|
|
367
|
-
.map(([
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
date: '',
|
|
372
|
-
usage_count: data.usage_count,
|
|
373
|
-
total_execution_time_ms: data.totalTime,
|
|
374
|
-
error_count: data.errors,
|
|
375
|
-
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,
|
|
376
416
|
}))
|
|
377
|
-
.filter(c => c.total_execution_time_ms > 0)
|
|
378
417
|
.sort((a, b) => b.avgExecutionTimeMs - a.avgExecutionTimeMs)
|
|
379
418
|
.slice(0, limit);
|
|
380
419
|
}
|
|
@@ -387,6 +426,15 @@ export class AnalyticsCollector {
|
|
|
387
426
|
.sort('date', 'asc')
|
|
388
427
|
.exec();
|
|
389
428
|
}
|
|
429
|
+
async getChannelMessageStats(guildId, daysBack) {
|
|
430
|
+
const repo = this.repo(ChannelMessageStats);
|
|
431
|
+
const dateMin = this.dateDaysAgo(daysBack);
|
|
432
|
+
return repo.query()
|
|
433
|
+
.where('guild_id', 'eq', guildId)
|
|
434
|
+
.where('date', 'gte', dateMin)
|
|
435
|
+
.sort('date', 'asc')
|
|
436
|
+
.exec();
|
|
437
|
+
}
|
|
390
438
|
// ── Config ───────────────────────────────────────────────────
|
|
391
439
|
async getConfig(guildId) {
|
|
392
440
|
return this.ensuredConfig(guildId);
|
|
@@ -434,6 +482,14 @@ export class AnalyticsCollector {
|
|
|
434
482
|
for (const cmd of oldCmds) {
|
|
435
483
|
await cmdRepo.delete({ id: cmd.id });
|
|
436
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
|
+
}
|
|
437
493
|
const voiceRepo = this.repo(VoiceChannelDailyStats);
|
|
438
494
|
const oldVoice = await voiceRepo.query()
|
|
439
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>
|
|
@@ -10,13 +10,19 @@
|
|
|
10
10
|
<div class="card mb-6">
|
|
11
11
|
<div class="text-center py-4">
|
|
12
12
|
<div class="text-4xl font-bold text-discord-primary"><%= totalMessages %></div>
|
|
13
|
-
<div class="text-sm text-discord-foreground/60"><%= t('analytics.messages') %> (<%= daysBack %>
|
|
13
|
+
<div class="text-sm text-discord-foreground/60"><%= t('analytics.messages') %> (<%= daysBack %> dias)</div>
|
|
14
14
|
</div>
|
|
15
15
|
</div>
|
|
16
|
-
<div class="card">
|
|
16
|
+
<div class="card mb-6">
|
|
17
17
|
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.messagesPerDay') %></h3>
|
|
18
18
|
<canvas id="chartMessages" width="700" height="300"></canvas>
|
|
19
19
|
</div>
|
|
20
|
+
<% if (typeof channelMessages !== 'undefined' && channelMessages.length > 0) { %>
|
|
21
|
+
<div class="card">
|
|
22
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Mensajes por Canal</h3>
|
|
23
|
+
<canvas id="chartChannels" width="700" height="250"></canvas>
|
|
24
|
+
</div>
|
|
25
|
+
<% } %>
|
|
20
26
|
</div>
|
|
21
27
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
22
28
|
<script>
|
|
@@ -28,4 +34,14 @@ new Chart(document.getElementById('chartMessages'), {
|
|
|
28
34
|
},
|
|
29
35
|
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
30
36
|
});
|
|
37
|
+
<% if (typeof channelMessages !== 'undefined' && channelMessages.length > 0) { %>
|
|
38
|
+
new Chart(document.getElementById('chartChannels'), {
|
|
39
|
+
type: 'bar',
|
|
40
|
+
data: {
|
|
41
|
+
labels: <%- JSON.stringify(channelMessages.map(v => v.channel_id)) %>,
|
|
42
|
+
datasets: [{ label: '<%= t("analytics.messages") %>', data: <%- JSON.stringify(channelMessages.map(v => v.message_count)) %>, backgroundColor: '#57f287', borderRadius: 4 }]
|
|
43
|
+
},
|
|
44
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
45
|
+
});
|
|
46
|
+
<% } %>
|
|
31
47
|
</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
|
}
|