Skip to content

Commit ef45dad

Browse files
committed
Better Slack error alerts, added webhook error alerts
1 parent af31a14 commit ef45dad

File tree

6 files changed

+1193
-70
lines changed

6 files changed

+1193
-70
lines changed

apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts

Lines changed: 93 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { sendAlertEmail } from "~/services/email.server";
2525
import { logger } from "~/services/logger.server";
2626
import { decryptSecret } from "~/services/secrets/secretStore.server";
2727
import { subtle } from "crypto";
28+
import { generateErrorGroupWebhookPayload } from "./errorGroupWebhook.server";
2829

2930
type ErrorAlertClassification = "new_issue" | "regression" | "unignored";
3031

@@ -183,58 +184,15 @@ export class DeliverErrorGroupAlertService {
183184
return;
184185
}
185186

186-
const label = this.#classificationLabel(payload.classification);
187-
const errorType = payload.error.errorType || "Error";
188-
const task = payload.error.taskIdentifier;
189-
const envName = payload.error.environmentName;
190-
191-
const emoji =
192-
payload.classification === "new_issue"
193-
? ":rotating_light:"
194-
: payload.classification === "regression"
195-
? ":warning:"
196-
: ":bell:";
187+
const message = this.#buildErrorGroupSlackMessage(
188+
payload,
189+
errorLink,
190+
channel.project.name
191+
);
197192

198193
await this.#postSlackMessage(integration, {
199194
channel: slackProperties.data.channelId,
200-
text: `${label}: ${errorType} in ${task} [${envName}]`,
201-
blocks: [
202-
{
203-
type: "section",
204-
text: {
205-
type: "mrkdwn",
206-
text: `${emoji} *${label}* in *${task}* [${envName}]`,
207-
},
208-
},
209-
{
210-
type: "section",
211-
text: {
212-
type: "mrkdwn",
213-
text: this.#wrapInCodeBlock(
214-
payload.error.sampleStackTrace || payload.error.errorMessage
215-
),
216-
},
217-
},
218-
{
219-
type: "context",
220-
elements: [
221-
{
222-
type: "mrkdwn",
223-
text: `> *${task}* | ${envName} | ${channel.project.name}\n> ${payload.error.occurrenceCount} occurrences | ${this.#formatTimestamp(new Date(Number(payload.error.lastSeen)))}`,
224-
},
225-
],
226-
},
227-
{
228-
type: "actions",
229-
elements: [
230-
{
231-
type: "button",
232-
text: { type: "plain_text", text: "Investigate" },
233-
url: errorLink,
234-
},
235-
],
236-
},
237-
],
195+
...message,
238196
});
239197
}
240198

@@ -255,36 +213,22 @@ export class DeliverErrorGroupAlertService {
255213
return;
256214
}
257215

258-
const webhookPayload = {
259-
type: "alert.error_group" as const,
216+
const webhookPayload = generateErrorGroupWebhookPayload({
260217
classification: payload.classification,
261-
error: {
262-
fingerprint: payload.error.fingerprint,
263-
type: payload.error.errorType,
264-
message: payload.error.errorMessage,
265-
stackTrace: payload.error.sampleStackTrace || undefined,
266-
firstSeen: payload.error.firstSeen,
267-
lastSeen: payload.error.lastSeen,
268-
occurrenceCount: payload.error.occurrenceCount,
269-
taskIdentifier: payload.error.taskIdentifier,
270-
},
271-
environment: {
272-
id: payload.error.environmentId,
273-
name: payload.error.environmentName,
274-
},
218+
error: payload.error,
275219
organization: {
276220
id: channel.project.organizationId,
277221
slug: channel.project.organization.slug,
278222
name: channel.project.organization.title,
279223
},
280224
project: {
281225
id: channel.project.id,
282-
ref: channel.project.externalRef,
226+
externalRef: channel.project.externalRef,
283227
slug: channel.project.slug,
284228
name: channel.project.name,
285229
},
286230
dashboardUrl: errorLink,
287-
};
231+
});
288232

289233
const rawPayload = JSON.stringify(webhookPayload);
290234
const hashPayload = Buffer.from(rawPayload, "utf-8");
@@ -352,11 +296,90 @@ export class DeliverErrorGroupAlertService {
352296
}
353297
}
354298

299+
#buildErrorGroupSlackMessage(
300+
payload: ErrorAlertPayload,
301+
errorLink: string,
302+
projectName: string
303+
): Pick<ChatPostMessageArguments, "text" | "blocks" | "attachments"> {
304+
const label = this.#classificationLabel(payload.classification);
305+
const errorType = payload.error.errorType || "Error";
306+
const task = payload.error.taskIdentifier;
307+
const envName = payload.error.environmentName;
308+
309+
return {
310+
text: `${label}: ${errorType} in ${task} [${envName}]`,
311+
blocks: [
312+
{
313+
type: "section",
314+
text: {
315+
type: "mrkdwn",
316+
text: `*${label} in ${task} [${envName}]*`,
317+
},
318+
},
319+
],
320+
attachments: [
321+
{
322+
color: "danger",
323+
blocks: [
324+
{
325+
type: "section",
326+
text: {
327+
type: "mrkdwn",
328+
text: this.#wrapInCodeBlock(
329+
payload.error.sampleStackTrace || payload.error.errorMessage
330+
),
331+
},
332+
},
333+
{
334+
type: "section",
335+
fields: [
336+
{
337+
type: "mrkdwn",
338+
text: `*Task:*\n${task}`,
339+
},
340+
{
341+
type: "mrkdwn",
342+
text: `*Environment:*\n${envName}`,
343+
},
344+
{
345+
type: "mrkdwn",
346+
text: `*Project:*\n${projectName}`,
347+
},
348+
{
349+
type: "mrkdwn",
350+
text: `*Occurrences:*\n${payload.error.occurrenceCount}`,
351+
},
352+
{
353+
type: "mrkdwn",
354+
text: `*Last seen:*\n${this.#formatTimestamp(new Date(Number(payload.error.lastSeen)))}`,
355+
},
356+
],
357+
},
358+
{
359+
type: "actions",
360+
elements: [
361+
{
362+
type: "button",
363+
text: { type: "plain_text", text: "Investigate" },
364+
url: errorLink,
365+
style: "primary",
366+
},
367+
],
368+
},
369+
],
370+
},
371+
],
372+
};
373+
}
374+
355375
#wrapInCodeBlock(text: string, maxLength = 3000) {
376+
const wrapperLength = 6; // ``` prefix + ``` suffix
377+
const truncationSuffix = "\n\n...truncated — check dashboard for full error";
378+
const innerMax = maxLength - wrapperLength;
379+
356380
const truncated =
357-
text.length > maxLength - 10
358-
? text.slice(0, maxLength - 10 - 50) +
359-
"\n\ntruncated - check dashboard for complete error message"
381+
text.length > innerMax
382+
? text.slice(0, innerMax - truncationSuffix.length) + truncationSuffix
360383
: text;
361384
return `\`\`\`${truncated}\`\`\``;
362385
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { nanoid } from "nanoid";
2+
import type { ErrorWebhook } from "@trigger.dev/core/v3/schemas";
3+
4+
export type ErrorAlertClassification = "new_issue" | "regression" | "unignored";
5+
6+
export interface ErrorGroupAlertData {
7+
classification: ErrorAlertClassification;
8+
error: {
9+
fingerprint: string;
10+
environmentId: string;
11+
environmentName: string;
12+
taskIdentifier: string;
13+
errorType: string;
14+
errorMessage: string;
15+
sampleStackTrace: string;
16+
firstSeen: string;
17+
lastSeen: string;
18+
occurrenceCount: number;
19+
};
20+
organization: {
21+
id: string;
22+
slug: string;
23+
name: string;
24+
};
25+
project: {
26+
id: string;
27+
externalRef: string;
28+
slug: string;
29+
name: string;
30+
};
31+
dashboardUrl: string;
32+
}
33+
34+
/**
35+
* Generates a webhook payload for an error group alert that conforms to the
36+
* ErrorWebhook schema from @trigger.dev/core/v3/schemas
37+
*/
38+
export function generateErrorGroupWebhookPayload(data: ErrorGroupAlertData): ErrorWebhook {
39+
return {
40+
id: nanoid(),
41+
created: new Date(),
42+
webhookVersion: "2025-01-01",
43+
type: "alert.error" as const,
44+
object: {
45+
classification: data.classification,
46+
error: {
47+
fingerprint: data.error.fingerprint,
48+
type: data.error.errorType,
49+
message: data.error.errorMessage,
50+
stackTrace: data.error.sampleStackTrace || undefined,
51+
firstSeen: new Date(data.error.firstSeen),
52+
lastSeen: new Date(data.error.lastSeen),
53+
occurrenceCount: data.error.occurrenceCount,
54+
taskIdentifier: data.error.taskIdentifier,
55+
},
56+
environment: {
57+
id: data.error.environmentId,
58+
name: data.error.environmentName,
59+
},
60+
organization: {
61+
id: data.organization.id,
62+
slug: data.organization.slug,
63+
name: data.organization.name,
64+
},
65+
project: {
66+
id: data.project.id,
67+
ref: data.project.externalRef,
68+
slug: data.project.slug,
69+
name: data.project.name,
70+
},
71+
dashboardUrl: data.dashboardUrl,
72+
},
73+
};
74+
}

0 commit comments

Comments
 (0)