.188625fb7dd457ed-00000000.bun-build
.188625fb7dd457ed-00000000.bun-build
This is a binary file and will not be displayed.
.188625fbf9fffb7a-00000000.bun-build
.188625fbf9fffb7a-00000000.bun-build
This is a binary file and will not be displayed.
+13
-7
README.md
+13
-7
README.md
···
23
23
Your `.env` file should look like this:
24
24
25
25
```bash
26
-
SLACK_TOKEN=xoxb-123456789012-123456789012-123456789012-123456789012
26
+
# Either SLACK_BOT_TOKEN or SLACK_TOKEN works (SLACK_BOT_TOKEN takes precedence)
27
+
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
27
28
SLACK_SIGNING_SECRET=12345678901234567890123456789012
28
29
NODE_ENV=production
30
+
BEARER_TOKEN=your-secure-random-token-here # Required for admin endpoints
29
31
SENTRY_DSN="https://xxxxx@xxxx.ingest.us.sentry.io/123456" # Optional
30
32
DATABASE_PATH=/path/to/db.sqlite # Optional
31
33
PORT=3000 # Optional
34
+
35
+
# Optional: Slack rate limiting (adjust if hitting rate limits)
36
+
# SLACK_MAX_CONCURRENT=3 # Max concurrent requests (default: 3)
37
+
# SLACK_MIN_TIME_MS=200 # Min ms between requests (default: 200)
32
38
```
33
39
34
40
The slack app can be created from the [`manifest.yaml`](./manifest.yaml) in this repo. It just needs the `emoji:read` and `users:read` scopes.
···
128
134
// Analytics data access
129
135
const stats = await cache.getEssentialStats(7);
130
136
const chartData = await cache.getChartData(7);
131
-
const userAgents = await cache.getUserAgents(7);
137
+
const userAgents = await cache.getUserAgents();
132
138
```
133
139
134
140
The 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.
135
141
136
142
```typescript
137
-
const slack = new Slack(
138
-
process.env.SLACK_TOKEN,
143
+
const slack = new SlackWrapper(
144
+
process.env.SLACK_BOT_TOKEN,
139
145
process.env.SLACK_SIGNING_SECRET,
140
146
);
141
147
142
-
const user = await slack.getUser("U062UG485EE");
143
-
const emojis = await slack.getEmoji();
148
+
const user = await slack.getUserInfo("U062UG485EE");
149
+
const emojis = await slack.getEmojiList();
144
150
145
151
// Manually purge a specific user's cache using the API endpoint
146
152
const response = await fetch(
···
169
175
```typescript
170
176
// src/migrations/myNewMigration.ts
171
177
import { Database } from "bun:sqlite";
172
-
import { Migration } from "./types";
178
+
import type { Migration } from "./types";
173
179
174
180
export const myNewMigration: Migration = {
175
181
version: "0.3.2", // Should match package.json version
+5
-5
src/cache.ts
+5
-5
src/cache.ts
···
1216
1216
averageResponseTime: number;
1217
1217
}>;
1218
1218
averageResponseTime: number | null;
1219
-
topUserAgents: Array<{ userAgent: string; count: number }>;
1219
+
topUserAgents: Array<{ userAgent: string; hits: number }>;
1220
1220
latencyAnalytics: {
1221
1221
percentiles: {
1222
1222
p50: number | null;
···
1418
1418
const topUserAgents = this.db
1419
1419
.query(
1420
1420
`
1421
-
SELECT user_agent as userAgent, hits as count
1421
+
SELECT user_agent as userAgent, hits
1422
1422
FROM user_agent_stats
1423
1423
WHERE user_agent IS NOT NULL
1424
1424
ORDER BY hits DESC
1425
1425
LIMIT 50
1426
1426
`,
1427
1427
)
1428
-
.all() as Array<{ userAgent: string; count: number }>;
1428
+
.all() as Array<{ userAgent: string; hits: number }>;
1429
1429
1430
1430
// Simplified latency analytics from bucket data
1431
1431
const percentiles = {
···
1603
1603
peakDayRequests: peakDayData?.count || 0,
1604
1604
},
1605
1605
dashboardMetrics: {
1606
-
statsRequests: statsResult.count,
1607
-
totalWithStats: totalCount + statsResult.count,
1606
+
statsRequests: statsResult.count ?? 0,
1607
+
totalWithStats: totalCount + (statsResult.count ?? 0),
1608
1608
},
1609
1609
trafficOverview,
1610
1610
};
+2
-2
src/migrations/index.ts
+2
-2
src/migrations/index.ts
···
1
1
import { bucketAnalyticsMigration } from "./bucketAnalyticsMigration";
2
2
import { endpointGroupingMigration } from "./endpointGroupingMigration";
3
3
import { logGroupingMigration } from "./logGroupingMigration";
4
-
import { Migration } from "./types";
4
+
import type { Migration } from "./types";
5
5
6
6
// Export all migrations
7
7
export const migrations = [
···
12
12
];
13
13
14
14
// Export the migration types
15
-
export { Migration };
15
+
export type { Migration };
+7
-4
src/migrations/migrationManager.ts
+7
-4
src/migrations/migrationManager.ts
···
208
208
if (parts.length !== 3) return null;
209
209
210
210
const [major, minor, patch] = parts.map(Number);
211
+
if (major === undefined || minor === undefined || patch === undefined) {
212
+
return null;
213
+
}
211
214
212
215
// If patch > 0, decrement patch
213
216
if (patch > 0) {
214
217
return `${major}.${minor}.${patch - 1}`;
215
218
}
216
219
// If minor > 0, decrement minor and set patch to 0
217
-
else if (minor > 0) {
220
+
if (minor > 0) {
218
221
return `${major}.${minor - 1}.0`;
219
222
}
220
223
// If major > 0, decrement major and set minor and patch to 0
221
-
else if (major > 0) {
224
+
if (major > 0) {
222
225
return `${major - 1}.0.0`;
223
226
}
224
227
···
236
239
const partsB = b.split(".").map(Number);
237
240
238
241
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
239
-
const partA = i < partsA.length ? partsA[i] : 0;
240
-
const partB = i < partsB.length ? partsB[i] : 0;
242
+
const partA = partsA[i] ?? 0;
243
+
const partB = partsB[i] ?? 0;
241
244
242
245
if (partA < partB) return -1;
243
246
if (partA > partB) return 1;