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