@zumito-team/analytics-module 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@zumito-team/analytics-module",
3
- "version": "0.2.0",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
3
+ "version": "0.3.0",
6
4
  "description": "Server analytics module with configurable tracking and optional panel integration",
7
5
  "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
8
12
  "scripts": {
9
13
  "build": "tsc -p tsconfig.json && cp -r translations dist/ && cp -r views dist/"
10
14
  },
@@ -1,118 +0,0 @@
1
- import { Command, CommandParameters, CommandType, ServiceContainer } from 'zumito-framework';
2
- import { EmbedBuilder, PermissionFlagsBits } from 'zumito-framework/discord';
3
- import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
4
-
5
- export class AnalyticsConfig extends Command {
6
- name = 'analytics-config';
7
- categories = ['analytics', 'admin'];
8
- type = CommandType.any;
9
- dm = false;
10
- adminOnly = true;
11
-
12
- constructor(
13
- private collector: AnalyticsCollector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector,
14
- ) {
15
- super();
16
- }
17
-
18
- async execute({ message, interaction, trans, args }: CommandParameters): Promise<void> {
19
- const target = message || interaction;
20
- if (!target) return;
21
-
22
- const guildId = message?.guildId || interaction?.guildId;
23
- if (!guildId) {
24
- await target.reply({ content: trans('noGuild') } as any);
25
- return;
26
- }
27
-
28
- const config = await this.collector.getConfig(guildId);
29
-
30
- const subcommand = args instanceof Map ? args.get('action') : (typeof args === 'object' && 'action' in args ? args['action'] : undefined);
31
-
32
- if (!subcommand) {
33
- const embed = new EmbedBuilder()
34
- .setTitle(trans('configTitle'))
35
- .setDescription(trans('configDescription'))
36
- .addFields(
37
- { name: trans('enabled'), value: config.enabled ? '✅ Yes' : '❌ No', inline: true },
38
- { name: trans('trackMessages'), value: config.track_messages ? '✅' : '❌', inline: true },
39
- { name: trans('trackVoice'), value: config.track_voice ? '✅' : '❌', inline: true },
40
- { name: trans('trackMembers'), value: config.track_members ? '✅' : '❌', inline: true },
41
- { name: trans('trackCommands'), value: config.track_commands ? '✅' : '❌', inline: true },
42
- { name: trans('trackPerformance'), value: config.track_command_performance ? '✅' : '❌', inline: true },
43
- { name: trans('trackPerChannelVoice'), value: config.track_per_channel_voice ? '✅' : '❌', inline: true },
44
- { name: trans('retentionDays'), value: (config.retention_days || '90').toString(), inline: true },
45
- { name: trans('publicStats'), value: config.public_stats_page ? '✅' : '❌', inline: true },
46
- )
47
- .setColor(0x5865F2);
48
-
49
- if (message) {
50
- await message.reply({ embeds: [embed] });
51
- } else if (interaction) {
52
- if (interaction.replied || interaction.deferred) {
53
- await interaction.editReply({ embeds: [embed] });
54
- } else {
55
- await interaction.reply({ embeds: [embed], ephemeral: true });
56
- }
57
- }
58
- return;
59
- }
60
-
61
- switch (subcommand) {
62
- case 'enable':
63
- await this.collector.updateConfig(guildId, { enabled: true });
64
- break;
65
- case 'disable':
66
- await this.collector.updateConfig(guildId, { enabled: false });
67
- break;
68
- case 'messages':
69
- await this.collector.updateConfig(guildId, { track_messages: !config.track_messages });
70
- break;
71
- case 'voice':
72
- await this.collector.updateConfig(guildId, { track_voice: !config.track_voice });
73
- break;
74
- case 'members':
75
- await this.collector.updateConfig(guildId, { track_members: !config.track_members });
76
- break;
77
- case 'commands':
78
- await this.collector.updateConfig(guildId, { track_commands: !config.track_commands });
79
- break;
80
- case 'performance':
81
- await this.collector.updateConfig(guildId, { track_command_performance: !config.track_command_performance });
82
- break;
83
- case 'retention': {
84
- const days = args instanceof Map ? args.get('days') : (typeof args === 'object' && 'days' in args ? args['days'] : null);
85
- if (days && typeof days === 'number') {
86
- await this.collector.updateConfig(guildId, { retention_days: days });
87
- }
88
- break;
89
- }
90
- default:
91
- break;
92
- }
93
-
94
- const updated = await this.collector.getConfig(guildId);
95
- const embed = new EmbedBuilder()
96
- .setTitle(trans('configUpdated'))
97
- .addFields(
98
- { name: trans('enabled'), value: updated.enabled ? '✅ Yes' : '❌ No', inline: true },
99
- { name: trans('trackMessages'), value: updated.track_messages ? '✅' : '❌', inline: true },
100
- { name: trans('trackVoice'), value: updated.track_voice ? '✅' : '❌', inline: true },
101
- { name: trans('trackMembers'), value: updated.track_members ? '✅' : '❌', inline: true },
102
- { name: trans('trackCommands'), value: updated.track_commands ? '✅' : '❌', inline: true },
103
- { name: trans('trackPerformance'), value: updated.track_command_performance ? '✅' : '❌', inline: true },
104
- { name: trans('retentionDays'), value: (updated.retention_days || '90').toString(), inline: true },
105
- )
106
- .setColor(0x57F287);
107
-
108
- if (message) {
109
- await message.reply({ embeds: [embed] });
110
- } else if (interaction) {
111
- if (interaction.replied || interaction.deferred) {
112
- await interaction.editReply({ embeds: [embed] });
113
- } else {
114
- await interaction.reply({ embeds: [embed], ephemeral: true });
115
- }
116
- }
117
- }
118
- }
package/commands/Stats.ts DELETED
@@ -1,66 +0,0 @@
1
- import { Command, CommandParameters, CommandType, ServiceContainer } from 'zumito-framework';
2
- import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'zumito-framework/discord';
3
- import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
4
-
5
- export class Stats extends Command {
6
- name = 'stats';
7
- categories = ['analytics', 'utility'];
8
- type = CommandType.any;
9
- dm = false;
10
-
11
- constructor(
12
- private collector: AnalyticsCollector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector,
13
- ) {
14
- super();
15
- }
16
-
17
- async execute({ message, interaction, framework, trans }: CommandParameters): Promise<void> {
18
- const target = message || interaction;
19
- if (!target) return;
20
-
21
- const guildId = message?.guildId || interaction?.guildId;
22
- if (!guildId) {
23
- await target.reply({ content: trans('noGuild'), ephemeral: true } as any);
24
- return;
25
- }
26
-
27
- const daysBack = 7;
28
- const stats = await this.collector.getGuildStats(guildId, daysBack);
29
-
30
- let totalMessages = 0, totalJoins = 0, totalLeaves = 0;
31
- let totalVoice = 0, totalCommands = 0;
32
-
33
- for (const s of stats) {
34
- totalMessages += s.message_count;
35
- totalJoins += s.join_count;
36
- totalLeaves += s.leave_count;
37
- totalVoice += s.voice_minutes;
38
- totalCommands += s.command_count;
39
- }
40
-
41
- const embed = new EmbedBuilder()
42
- .setTitle(trans('title'))
43
- .setDescription(trans('description', { days: daysBack }))
44
- .addFields(
45
- { name: trans('messages'), value: totalMessages.toString(), inline: true },
46
- { name: trans('commands'), value: totalCommands.toString(), inline: true },
47
- { name: trans('joins'), value: totalJoins.toString(), inline: true },
48
- { name: trans('leaves'), value: totalLeaves.toString(), inline: true },
49
- { name: trans('voice'), value: `${Math.round(totalVoice)} min`, inline: true },
50
- )
51
- .setColor(0x5865F2)
52
- .setTimestamp();
53
-
54
- const replyPayload = { embeds: [embed] } as any;
55
-
56
- if (message) {
57
- await message.reply(replyPayload);
58
- } else if (interaction) {
59
- if (interaction.replied || interaction.deferred) {
60
- await interaction.editReply(replyPayload);
61
- } else {
62
- await interaction.reply({ ...replyPayload, ephemeral: true });
63
- }
64
- }
65
- }
66
- }
package/config.ts DELETED
@@ -1,14 +0,0 @@
1
- export class AnalyticsModuleConfig {
2
- static defaultRetentionDays = 90;
3
- static cleanupIntervalHours = 24;
4
- static defaultTrackMessages = true;
5
- static defaultTrackVoice = true;
6
- static defaultTrackMembers = true;
7
- static defaultTrackCommands = true;
8
- static defaultTrackCommandPerformance = false;
9
- static defaultTrackPerChannelVoice = false;
10
-
11
- static configure(opts: Partial<typeof AnalyticsModuleConfig>) {
12
- Object.assign(this, opts);
13
- }
14
- }
@@ -1,23 +0,0 @@
1
- import { FrameworkEvent } from 'zumito-framework';
2
- import { ServiceContainer } from 'zumito-framework';
3
- import { AnalyticsCollector } from '../../services/AnalyticsCollector.js';
4
-
5
- export class GuildMemberAdd extends FrameworkEvent {
6
- once = false;
7
- source = 'discord';
8
-
9
- private collector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector;
10
-
11
- async execute({ guildmember }: any): Promise<void> {
12
- if (!guildmember?.guild?.id) return;
13
- const guildId = guildmember.guild.id;
14
- await this.collector.recordMemberJoin(guildId);
15
-
16
- try {
17
- const memberCount = guildmember.guild.memberCount;
18
- if (memberCount) {
19
- await this.collector.recordMemberCount(guildId, memberCount);
20
- }
21
- } catch (_) { /* ignore */ }
22
- }
23
- }
@@ -1,23 +0,0 @@
1
- import { FrameworkEvent } from 'zumito-framework';
2
- import { ServiceContainer } from 'zumito-framework';
3
- import { AnalyticsCollector } from '../../services/AnalyticsCollector.js';
4
-
5
- export class GuildMemberRemove extends FrameworkEvent {
6
- once = false;
7
- source = 'discord';
8
-
9
- private collector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector;
10
-
11
- async execute({ guildmember }: any): Promise<void> {
12
- if (!guildmember?.guild?.id) return;
13
- const guildId = guildmember.guild.id;
14
- await this.collector.recordMemberLeave(guildId);
15
-
16
- try {
17
- const memberCount = guildmember.guild.memberCount;
18
- if (memberCount) {
19
- await this.collector.recordMemberCount(guildId, memberCount);
20
- }
21
- } catch (_) { /* ignore */ }
22
- }
23
- }
@@ -1,16 +0,0 @@
1
- import { FrameworkEvent } from 'zumito-framework';
2
- import { ServiceContainer } from 'zumito-framework';
3
- import { AnalyticsCollector } from '../../services/AnalyticsCollector.js';
4
-
5
- export class MessageCreate extends FrameworkEvent {
6
- once = false;
7
- source = 'discord';
8
-
9
- private collector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector;
10
-
11
- async execute({ message }: any): Promise<void> {
12
- if (!message.guildId) return;
13
- if (message.author?.bot) return;
14
- await this.collector.recordMessage(message.guildId);
15
- }
16
- }
@@ -1,31 +0,0 @@
1
- import { FrameworkEvent } from 'zumito-framework';
2
- import { ServiceContainer } from 'zumito-framework';
3
- import { AnalyticsCollector } from '../../services/AnalyticsCollector.js';
4
-
5
- export class VoiceStateUpdate extends FrameworkEvent {
6
- once = false;
7
- source = 'discord';
8
-
9
- private collector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector;
10
-
11
- async execute({ oldState, newState }: any): Promise<void> {
12
- const guildId = newState?.guild?.id || oldState?.guild?.id;
13
- if (!guildId) return;
14
-
15
- const userId = newState?.id || oldState?.id;
16
- if (!userId) return;
17
-
18
- const oldChannelId = oldState?.channelId || null;
19
- const newChannelId = newState?.channelId || null;
20
-
21
- if (oldChannelId === newChannelId) return;
22
-
23
- if (oldChannelId) {
24
- await this.collector.recordVoiceLeave(guildId, oldChannelId, userId);
25
- }
26
-
27
- if (newChannelId) {
28
- await this.collector.recordVoiceJoin(guildId, newChannelId, userId);
29
- }
30
- }
31
- }
@@ -1,16 +0,0 @@
1
- import { FrameworkEvent } from 'zumito-framework';
2
- import { ServiceContainer } from 'zumito-framework';
3
- import { AnalyticsCollector } from '../../services/AnalyticsCollector.js';
4
-
5
- export class CommandExecuted extends FrameworkEvent {
6
- once = false;
7
- source = 'framework';
8
-
9
- private collector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector;
10
-
11
- async execute(args: any): Promise<void> {
12
- const payload = args.object || args;
13
- if (!payload.guildId) return;
14
- await this.collector.recordCommand(payload);
15
- }
16
- }
package/index.ts DELETED
@@ -1,69 +0,0 @@
1
- import { Module, ServiceContainer } from 'zumito-framework';
2
- import { AnalyticsCollector } from './services/AnalyticsCollector.js';
3
- import { AnalyticsModuleConfig } from './config.js';
4
-
5
- export class AnalyticsModule extends Module {
6
- static dependencies = [] as const;
7
- static optionalDependencies = ['admin-module', 'user-panel-module'] as const;
8
-
9
- constructor(modulePath: string = import.meta.url) {
10
- super(modulePath);
11
- ServiceContainer.addService(AnalyticsCollector, [], true);
12
- }
13
-
14
- async initialize(): Promise<void> {
15
- await super.initialize();
16
-
17
- try {
18
- const { NavigationService } = await import('@zumito-team/admin-module/services/NavigationService.js');
19
- const nav = ServiceContainer.getService(NavigationService);
20
- nav.registerItem({
21
- id: 'analytics',
22
- icon: `<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-discord-white/60 group-hover:text-white" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 16l4-8 4 5 4-3"/></svg>`,
23
- label: 'Analiticas',
24
- url: '/admin/analytics',
25
- order: 8,
26
- category: 'modules',
27
- sidebar: {
28
- showDropdown: false,
29
- sections: [
30
- {
31
- label: 'Analiticas',
32
- items: [
33
- { label: 'Dashboard', url: '/admin/analytics' },
34
- ],
35
- },
36
- ],
37
- },
38
- });
39
- } catch (e) {
40
- console.warn('[AnalyticsModule] Admin panel not available, skipping admin integration');
41
- }
42
-
43
- try {
44
- const { UserPanelNavigationService } = await import('@zumito-team/user-panel-module/services/UserPanelNavigationService');
45
- const nav = ServiceContainer.getService(UserPanelNavigationService);
46
- nav.registerSubItems('dashboard', 'general', [
47
- { id: 'analytics', label: 'analytics.sidebarTitle', url: '/panel/:guildId/analytics' },
48
- ]);
49
- } catch (e) {
50
- console.warn('[AnalyticsModule] User panel not available, skipping user panel integration');
51
- }
52
-
53
- const collector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector;
54
- collector.startCleanupScheduler();
55
- }
56
-
57
- async onAllReady(): Promise<void> {
58
- const collector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector;
59
- collector.clearVoiceSessions();
60
- }
61
- }
62
-
63
- export { AnalyticsCollector } from './services/AnalyticsCollector.js';
64
- export type { CommandExecutedPayload } from './services/AnalyticsCollector.js';
65
- export { AnalyticsModuleConfig } from './config.js';
66
- export { GuildDailyStats } from './models/GuildDailyStats.js';
67
- export { CommandDailyStats } from './models/CommandDailyStats.js';
68
- export { VoiceChannelDailyStats } from './models/VoiceChannelDailyStats.js';
69
- export { GuildAnalyticsConfig } from './models/GuildAnalyticsConfig.js';
@@ -1,25 +0,0 @@
1
- import { Collection, Field } from 'zumito-framework';
2
-
3
- @Collection({ name: 'analytics_command_daily_stats' })
4
- export class CommandDailyStats {
5
- @Field({ type: 'string', primary: true, unique: true })
6
- id!: string;
7
-
8
- @Field({ type: 'string' })
9
- guild_id!: string;
10
-
11
- @Field({ type: 'string' })
12
- command_name!: string;
13
-
14
- @Field({ type: 'string' })
15
- date!: string;
16
-
17
- @Field({ type: 'number', default: 0 })
18
- usage_count!: number;
19
-
20
- @Field({ type: 'number', default: 0 })
21
- total_execution_time_ms!: number;
22
-
23
- @Field({ type: 'number', default: 0 })
24
- error_count!: number;
25
- }
@@ -1,34 +0,0 @@
1
- import { Collection, Field } from 'zumito-framework';
2
-
3
- @Collection({ name: 'analytics_guild_config' })
4
- export class GuildAnalyticsConfig {
5
- @Field({ type: 'string', primary: true, unique: true })
6
- guild_id!: string;
7
-
8
- @Field({ type: 'boolean', default: true })
9
- enabled!: boolean;
10
-
11
- @Field({ type: 'boolean', default: true })
12
- track_messages!: boolean;
13
-
14
- @Field({ type: 'boolean', default: true })
15
- track_voice!: boolean;
16
-
17
- @Field({ type: 'boolean', default: true })
18
- track_members!: boolean;
19
-
20
- @Field({ type: 'boolean', default: true })
21
- track_commands!: boolean;
22
-
23
- @Field({ type: 'boolean', default: false })
24
- track_command_performance!: boolean;
25
-
26
- @Field({ type: 'boolean', default: false })
27
- track_per_channel_voice!: boolean;
28
-
29
- @Field({ type: 'number' })
30
- retention_days!: number;
31
-
32
- @Field({ type: 'boolean', default: false })
33
- public_stats_page!: boolean;
34
- }
@@ -1,31 +0,0 @@
1
- import { Collection, Field } from 'zumito-framework';
2
-
3
- @Collection({ name: 'analytics_guild_daily_stats' })
4
- export class GuildDailyStats {
5
- @Field({ type: 'string', primary: true, unique: true })
6
- id!: string;
7
-
8
- @Field({ type: 'string' })
9
- guild_id!: string;
10
-
11
- @Field({ type: 'string' })
12
- date!: string;
13
-
14
- @Field({ type: 'number', default: 0 })
15
- message_count!: number;
16
-
17
- @Field({ type: 'number', default: 0 })
18
- join_count!: number;
19
-
20
- @Field({ type: 'number', default: 0 })
21
- leave_count!: number;
22
-
23
- @Field({ type: 'number', default: 0 })
24
- voice_minutes!: number;
25
-
26
- @Field({ type: 'number', default: 0 })
27
- command_count!: number;
28
-
29
- @Field({ type: 'number', default: 0 })
30
- member_count!: number;
31
- }
@@ -1,22 +0,0 @@
1
- import { Collection, Field } from 'zumito-framework';
2
-
3
- @Collection({ name: 'analytics_voice_channel_daily_stats' })
4
- export class VoiceChannelDailyStats {
5
- @Field({ type: 'string', primary: true, unique: true })
6
- id!: string;
7
-
8
- @Field({ type: 'string' })
9
- guild_id!: string;
10
-
11
- @Field({ type: 'string' })
12
- channel_id!: string;
13
-
14
- @Field({ type: 'string' })
15
- date!: string;
16
-
17
- @Field({ type: 'number', default: 0 })
18
- total_minutes!: number;
19
-
20
- @Field({ type: 'number', default: 0 })
21
- unique_users!: number;
22
- }
@@ -1,69 +0,0 @@
1
- import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
2
- import path, { dirname } from 'path';
3
- import { fileURLToPath } from 'url';
4
- import ejs from 'ejs';
5
- import { Client } from 'zumito-framework/discord';
6
- import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
-
10
- export class AdminAnalytics extends Route {
11
- method = RouteMethod.get;
12
- path = '/admin/analytics';
13
-
14
- constructor(
15
- private collector: AnalyticsCollector = ServiceContainer.getService(AnalyticsCollector) as AnalyticsCollector,
16
- private client: Client = ServiceContainer.getService(Client),
17
- ) {
18
- super();
19
- }
20
-
21
- async execute(req: any, res: any): Promise<void> {
22
- const { AdminAuthService } = await import('@zumito-team/admin-module/services/AdminAuthService.js');
23
- const auth = await ServiceContainer.getService(AdminAuthService).isLoginValid(req);
24
- if (!auth?.isValid) return res.redirect('/admin/login');
25
-
26
- const daysBack = parseInt(req.query.days as string) || 7;
27
-
28
- const summary = await this.collector.getGlobalStatsSummary(daysBack);
29
- const guildGrowth = await this.collector.getGuildGrowth(daysBack);
30
- const messagesPerDay = await this.collector.getMessagesPerDay(daysBack);
31
- const commandsPerDay = await this.collector.getCommandsPerDay(null, daysBack);
32
- const topCommands = await this.collector.getTopCommands(null, daysBack);
33
- const slowestCommands = await this.collector.getSlowestCommands(null, daysBack);
34
-
35
- const chartData = {
36
- guildGrowth,
37
- messagesPerDay,
38
- commandsPerDay,
39
- topCommands,
40
- slowestCommands,
41
- };
42
-
43
- const { TranslationManager } = await import('zumito-framework');
44
- const tm = ServiceContainer.getService(TranslationManager) as any;
45
- const t = (key: string, params?: any) => tm.get(key, 'en', params);
46
-
47
- const content = await ejs.renderFile(
48
- path.resolve(__dirname, '../views/admin-analytics.ejs'),
49
- {
50
- summary,
51
- chartData,
52
- slowestCommands: slowestCommands.length > 0 ? slowestCommands : undefined,
53
- t,
54
- daysBack,
55
- },
56
- );
57
-
58
- const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
59
- const view = ServiceContainer.getService(AdminViewService);
60
- const html = await view.render({
61
- title: 'Analiticas',
62
- content,
63
- reqPath: this.path,
64
- user: { name: 'Admin' },
65
- });
66
-
67
- res.send(html);
68
- }
69
- }