@zumito-team/analytics-module 0.7.0 → 0.8.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/dist/events/discord/MessageCreate.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/ChannelMessageStats.d.ts +8 -0
- package/dist/models/ChannelMessageStats.js +31 -0
- package/dist/routes/UserPanelAnalyticsMessages.js +2 -1
- package/dist/services/AnalyticsCollector.d.ts +5 -1
- package/dist/services/AnalyticsCollector.js +61 -3
- package/dist/views/user-analytics-messages.ejs +18 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -14,4 +14,5 @@ export { AnalyticsModuleConfig } from './config.js';
|
|
|
14
14
|
export { GuildDailyStats } from './models/GuildDailyStats.js';
|
|
15
15
|
export { CommandDailyStats } from './models/CommandDailyStats.js';
|
|
16
16
|
export { VoiceChannelDailyStats } from './models/VoiceChannelDailyStats.js';
|
|
17
|
+
export { ChannelMessageStats } from './models/ChannelMessageStats.js';
|
|
17
18
|
export { GuildAnalyticsConfig } from './models/GuildAnalyticsConfig.js';
|
package/dist/index.js
CHANGED
|
@@ -89,4 +89,5 @@ export { AnalyticsModuleConfig } from './config.js';
|
|
|
89
89
|
export { GuildDailyStats } from './models/GuildDailyStats.js';
|
|
90
90
|
export { CommandDailyStats } from './models/CommandDailyStats.js';
|
|
91
91
|
export { VoiceChannelDailyStats } from './models/VoiceChannelDailyStats.js';
|
|
92
|
+
export { ChannelMessageStats } from './models/ChannelMessageStats.js';
|
|
92
93
|
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 };
|
|
@@ -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);
|
|
@@ -2,6 +2,7 @@ import { GuildDailyStats } from '../models/GuildDailyStats.js';
|
|
|
2
2
|
import { CommandDailyStats } from '../models/CommandDailyStats.js';
|
|
3
3
|
import { VoiceChannelDailyStats } from '../models/VoiceChannelDailyStats.js';
|
|
4
4
|
import { GuildAnalyticsConfig } from '../models/GuildAnalyticsConfig.js';
|
|
5
|
+
import { ChannelMessageStats } from '../models/ChannelMessageStats.js';
|
|
5
6
|
export interface CommandExecutedPayload {
|
|
6
7
|
guildId: string;
|
|
7
8
|
commandName: string;
|
|
@@ -13,11 +14,13 @@ export declare class AnalyticsCollector {
|
|
|
13
14
|
private db;
|
|
14
15
|
private voiceSessions;
|
|
15
16
|
private dailyVoiceUsers;
|
|
17
|
+
private dailyMessageAuthors;
|
|
18
|
+
private dailyChannelMessageAuthors;
|
|
16
19
|
private cleanupTimer;
|
|
17
20
|
constructor(db?: any);
|
|
18
21
|
private repo;
|
|
19
22
|
private ensuredConfig;
|
|
20
|
-
recordMessage(guildId: string): Promise<void>;
|
|
23
|
+
recordMessage(guildId: string, channelId?: string, authorId?: string): Promise<void>;
|
|
21
24
|
recordMemberJoin(guildId: string): Promise<void>;
|
|
22
25
|
recordMemberLeave(guildId: string): Promise<void>;
|
|
23
26
|
recordVoiceJoin(guildId: string, channelId: string, userId: string): Promise<void>;
|
|
@@ -52,6 +55,7 @@ export declare class AnalyticsCollector {
|
|
|
52
55
|
avgExecutionTimeMs: number;
|
|
53
56
|
})[]>;
|
|
54
57
|
getVoiceChannelStats(guildId: string, daysBack: number): Promise<VoiceChannelDailyStats[]>;
|
|
58
|
+
getChannelMessageStats(guildId: string, daysBack: number): Promise<ChannelMessageStats[]>;
|
|
55
59
|
getConfig(guildId: string): Promise<GuildAnalyticsConfig>;
|
|
56
60
|
updateConfig(guildId: string, partial: Partial<GuildAnalyticsConfig>): Promise<void>;
|
|
57
61
|
runCleanup(): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import { GuildDailyStats } from '../models/GuildDailyStats.js';
|
|
|
3
3
|
import { CommandDailyStats } from '../models/CommandDailyStats.js';
|
|
4
4
|
import { VoiceChannelDailyStats } from '../models/VoiceChannelDailyStats.js';
|
|
5
5
|
import { GuildAnalyticsConfig } from '../models/GuildAnalyticsConfig.js';
|
|
6
|
+
import { ChannelMessageStats } from '../models/ChannelMessageStats.js';
|
|
6
7
|
import { AnalyticsModuleConfig } from '../config.js';
|
|
7
8
|
function today() {
|
|
8
9
|
return new Date().toISOString().slice(0, 10);
|
|
@@ -12,6 +13,8 @@ export class AnalyticsCollector {
|
|
|
12
13
|
this.db = db;
|
|
13
14
|
this.voiceSessions = new Map();
|
|
14
15
|
this.dailyVoiceUsers = new Map();
|
|
16
|
+
this.dailyMessageAuthors = new Map();
|
|
17
|
+
this.dailyChannelMessageAuthors = new Map();
|
|
15
18
|
this.cleanupTimer = null;
|
|
16
19
|
}
|
|
17
20
|
repo(name) {
|
|
@@ -38,11 +41,48 @@ export class AnalyticsCollector {
|
|
|
38
41
|
await repo.insert(defaults);
|
|
39
42
|
return defaults;
|
|
40
43
|
}
|
|
41
|
-
async recordMessage(guildId) {
|
|
44
|
+
async recordMessage(guildId, channelId, authorId) {
|
|
42
45
|
const config = await this.ensuredConfig(guildId);
|
|
43
46
|
if (!config.enabled || !config.track_messages)
|
|
44
47
|
return;
|
|
45
48
|
await this.upsertGuildStats(guildId, { message_count: 1 });
|
|
49
|
+
// Track unique authors per day
|
|
50
|
+
if (authorId) {
|
|
51
|
+
const authorKey = `${guildId}_${today()}`;
|
|
52
|
+
if (!this.dailyMessageAuthors.has(authorKey)) {
|
|
53
|
+
this.dailyMessageAuthors.set(authorKey, new Set());
|
|
54
|
+
}
|
|
55
|
+
this.dailyMessageAuthors.get(authorKey).add(authorId);
|
|
56
|
+
}
|
|
57
|
+
// Track per-channel stats
|
|
58
|
+
if (channelId && config.track_per_channel_voice) {
|
|
59
|
+
const date = today();
|
|
60
|
+
const chanKey = `${guildId}_${channelId}_${date}`;
|
|
61
|
+
const repo = this.repo(ChannelMessageStats);
|
|
62
|
+
const existing = await repo.findOne({ id: chanKey });
|
|
63
|
+
if (authorId) {
|
|
64
|
+
if (!this.dailyChannelMessageAuthors.has(chanKey)) {
|
|
65
|
+
this.dailyChannelMessageAuthors.set(chanKey, new Set());
|
|
66
|
+
}
|
|
67
|
+
this.dailyChannelMessageAuthors.get(chanKey).add(authorId);
|
|
68
|
+
}
|
|
69
|
+
if (existing) {
|
|
70
|
+
await repo.update({ id: chanKey }, {
|
|
71
|
+
message_count: existing.message_count + 1,
|
|
72
|
+
unique_authors: this.dailyChannelMessageAuthors.get(chanKey)?.size || 0,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
await repo.insert({
|
|
77
|
+
id: chanKey,
|
|
78
|
+
guild_id: guildId,
|
|
79
|
+
channel_id: channelId,
|
|
80
|
+
date,
|
|
81
|
+
message_count: 1,
|
|
82
|
+
unique_authors: 0,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
46
86
|
}
|
|
47
87
|
async recordMemberJoin(guildId) {
|
|
48
88
|
const config = await this.ensuredConfig(guildId);
|
|
@@ -374,8 +414,17 @@ export class AnalyticsCollector {
|
|
|
374
414
|
error_count: data.errors,
|
|
375
415
|
avgExecutionTimeMs: data.usage_count > 0 ? Math.round(data.totalTime / data.usage_count) : 0,
|
|
376
416
|
}))
|
|
377
|
-
.
|
|
378
|
-
|
|
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
|
+
})
|
|
379
428
|
.slice(0, limit);
|
|
380
429
|
}
|
|
381
430
|
async getVoiceChannelStats(guildId, daysBack) {
|
|
@@ -387,6 +436,15 @@ export class AnalyticsCollector {
|
|
|
387
436
|
.sort('date', 'asc')
|
|
388
437
|
.exec();
|
|
389
438
|
}
|
|
439
|
+
async getChannelMessageStats(guildId, daysBack) {
|
|
440
|
+
const repo = this.repo(ChannelMessageStats);
|
|
441
|
+
const dateMin = this.dateDaysAgo(daysBack);
|
|
442
|
+
return repo.query()
|
|
443
|
+
.where('guild_id', 'eq', guildId)
|
|
444
|
+
.where('date', 'gte', dateMin)
|
|
445
|
+
.sort('date', 'asc')
|
|
446
|
+
.exec();
|
|
447
|
+
}
|
|
390
448
|
// ── Config ───────────────────────────────────────────────────
|
|
391
449
|
async getConfig(guildId) {
|
|
392
450
|
return this.ensuredConfig(guildId);
|
|
@@ -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>
|