···2323Your `.env` file should look like this:
24242525```bash
2626-SLACK_TOKEN=xoxb-123456789012-123456789012-123456789012-123456789012
2626+# Either SLACK_BOT_TOKEN or SLACK_TOKEN works (SLACK_BOT_TOKEN takes precedence)
2727+SLACK_BOT_TOKEN=xoxb-your-bot-token-here
2728SLACK_SIGNING_SECRET=12345678901234567890123456789012
2829NODE_ENV=production
3030+BEARER_TOKEN=your-secure-random-token-here # Required for admin endpoints
2931SENTRY_DSN="https://xxxxx@xxxx.ingest.us.sentry.io/123456" # Optional
3032DATABASE_PATH=/path/to/db.sqlite # Optional
3133PORT=3000 # Optional
3434+3535+# Optional: Slack rate limiting (adjust if hitting rate limits)
3636+# SLACK_MAX_CONCURRENT=3 # Max concurrent requests (default: 3)
3737+# SLACK_MIN_TIME_MS=200 # Min ms between requests (default: 200)
3238```
33393440The slack app can be created from the [`manifest.yaml`](./manifest.yaml) in this repo. It just needs the `emoji:read` and `users:read` scopes.
···128134// Analytics data access
129135const stats = await cache.getEssentialStats(7);
130136const chartData = await cache.getChartData(7);
131131-const userAgents = await cache.getUserAgents(7);
137137+const userAgents = await cache.getUserAgents();
132138```
133139134140The final bit was at this point a bit of a ridiculous one. I didn't like how heavyweight the `bolt` or `slack-edge` packages were so I rolled my own slack api wrapper. It's again fully typed and designed to be as lightweight as possible. The background user update queue processes up to 3 users every 30 seconds to respect Slack's rate limits.
135141136142```typescript
137137-const slack = new Slack(
138138- process.env.SLACK_TOKEN,
143143+const slack = new SlackWrapper(
144144+ process.env.SLACK_BOT_TOKEN,
139145 process.env.SLACK_SIGNING_SECRET,
140146);
141147142142-const user = await slack.getUser("U062UG485EE");
143143-const emojis = await slack.getEmoji();
148148+const user = await slack.getUserInfo("U062UG485EE");
149149+const emojis = await slack.getEmojiList();
144150145151// Manually purge a specific user's cache using the API endpoint
146152const response = await fetch(
···169175```typescript
170176// src/migrations/myNewMigration.ts
171177import { Database } from "bun:sqlite";
172172-import { Migration } from "./types";
178178+import type { Migration } from "./types";
173179174180export const myNewMigration: Migration = {
175181 version: "0.3.2", // Should match package.json version
+5-5
src/cache.ts
···12161216 averageResponseTime: number;
12171217 }>;
12181218 averageResponseTime: number | null;
12191219- topUserAgents: Array<{ userAgent: string; count: number }>;
12191219+ topUserAgents: Array<{ userAgent: string; hits: number }>;
12201220 latencyAnalytics: {
12211221 percentiles: {
12221222 p50: number | null;
···14181418 const topUserAgents = this.db
14191419 .query(
14201420 `
14211421- SELECT user_agent as userAgent, hits as count
14211421+ SELECT user_agent as userAgent, hits
14221422 FROM user_agent_stats
14231423 WHERE user_agent IS NOT NULL
14241424 ORDER BY hits DESC
14251425 LIMIT 50
14261426 `,
14271427 )
14281428- .all() as Array<{ userAgent: string; count: number }>;
14281428+ .all() as Array<{ userAgent: string; hits: number }>;
1429142914301430 // Simplified latency analytics from bucket data
14311431 const percentiles = {
···16031603 peakDayRequests: peakDayData?.count || 0,
16041604 },
16051605 dashboardMetrics: {
16061606- statsRequests: statsResult.count,
16071607- totalWithStats: totalCount + statsResult.count,
16061606+ statsRequests: statsResult.count ?? 0,
16071607+ totalWithStats: totalCount + (statsResult.count ?? 0),
16081608 },
16091609 trafficOverview,
16101610 };
+2-2
src/migrations/index.ts
···11import { bucketAnalyticsMigration } from "./bucketAnalyticsMigration";
22import { endpointGroupingMigration } from "./endpointGroupingMigration";
33import { logGroupingMigration } from "./logGroupingMigration";
44-import { Migration } from "./types";
44+import type { Migration } from "./types";
5566// Export all migrations
77export const migrations = [
···1212];
13131414// Export the migration types
1515-export { Migration };
1515+export type { Migration };
+7-4
src/migrations/migrationManager.ts
···208208 if (parts.length !== 3) return null;
209209210210 const [major, minor, patch] = parts.map(Number);
211211+ if (major === undefined || minor === undefined || patch === undefined) {
212212+ return null;
213213+ }
211214212215 // If patch > 0, decrement patch
213216 if (patch > 0) {
214217 return `${major}.${minor}.${patch - 1}`;
215218 }
216219 // If minor > 0, decrement minor and set patch to 0
217217- else if (minor > 0) {
220220+ if (minor > 0) {
218221 return `${major}.${minor - 1}.0`;
219222 }
220223 // If major > 0, decrement major and set minor and patch to 0
221221- else if (major > 0) {
224224+ if (major > 0) {
222225 return `${major - 1}.0.0`;
223226 }
224227···236239 const partsB = b.split(".").map(Number);
237240238241 for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
239239- const partA = i < partsA.length ? partsA[i] : 0;
240240- const partB = i < partsB.length ? partsB[i] : 0;
242242+ const partA = partsA[i] ?? 0;
243243+ const partB = partsB[i] ?? 0;
241244242245 if (partA < partB) return -1;
243246 if (partA > partB) return 1;