argus-discord-analytics 0.1.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 +173 -0
- package/dist/index.d.mts +251 -0
- package/dist/index.d.ts +251 -0
- package/dist/index.js +444 -0
- package/dist/index.mjs +419 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Argus: () => Argus,
|
|
24
|
+
default: () => index_default
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
function hashString(str) {
|
|
28
|
+
let hash = 0;
|
|
29
|
+
for (let i = 0; i < str.length; i++) {
|
|
30
|
+
const char = str.charCodeAt(i);
|
|
31
|
+
hash = (hash << 5) - hash + char;
|
|
32
|
+
hash = hash & hash;
|
|
33
|
+
}
|
|
34
|
+
return Math.abs(hash).toString(36);
|
|
35
|
+
}
|
|
36
|
+
var Argus = class {
|
|
37
|
+
constructor(apiKeyOrConfig) {
|
|
38
|
+
this.queue = [];
|
|
39
|
+
this.serverQueue = [];
|
|
40
|
+
this.revenueQueue = [];
|
|
41
|
+
this.flushTimer = null;
|
|
42
|
+
this.heartbeatTimer = null;
|
|
43
|
+
this.botId = null;
|
|
44
|
+
this.isHeartbeatEnabled = false;
|
|
45
|
+
this.commandStartTimes = /* @__PURE__ */ new Map();
|
|
46
|
+
const config = typeof apiKeyOrConfig === "string" ? { apiKey: apiKeyOrConfig } : apiKeyOrConfig;
|
|
47
|
+
if (!config.apiKey) {
|
|
48
|
+
throw new Error("Argus: API key is required");
|
|
49
|
+
}
|
|
50
|
+
if (!config.apiKey.startsWith("arg_live_") && !config.apiKey.startsWith("arg_test_")) {
|
|
51
|
+
console.warn("Argus: API key should start with arg_live_ or arg_test_");
|
|
52
|
+
}
|
|
53
|
+
this.apiKey = config.apiKey;
|
|
54
|
+
this.endpoint = config.endpoint || "http://localhost:3000";
|
|
55
|
+
this.debug = config.debug || false;
|
|
56
|
+
this.batchSize = config.batchSize || 10;
|
|
57
|
+
this.flushInterval = config.flushInterval || 1e4;
|
|
58
|
+
this.hashUserIds = config.hashUserIds !== false;
|
|
59
|
+
this.heartbeatInterval = config.heartbeatInterval || 3e4;
|
|
60
|
+
this.startFlushTimer();
|
|
61
|
+
this.log("Argus initialized");
|
|
62
|
+
}
|
|
63
|
+
log(...args) {
|
|
64
|
+
if (this.debug) {
|
|
65
|
+
console.log("[Argus]", ...args);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
startFlushTimer() {
|
|
69
|
+
if (this.flushTimer) {
|
|
70
|
+
clearInterval(this.flushTimer);
|
|
71
|
+
}
|
|
72
|
+
this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Track a Discord.js interaction automatically
|
|
76
|
+
* Extracts command name, server ID, and user ID from the interaction
|
|
77
|
+
*/
|
|
78
|
+
track(interaction) {
|
|
79
|
+
const name = interaction.commandName || interaction.customId || "unknown";
|
|
80
|
+
const serverId = interaction.guildId || void 0;
|
|
81
|
+
const userId = interaction.user?.id;
|
|
82
|
+
this.trackEvent("command", name, {
|
|
83
|
+
serverId,
|
|
84
|
+
userId,
|
|
85
|
+
metadata: {
|
|
86
|
+
interactionType: interaction.type
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Track a custom event
|
|
92
|
+
*/
|
|
93
|
+
trackEvent(type, name, options) {
|
|
94
|
+
const event = {
|
|
95
|
+
type,
|
|
96
|
+
name,
|
|
97
|
+
serverId: options?.serverId,
|
|
98
|
+
userId: options?.userId,
|
|
99
|
+
userHash: options?.userId && this.hashUserIds ? hashString(options.userId) : options?.userId,
|
|
100
|
+
metadata: options?.metadata,
|
|
101
|
+
timestamp: Date.now()
|
|
102
|
+
};
|
|
103
|
+
if (this.hashUserIds) {
|
|
104
|
+
delete event.userId;
|
|
105
|
+
}
|
|
106
|
+
this.queue.push(event);
|
|
107
|
+
this.log("Event queued:", event);
|
|
108
|
+
if (this.queue.length >= this.batchSize) {
|
|
109
|
+
this.flush();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Track an error
|
|
114
|
+
*/
|
|
115
|
+
trackError(error, options) {
|
|
116
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
117
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
118
|
+
this.trackEvent("error", errorMessage, {
|
|
119
|
+
serverId: options?.serverId,
|
|
120
|
+
userId: options?.userId,
|
|
121
|
+
metadata: {
|
|
122
|
+
...options?.metadata,
|
|
123
|
+
command: options?.command,
|
|
124
|
+
stack: errorStack
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Track server join or leave events
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* // When bot joins a server
|
|
134
|
+
* client.on('guildCreate', (guild) => {
|
|
135
|
+
* argus.trackServer('join', {
|
|
136
|
+
* serverId: guild.id,
|
|
137
|
+
* memberCount: guild.memberCount
|
|
138
|
+
* });
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* // When bot leaves/is kicked from a server
|
|
142
|
+
* client.on('guildDelete', (guild) => {
|
|
143
|
+
* argus.trackServer('leave', { serverId: guild.id });
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
trackServer(eventType, options) {
|
|
148
|
+
const event = {
|
|
149
|
+
type: "server",
|
|
150
|
+
eventType,
|
|
151
|
+
serverId: options.serverId,
|
|
152
|
+
memberCount: options.memberCount,
|
|
153
|
+
timestamp: Date.now()
|
|
154
|
+
};
|
|
155
|
+
this.serverQueue.push(event);
|
|
156
|
+
this.log("Server event queued:", event);
|
|
157
|
+
this.flushServerEvents();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Track revenue events from Patreon, Ko-fi, Stripe, or custom sources
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* // Track a Patreon pledge
|
|
165
|
+
* argus.trackRevenue({
|
|
166
|
+
* source: 'patreon',
|
|
167
|
+
* amount: 500, // $5.00 in cents
|
|
168
|
+
* userId: 'discord_user_id', // optional
|
|
169
|
+
* tier: 'Premium'
|
|
170
|
+
* });
|
|
171
|
+
*
|
|
172
|
+
* // Track a Ko-fi donation
|
|
173
|
+
* argus.trackRevenue({
|
|
174
|
+
* source: 'kofi',
|
|
175
|
+
* amount: 300,
|
|
176
|
+
* currency: 'USD'
|
|
177
|
+
* });
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
trackRevenue(options) {
|
|
181
|
+
const event = {
|
|
182
|
+
type: "revenue",
|
|
183
|
+
source: options.source,
|
|
184
|
+
amount: options.amount,
|
|
185
|
+
currency: options.currency || "USD",
|
|
186
|
+
userId: options.userId,
|
|
187
|
+
userHash: options.userId && this.hashUserIds ? hashString(options.userId) : options.userId,
|
|
188
|
+
tier: options.tier,
|
|
189
|
+
timestamp: Date.now()
|
|
190
|
+
};
|
|
191
|
+
if (this.hashUserIds) {
|
|
192
|
+
delete event.userId;
|
|
193
|
+
}
|
|
194
|
+
this.revenueQueue.push(event);
|
|
195
|
+
this.log("Revenue event queued:", event);
|
|
196
|
+
this.flushRevenueEvents();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Manually flush all queued events
|
|
200
|
+
*/
|
|
201
|
+
async flush() {
|
|
202
|
+
await Promise.all([
|
|
203
|
+
this.flushCommandEvents(),
|
|
204
|
+
this.flushServerEvents(),
|
|
205
|
+
this.flushRevenueEvents()
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
208
|
+
async flushCommandEvents() {
|
|
209
|
+
if (this.queue.length === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const events = [...this.queue];
|
|
213
|
+
this.queue = [];
|
|
214
|
+
this.log(`Flushing ${events.length} events`);
|
|
215
|
+
try {
|
|
216
|
+
const response = await fetch(`${this.endpoint}/api/events`, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: {
|
|
219
|
+
"Content-Type": "application/json",
|
|
220
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify({ events })
|
|
223
|
+
});
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
226
|
+
}
|
|
227
|
+
this.log("Events sent successfully");
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this.queue = [...events, ...this.queue];
|
|
230
|
+
this.log("Failed to send events, re-queued:", error);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async flushServerEvents() {
|
|
234
|
+
if (this.serverQueue.length === 0) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const events = [...this.serverQueue];
|
|
238
|
+
this.serverQueue = [];
|
|
239
|
+
this.log(`Flushing ${events.length} server events`);
|
|
240
|
+
try {
|
|
241
|
+
const response = await fetch(`${this.endpoint}/api/events/server`, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: {
|
|
244
|
+
"Content-Type": "application/json",
|
|
245
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({ events })
|
|
248
|
+
});
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
251
|
+
}
|
|
252
|
+
this.log("Server events sent successfully");
|
|
253
|
+
} catch (error) {
|
|
254
|
+
this.serverQueue = [...events, ...this.serverQueue];
|
|
255
|
+
this.log("Failed to send server events, re-queued:", error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async flushRevenueEvents() {
|
|
259
|
+
if (this.revenueQueue.length === 0) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const events = [...this.revenueQueue];
|
|
263
|
+
this.revenueQueue = [];
|
|
264
|
+
this.log(`Flushing ${events.length} revenue events`);
|
|
265
|
+
try {
|
|
266
|
+
const response = await fetch(`${this.endpoint}/api/events/revenue`, {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: {
|
|
269
|
+
"Content-Type": "application/json",
|
|
270
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify({ events })
|
|
273
|
+
});
|
|
274
|
+
if (!response.ok) {
|
|
275
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
276
|
+
}
|
|
277
|
+
this.log("Revenue events sent successfully");
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.revenueQueue = [...events, ...this.revenueQueue];
|
|
280
|
+
this.log("Failed to send revenue events, re-queued:", error);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Set the bot ID for all future events
|
|
285
|
+
*/
|
|
286
|
+
setBotId(botId) {
|
|
287
|
+
this.botId = botId;
|
|
288
|
+
this.log("Bot ID set:", botId);
|
|
289
|
+
}
|
|
290
|
+
// ==========================================
|
|
291
|
+
// Heartbeat / Uptime Monitoring
|
|
292
|
+
// ==========================================
|
|
293
|
+
/**
|
|
294
|
+
* Start sending heartbeat pings to monitor bot uptime
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* // Start with default 30 second interval
|
|
299
|
+
* argus.startHeartbeat();
|
|
300
|
+
*
|
|
301
|
+
* // Or customize the interval
|
|
302
|
+
* argus.startHeartbeat({ interval: 60000 }); // 1 minute
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
startHeartbeat(config) {
|
|
306
|
+
if (this.isHeartbeatEnabled) {
|
|
307
|
+
this.log("Heartbeat already running");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const interval = config?.interval || this.heartbeatInterval;
|
|
311
|
+
this.isHeartbeatEnabled = true;
|
|
312
|
+
this.sendHeartbeat();
|
|
313
|
+
this.heartbeatTimer = setInterval(() => {
|
|
314
|
+
this.sendHeartbeat();
|
|
315
|
+
}, interval);
|
|
316
|
+
this.log(`Heartbeat started with ${interval}ms interval`);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Stop sending heartbeat pings
|
|
320
|
+
*/
|
|
321
|
+
stopHeartbeat() {
|
|
322
|
+
if (this.heartbeatTimer) {
|
|
323
|
+
clearInterval(this.heartbeatTimer);
|
|
324
|
+
this.heartbeatTimer = null;
|
|
325
|
+
}
|
|
326
|
+
this.isHeartbeatEnabled = false;
|
|
327
|
+
this.log("Heartbeat stopped");
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Send a single heartbeat ping
|
|
331
|
+
*/
|
|
332
|
+
async sendHeartbeat() {
|
|
333
|
+
const startTime = Date.now();
|
|
334
|
+
try {
|
|
335
|
+
const response = await fetch(`${this.endpoint}/api/heartbeat`, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: {
|
|
338
|
+
"Content-Type": "application/json",
|
|
339
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
timestamp: startTime
|
|
343
|
+
})
|
|
344
|
+
});
|
|
345
|
+
const latencyMs = Date.now() - startTime;
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
348
|
+
}
|
|
349
|
+
this.log(`Heartbeat sent (${latencyMs}ms)`);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
this.log("Heartbeat failed:", error);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// ==========================================
|
|
355
|
+
// Latency Tracking
|
|
356
|
+
// ==========================================
|
|
357
|
+
/**
|
|
358
|
+
* Start timing a command execution
|
|
359
|
+
* Call this at the beginning of your command handler
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```typescript
|
|
363
|
+
* client.on('interactionCreate', async (interaction) => {
|
|
364
|
+
* const timerId = argus.startTimer(interaction.id);
|
|
365
|
+
*
|
|
366
|
+
* try {
|
|
367
|
+
* await handleCommand(interaction);
|
|
368
|
+
* } finally {
|
|
369
|
+
* argus.endTimer(timerId, interaction);
|
|
370
|
+
* }
|
|
371
|
+
* });
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
startTimer(id) {
|
|
375
|
+
this.commandStartTimes.set(id, Date.now());
|
|
376
|
+
return id;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* End timing and track the command with latency
|
|
380
|
+
*/
|
|
381
|
+
endTimer(timerId, interaction) {
|
|
382
|
+
const startTime = this.commandStartTimes.get(timerId);
|
|
383
|
+
if (!startTime) {
|
|
384
|
+
this.log("Timer not found:", timerId);
|
|
385
|
+
this.track(interaction);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const latencyMs = Date.now() - startTime;
|
|
389
|
+
this.commandStartTimes.delete(timerId);
|
|
390
|
+
const name = interaction.commandName || interaction.customId || "unknown";
|
|
391
|
+
const serverId = interaction.guildId || void 0;
|
|
392
|
+
const userId = interaction.user?.id;
|
|
393
|
+
this.trackEventWithLatency("command", name, latencyMs, {
|
|
394
|
+
serverId,
|
|
395
|
+
userId,
|
|
396
|
+
metadata: {
|
|
397
|
+
interactionType: interaction.type
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Track an event with latency measurement
|
|
403
|
+
*/
|
|
404
|
+
trackEventWithLatency(type, name, latencyMs, options) {
|
|
405
|
+
const event = {
|
|
406
|
+
type,
|
|
407
|
+
name,
|
|
408
|
+
serverId: options?.serverId,
|
|
409
|
+
userId: options?.userId,
|
|
410
|
+
userHash: options?.userId && this.hashUserIds ? hashString(options.userId) : options?.userId,
|
|
411
|
+
metadata: {
|
|
412
|
+
...options?.metadata,
|
|
413
|
+
latencyMs
|
|
414
|
+
},
|
|
415
|
+
latencyMs,
|
|
416
|
+
timestamp: Date.now()
|
|
417
|
+
};
|
|
418
|
+
if (this.hashUserIds) {
|
|
419
|
+
delete event.userId;
|
|
420
|
+
}
|
|
421
|
+
this.queue.push(event);
|
|
422
|
+
this.log("Event with latency queued:", event);
|
|
423
|
+
if (this.queue.length >= this.batchSize) {
|
|
424
|
+
this.flush();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Shutdown the Argus instance, flushing any remaining events
|
|
429
|
+
*/
|
|
430
|
+
async shutdown() {
|
|
431
|
+
this.stopHeartbeat();
|
|
432
|
+
if (this.flushTimer) {
|
|
433
|
+
clearInterval(this.flushTimer);
|
|
434
|
+
this.flushTimer = null;
|
|
435
|
+
}
|
|
436
|
+
await this.flush();
|
|
437
|
+
this.log("Argus shutdown complete");
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
var index_default = Argus;
|
|
441
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
442
|
+
0 && (module.exports = {
|
|
443
|
+
Argus
|
|
444
|
+
});
|