Skip to content

Commit a0b6b2e

Browse files
committed
chore: api errors to slack notifications
Signed-off-by: Uroš Marolt <[email protected]>
1 parent ce17f0d commit a0b6b2e

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@crowd/opensearch": "workspace:*",
6161
"@crowd/queue": "workspace:*",
6262
"@crowd/redis": "workspace:*",
63+
"@crowd/slack": "workspace:*",
6364
"@crowd/snowflake": "workspace:*",
6465
"@crowd/telemetry": "workspace:*",
6566
"@crowd/temporal": "workspace:*",

backend/src/api/apiResponseHandler.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { LoggerBase } from '@crowd/logging'
2+
import {
3+
SlackChannel,
4+
SlackMessageSection,
5+
SlackPersona,
6+
sendSlackNotification,
7+
} from '@crowd/slack'
28

39
import { IServiceOptions } from '../services/IServiceOptions'
410

11+
// Slack section text limit is 3000 chars. Keep conservative to account for title + formatting.
12+
const SLACK_SECTION_TEXT_LIMIT = 2800
13+
514
/* eslint-disable class-methods-use-this */
615
export default class ApiResponseHandler extends LoggerBase {
716
public constructor(options: IServiceOptions) {
@@ -21,6 +30,89 @@ export default class ApiResponseHandler extends LoggerBase {
2130
}
2231
}
2332

33+
private truncateForSlack(text: string, maxLength: number = SLACK_SECTION_TEXT_LIMIT): string {
34+
if (text.length <= maxLength) {
35+
return text
36+
}
37+
return `${text.substring(0, maxLength - 3)}...`
38+
}
39+
40+
private sendServerErrorToSlack(
41+
req,
42+
error,
43+
code: number,
44+
options?: { sql?: string; dbErrorMessage?: string },
45+
): void {
46+
const sections: SlackMessageSection[] = []
47+
48+
// Request info section
49+
sections.push({
50+
title: 'Request',
51+
text: `*Method:* \`${req.method}\`\n*URL:* \`${req.url}\`\n*Status Code:* \`${code}\``,
52+
})
53+
54+
// Error info section
55+
const errorName = error?.name || 'Unknown'
56+
const errorMessage = this.truncateForSlack(error?.message || 'No message', 2000)
57+
sections.push({
58+
title: 'Error',
59+
text: `*Name:* \`${errorName}\`\n*Message:* ${errorMessage}`,
60+
})
61+
62+
// SQL query section (for Sequelize errors)
63+
if (options?.sql) {
64+
const truncatedSql = this.truncateForSlack(options.sql, 2700)
65+
sections.push({
66+
title: 'SQL Query',
67+
text: `\`\`\`${truncatedSql}\`\`\``,
68+
})
69+
}
70+
71+
// DB error message (for Sequelize errors)
72+
if (options?.dbErrorMessage) {
73+
sections.push({
74+
title: 'Database Error',
75+
text: this.truncateForSlack(options.dbErrorMessage, 2700),
76+
})
77+
}
78+
79+
// Request body section (if present)
80+
if (req.body && Object.keys(req.body).length > 0) {
81+
const bodyStr = JSON.stringify(req.body, null, 2)
82+
const truncatedBody = this.truncateForSlack(bodyStr, 2700)
83+
sections.push({
84+
title: 'Request Body',
85+
text: `\`\`\`${truncatedBody}\`\`\``,
86+
})
87+
}
88+
89+
// Query params section (if present)
90+
if (req.query && Object.keys(req.query).length > 0) {
91+
const queryStr = JSON.stringify(req.query, null, 2)
92+
const truncatedQuery = this.truncateForSlack(queryStr, 2700)
93+
sections.push({
94+
title: 'Query Params',
95+
text: `\`\`\`${truncatedQuery}\`\`\``,
96+
})
97+
}
98+
99+
// Stack trace section
100+
if (error?.stack) {
101+
const truncatedStack = this.truncateForSlack(error.stack, 2700)
102+
sections.push({
103+
title: 'Stack Trace',
104+
text: `\`\`\`${truncatedStack}\`\`\``,
105+
})
106+
}
107+
108+
sendSlackNotification(
109+
SlackChannel.ALERTS,
110+
SlackPersona.ERROR_REPORTER,
111+
`API Error ${code}: ${req.method} ${req.url}`,
112+
sections,
113+
)
114+
}
115+
24116
async error(req, res, error) {
25117
if (error && error.name && error.name.includes('Sequelize')) {
26118
req.log.error(
@@ -35,6 +127,10 @@ export default class ApiResponseHandler extends LoggerBase {
35127
},
36128
'Database error while processing REST API request!',
37129
)
130+
this.sendServerErrorToSlack(req, error, 500, {
131+
sql: error.sql,
132+
dbErrorMessage: error.original?.message,
133+
})
38134
res.status(500).send('Internal Server Error')
39135
} else if (error && [400, 401, 403, 404].includes(error.code)) {
40136
req.log.error(
@@ -57,6 +153,12 @@ export default class ApiResponseHandler extends LoggerBase {
57153
{ code: error.code, url: req.url, method: req.method, query: req.query, body: req.body },
58154
'Error while processing REST API request!',
59155
)
156+
157+
// Send Slack notification for server errors (500-599)
158+
if (error.code >= 500 && error.code <= 599) {
159+
this.sendServerErrorToSlack(req, error, error.code)
160+
}
161+
60162
res.status(error.code).send(error.message)
61163
}
62164
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)