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/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
+ });