[READ-ONLY] Mirror of https://github.com/maybeanerd/MagiBot. MagiBot - the community building discord bot that adds joinsounds to your voice channels! discord.gg/2Evcf4T
bot bots community discord gaming hacktoberfest sound
1

Configure Feed

Select the types of activity you want to include in your feed.

feat: use posthog to track command usage (#1392)

authored by

Sebastian Di Luzio and committed by
GitHub
(Jun 17, 2025, 6:07 PM +0200) 6cb19c4a f9e3eaf7

+188 -24
+10 -5
.github/workflows/release.dev.yml
··· 1 1 name: Release to dev 2 2 3 - on: 3 + on: 4 4 push: 5 5 branches: 6 6 - main ··· 29 29 echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY@L}-${{ secrets.ENVIRONMENT }}" >> $GITHUB_ENV 30 30 31 31 - name: Build containers 32 - run: docker compose --project-name ${{ env.PROJECT_NAME }} build 32 + run: docker compose --project-name ${{ env.PROJECT_NAME }} build 33 33 34 34 - name: Log in to the Container registry 35 35 uses: docker/login-action@v3 ··· 43 43 uses: docker/metadata-action@v5 44 44 with: 45 45 images: ${{ env.IMAGE_NAME }} 46 - 46 + 47 47 - name: Push Docker image 48 48 run: docker push ${{ env.IMAGE_NAME }} 49 49 50 50 - name: Install SSH Key 51 51 uses: shimataro/ssh-key-action@v2 52 52 with: 53 - key: ${{ secrets.SSH_PRIVATE_KEY }} 53 + key: ${{ secrets.SSH_PRIVATE_KEY }} 54 54 known_hosts: 'just-a-placeholder-so-we-dont-get-errors' 55 - 55 + 56 56 - name: Add Known Hosts 57 57 run: ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 58 58 ··· 73 73 DATABASE_URL: ${{ secrets.DATABASE_URL }} 74 74 STATCORD_TOKEN: ${{ secrets.STATCORD_TOKEN }} 75 75 APP_ID: ${{ secrets.APP_ID }} 76 + 77 + # PostHog analytics 78 + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} 79 + 80 + # discord webhook tokens 76 81 WEBHOOK_ID_EX: ${{ secrets.WEBHOOK_ID_EX }} 77 82 WEBHOOK_TOKEN_EX: ${{ secrets.WEBHOOK_TOKEN_EX }} 78 83 WEBHOOK_ID_JOIN: ${{ secrets.WEBHOOK_ID_JOIN }}
+12 -8
.github/workflows/release.prod.yml
··· 1 1 name: Release to production 2 2 3 - on: 4 - release: 3 + on: 4 + release: 5 5 types: [published] 6 - 7 6 8 7 jobs: 9 8 container: ··· 27 26 - name: Create Image Name 28 27 run: | 29 28 echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY@L}-${{ secrets.ENVIRONMENT }}" >> $GITHUB_ENV 30 - 29 + 31 30 - name: Build containers 32 - run: docker compose --project-name ${{ env.PROJECT_NAME }} build 31 + run: docker compose --project-name ${{ env.PROJECT_NAME }} build 33 32 34 33 - name: Log in to the Container registry 35 34 uses: docker/login-action@v3 ··· 43 42 uses: docker/metadata-action@v5 44 43 with: 45 44 images: ${{ env.IMAGE_NAME }} 46 - 45 + 47 46 - name: Push Docker image 48 47 run: docker push ${{ env.IMAGE_NAME }} 49 48 50 49 - name: Install SSH Key 51 50 uses: shimataro/ssh-key-action@v2 52 51 with: 53 - key: ${{ secrets.SSH_PRIVATE_KEY }} 52 + key: ${{ secrets.SSH_PRIVATE_KEY }} 54 53 known_hosts: 'just-a-placeholder-so-we-dont-get-errors' 55 - 54 + 56 55 - name: Add Known Hosts 57 56 run: ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 58 57 ··· 73 72 DATABASE_URL: ${{ secrets.DATABASE_URL }} 74 73 STATCORD_TOKEN: ${{ secrets.STATCORD_TOKEN }} 75 74 APP_ID: ${{ secrets.APP_ID }} 75 + 76 + # PostHog analytics 77 + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} 78 + 79 + # discord webhook tokens 76 80 WEBHOOK_ID_EX: ${{ secrets.WEBHOOK_ID_EX }} 77 81 WEBHOOK_TOKEN_EX: ${{ secrets.WEBHOOK_TOKEN_EX }} 78 82 WEBHOOK_ID_JOIN: ${{ secrets.WEBHOOK_ID_JOIN }}
+5 -2
docker-compose.yml
··· 1 - version: "3.9" 1 + version: '3.9' 2 2 services: 3 3 main: 4 4 build: . ··· 12 12 - STATCORD_TOKEN=${STATCORD_TOKEN} 13 13 - APP_ID=${APP_ID} 14 14 15 + # PostHog analytics 16 + - POSTHOG_API_KEY=${POSTHOG_API_KEY} 17 + 15 18 # discord webhook tokens 16 19 - WEBHOOK_ID_EX=${WEBHOOK_ID_EX} 17 20 - WEBHOOK_TOKEN_EX=${WEBHOOK_TOKEN_EX} ··· 22 25 - WEBHOOK_ID_STARTUP=${WEBHOOK_ID_STARTUP} 23 26 - WEBHOOK_TOKEN_STARTUP=${WEBHOOK_TOKEN_STARTUP} 24 27 volumes: 25 - joinsounds: 28 + joinsounds:
+15
package-lock.json
··· 25 25 "libsodium-wrappers": "0.7.15", 26 26 "mongoose": "8.16.0", 27 27 "opusscript": "0.1.1", 28 + "posthog-node": "5.1.1", 28 29 "statcord.js": "3.4.3", 29 30 "tweetnacl": "1.0.3", 30 31 "utf-8-validate": "6.0.5" ··· 6404 6405 "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", 6405 6406 "engines": { 6406 6407 "node": ">=0.10.0" 6408 + } 6409 + }, 6410 + "node_modules/posthog-node": { 6411 + "version": "5.1.1", 6412 + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.1.1.tgz", 6413 + "integrity": "sha512-6VISkNdxO24ehXiDA4dugyCSIV7lpGVaEu5kn/dlAj+SJ1lgcDru9PQ8p/+GSXsXVxohd1t7kHL2JKc9NoGb0w==", 6414 + "license": "MIT", 6415 + "engines": { 6416 + "node": ">=20" 6407 6417 } 6408 6418 }, 6409 6419 "node_modules/prelude-ls": { ··· 13242 13252 "version": "0.1.1", 13243 13253 "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", 13244 13254 "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==" 13255 + }, 13256 + "posthog-node": { 13257 + "version": "5.1.1", 13258 + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.1.1.tgz", 13259 + "integrity": "sha512-6VISkNdxO24ehXiDA4dugyCSIV7lpGVaEu5kn/dlAj+SJ1lgcDru9PQ8p/+GSXsXVxohd1t7kHL2JKc9NoGb0w==" 13245 13260 }, 13246 13261 "prelude-ls": { 13247 13262 "version": "1.2.1",
+1
package.json
··· 27 27 "libsodium-wrappers": "0.7.15", 28 28 "mongoose": "8.16.0", 29 29 "opusscript": "0.1.1", 30 + "posthog-node": "5.1.1", 30 31 "statcord.js": "3.4.3", 31 32 "tweetnacl": "1.0.3", 32 33 "utf-8-validate": "6.0.5"
+4 -1
rename-me.env
··· 11 11 WEBHOOK_ID_BUG= 12 12 WEBHOOK_TOKEN_BUG= 13 13 WEBHOOK_ID_STARTUP= 14 - WEBHOOK_TOKEN_STARTUP= 14 + WEBHOOK_TOKEN_STARTUP= 15 + 16 + # posthog tokens 17 + POSTHOG_API_KEY=
+81
src/analytics.ts
··· 1 + import posthog, { PostHog } from 'posthog-node'; 2 + import configuration from './configuration'; 3 + 4 + // Singleton PostHog client 5 + let client: PostHog | null = null; 6 + 7 + export function initPostHog() { 8 + if (!client) { 9 + // Use configuration for PostHog keys 10 + const apiKey = configuration.posthogApiKey; 11 + if (!apiKey) { 12 + console.warn('PostHog API key not set. Analytics disabled.'); 13 + return; 14 + } 15 + client = new posthog.PostHog(apiKey); 16 + } 17 + } 18 + 19 + export function trackGenericEvent({ 20 + userId, 21 + event, 22 + properties = {}, 23 + }: { 24 + userId: string; 25 + event: string; 26 + properties?: Record<string, any>; 27 + }) { 28 + if (!client) { 29 + console.warn('PostHog client not initialized.'); 30 + return; 31 + } 32 + client.capture({ 33 + distinctId: userId, 34 + event, 35 + properties: { 36 + ...properties, 37 + }, 38 + }); 39 + } 40 + 41 + export function trackCommandUsage({ 42 + commandName, 43 + userId, 44 + properties = {}, 45 + }: { 46 + commandName: string; 47 + userId: string; 48 + properties?: Record<string, any>; 49 + }) { 50 + trackGenericEvent({ 51 + userId, 52 + event: 'command_used', 53 + properties: { 54 + command: commandName, 55 + ...properties, 56 + }, 57 + }); 58 + } 59 + 60 + export function trackJoinsoundPlayed({ 61 + userId, 62 + properties = {}, 63 + }: { 64 + userId: string; 65 + properties?: Record<string, any>; 66 + }) { 67 + trackGenericEvent({ 68 + userId, 69 + event: 'joinsound_played', 70 + properties: { 71 + ...properties, 72 + }, 73 + }); 74 + } 75 + 76 + export function shutdownPostHog() { 77 + if (client) { 78 + client.shutdown(); 79 + client = null; 80 + } 81 + }
+9
src/applicationCommandHandler.ts
··· 7 7 import { DeferReply } from './types/command'; 8 8 import { doesInteractionRequireFollowup } from './helperFunctions'; 9 9 import { getUser } from './dbHelpers'; 10 + import { trackCommandUsage } from './analytics'; 10 11 11 12 export async function usageUp(userid: string, guildID: string) { 12 13 const usr = await getUser(userid, guildID); ··· 71 72 ephemeral: command.defer === DeferReply.ephemeral, 72 73 }); 73 74 } 75 + // Track command usage with PostHog 76 + trackCommandUsage({ 77 + commandName: interaction.commandName, 78 + userId: interaction.member.user.id, 79 + properties: { 80 + guildId: interaction.guild.id, 81 + }, 82 + }); 74 83 // actually use the command 75 84 await command.run(interaction); 76 85 await usageUp(interaction.member.user.id, interaction.guild.id);
+12 -4
src/bot.ts
··· 1 1 import { 2 2 ChatInputCommandInteraction, 3 - Client, DiscordAPIError, GatewayIntentBits, 3 + Client, 4 + DiscordAPIError, 5 + GatewayIntentBits, 4 6 } from 'discord.js'; 5 7 import { handle } from 'blapi'; 6 8 import { generateDependencyReport } from '@discordjs/voice'; 7 9 import { ActivityType } from 'discord-api-types/v10'; 8 10 import config from './configuration'; 9 - import { 10 - TOKEN, setUser, 11 - } from './shared_assets'; 11 + import { TOKEN, setUser } from './shared_assets'; 12 12 // eslint-disable-next-line import/no-cycle 13 13 import { catchErrorOnDiscord } from './sendToMyDiscord'; 14 14 import { getUserMention } from './helperFunctions'; ··· 18 18 import { onVoiceStateChange } from './voiceChannelManager'; 19 19 import { onInteraction } from './commands/queue/buttonInteractions'; 20 20 import { checkGuild } from './dbHelpers'; 21 + import { initPostHog, shutdownPostHog } from './analytics'; 21 22 22 23 console.log(generateDependencyReport()); 23 24 ··· 34 35 export const bot = new Client({ intents }); 35 36 // post to the APIs every 30 minutes 36 37 handle(bot, config.blapis, 30); 38 + 39 + // Initialize PostHog analytics 40 + initPostHog(); 37 41 38 42 process.on('uncaughtException', async (err) => { 39 43 console.error(`Uncaught Exception:\n${err.stack ? err.stack : err}`); ··· 161 165 162 166 bot.on('disconnect', () => { 163 167 console.log('Disconnected!'); 168 + }); 169 + 170 + process.on('exit', () => { 171 + shutdownPostHog(); 164 172 }); 165 173 166 174 bot.login(TOKEN); // connect to discord
+2
src/configuration.ts
··· 7 7 const appId = process.env.APP_ID; 8 8 const owner = process.env.OWNERID; 9 9 const dburl = process.env.DATABASE_URL; 10 + const posthogApiKey = process.env.POSTHOG_API_KEY; 10 11 let blapis: { 11 12 [listname: string]: string; 12 13 }; ··· 31 32 dburl, 32 33 blapis, 33 34 appId, 35 + posthogApiKey, 34 36 };
+37 -4
src/voiceChannelManager.ts
··· 18 18 shadowBannedSound, 19 19 } from './shared_assets'; 20 20 import { saveJoinsoundsPlayedOfShard } from './statTracking'; 21 + import { trackJoinsoundPlayed, trackGenericEvent } from './analytics'; 21 22 22 23 async function isStillMuted(userID: string, guildID: string) { 23 24 const find = await StillMutedModel.findOne({ ··· 37 38 clearTimeout(timeout); 38 39 } 39 40 connection.destroy(); 40 - player.removeAllListeners().stop(true);// To be sure noone listens to this anymore 41 + player.removeAllListeners().stop(true); // To be sure noone listens to this anymore 41 42 } 42 43 43 44 export async function onVoiceStateChange( ··· 53 54 && !newState.member.user.bot 54 55 && (!oldState.channel || !newVc || oldState.channel.id !== newVc.id) 55 56 ) { 57 + trackGenericEvent({ 58 + userId: newState.member.id, 59 + event: 'voice_channel_join_detected', 60 + properties: { 61 + guildId: newState.guild.id, 62 + }, 63 + }); 64 + 56 65 // is muted and joined a vc? maybe still muted from queue 57 66 if ( 58 67 newState.serverMute ··· 76 85 && ((await isJoinableVc(newState.guild.id, newVc.id)) 77 86 || shadowBanned === shadowBannedLevel.guild) 78 87 ) { 88 + trackGenericEvent({ 89 + userId: newState.member.id, 90 + event: 'voice_channel_should_play_joinsound', 91 + properties: { 92 + guildId: newState.guild.id, 93 + }, 94 + }); 95 + 79 96 const perms = newVc.permissionsFor(newState.guild.members.me); 80 - if (perms && perms.has( 81 - PermissionFlagsBits.Connect, 82 - )) { 97 + if (perms && perms.has(PermissionFlagsBits.Connect)) { 83 98 let sound = await getJoinsoundOfUser(newState.id, newState.guild.id); 84 99 if (shadowBanned !== shadowBannedLevel.not) { 85 100 sound = shadowBannedSound; 86 101 } 102 + 103 + trackGenericEvent({ 104 + userId: newState.member.id, 105 + event: 'voice_channel_loaded_joinsound', 106 + properties: { 107 + guildId: newState.guild.id, 108 + hasSound: Boolean(sound), 109 + }, 110 + }); 111 + 87 112 if (sound) { 88 113 connection = joinVoiceChannel({ 89 114 channelId: newVc.id, ··· 98 123 inlineVolume: true, 99 124 }); 100 125 player.play(resource); 126 + 127 + trackJoinsoundPlayed({ 128 + userId: newState.member.id, 129 + properties: { 130 + guildId: newState.guild.id, 131 + }, 132 + }); 133 + 101 134 resource.volume!.setVolume(0.5); 102 135 saveJoinsoundsPlayedOfShard(-1); // no shard data here atm 103 136