From 25db391c371bb9b1484e34dd446ffb33e55e4921 Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Wed, 2 Jul 2025 16:17:30 +0200
Subject: [PATCH 01/12] feat: separate parsers into legacy & new
---
lib/legacyParser.ts | 132 ++++++++++++++++++++++++++++++++++++++++++++
lib/parser.ts | 4 +-
2 files changed, 134 insertions(+), 2 deletions(-)
create mode 100644 lib/legacyParser.ts
diff --git a/lib/legacyParser.ts b/lib/legacyParser.ts
new file mode 100644
index 0000000..6a80b67
--- /dev/null
+++ b/lib/legacyParser.ts
@@ -0,0 +1,132 @@
+type SentryIssue = Record;
+
+export function getEvent(issue: SentryIssue) {
+ return issue?.event ?? issue?.data?.issue ?? issue;
+}
+
+export function getProject(issue: SentryIssue) {
+ return issue?.project?.project_name ?? getEvent(issue)?.project?.name ?? issue.project_name;
+}
+
+export function getPlatform(issue: SentryIssue) {
+ return getEvent(issue)?.platform;
+}
+
+export function getLanguage(issue: SentryIssue) {
+ return getEvent(issue)?.location?.split(".")?.slice(-1)?.[0] || "";
+}
+
+export function getContexts(issue: SentryIssue) {
+ const contexts = getEvent(issue)?.contexts ?? {};
+ const values = Object.values(contexts)
+ .map((value: Record) => `${value?.name} ${value?.version}`)
+ // TODO: Have a better decision tree here
+ .filter((value) => value !== "undefined undefined");
+
+ return values ?? [];
+}
+
+export function getExtras(issue: SentryIssue) {
+ const extras = getEvent(issue)?.extra ?? {};
+ const values = Object.entries(extras).map(
+ ([key, value]) => `${key}: ${value}`
+ );
+
+ return values ?? [];
+}
+
+export function getLink(issue: SentryIssue) {
+ return issue?.url ?? "https://sentry.io";
+}
+
+export function getTags(issue: SentryIssue) {
+ return getEvent(issue)?.tags ?? [];
+}
+
+export function getLevel(issue: SentryIssue) {
+ return getEvent(issue)?.level;
+}
+
+export function getType(issue: SentryIssue) {
+ return getEvent(issue)?.type;
+}
+
+export function getTitle(issue: SentryIssue) {
+ return getEvent(issue)?.title ?? "Sentry Event";
+}
+
+export function getTime(issue: SentryIssue) {
+ const event = getEvent(issue);
+
+ if (event?.timestamp) {
+ return new Date(getEvent(issue)?.timestamp * 1000);
+ }
+
+ if (event?.lastSeen != null) {
+ return new Date(event?.lastSeen);
+ }
+
+ if (event?.firstSeen != null) {
+ return new Date(event?.firstSeen);
+ }
+
+ return new Date();
+}
+
+export function getRelease(issue: SentryIssue) {
+ return getEvent(issue)?.release;
+}
+
+export function getUser(issue: SentryIssue) {
+ return getEvent(issue)?.user;
+}
+
+export function getFileLocation(issue: SentryIssue) {
+ return getEvent(issue)?.location;
+}
+
+export function getStacktrace(issue: SentryIssue) {
+ return (
+ getEvent(issue)?.stacktrace ??
+ getEvent(issue)?.exception?.values[0]?.stacktrace
+ );
+}
+
+export function getErrorLocation(issue: SentryIssue, maxLines = Infinity) {
+ const stacktrace = getStacktrace(issue);
+ const locations = stacktrace?.frames; /*.reverse();*/
+
+ let files = locations?.map(
+ (location) =>
+ `${location?.filename}, ${location?.lineno ?? "?"}:${
+ location?.colno ?? "?"
+ }`
+ );
+
+ if (maxLines < Infinity && files?.length > maxLines) {
+ files = files.slice(0, maxLines);
+ files.push("...");
+ }
+
+ return files;
+}
+
+export function getErrorCodeSnippet(issue: SentryIssue) {
+ const stacktrace = getStacktrace(issue);
+ const location = stacktrace?.frames?.reverse()?.[0];
+
+ if (!location) {
+ const event = getEvent(issue);
+ return event?.culprit ?? null;
+ }
+
+ // The spaces below are intentional - they help align the code
+ // aorund the additional `>` marker
+ return ` ${location.pre_context?.join("\n ") ?? ""}\n>${
+ location.context_line
+ }\n${location.post_context?.join("\n") ?? ""}`;
+}
+
+export function getMessage(issue: SentryIssue) {
+ return issue?.message;
+}
diff --git a/lib/parser.ts b/lib/parser.ts
index 7e1dba6..e1e8e0e 100644
--- a/lib/parser.ts
+++ b/lib/parser.ts
@@ -1,7 +1,7 @@
type SentryIssue = Record;
export function getEvent(issue: SentryIssue) {
- return issue?.event ?? issue?.data?.issue ?? issue;
+ return issue?.event ?? issue?.data?.issue ?? issue?.data?.event ?? issue;
}
export function getProject(issue: SentryIssue) {
@@ -128,5 +128,5 @@ export function getErrorCodeSnippet(issue: SentryIssue) {
}
export function getMessage(issue: SentryIssue) {
- return issue?.message;
+ return issue?.message ?? getEvent(issue)?.message;
}
From 063388408912e40a7b318a4a2c59704dd3c86efc Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Wed, 2 Jul 2025 16:18:07 +0200
Subject: [PATCH 02/12] feat: separate legacy from new API requests and handle
independently
---
lib/message.ts | 157 +++++++++++++++++++++++++++++++++++-
pages/api/webhooks/[key].ts | 25 ++++--
2 files changed, 174 insertions(+), 8 deletions(-)
diff --git a/lib/message.ts b/lib/message.ts
index 0ec401d..cfc4781 100644
--- a/lib/message.ts
+++ b/lib/message.ts
@@ -1,5 +1,6 @@
import { APIEmbedField, EmbedBuilder } from "discord.js";
import getColor from "./colors";
+import * as legacyParser from "./legacyParser";
import * as parser from "./parser";
function cap(str: string, length: number) {
@@ -10,11 +11,35 @@ function cap(str: string, length: number) {
return str.substr(0, length - 1) + "\u2026";
}
-export default function createMessage(event) {
+export function createMessage(event) {
+ console.debug("Received new event");
+
+ console.debug({
+ event: parser.getEvent(event),
+ project: parser.getProject(event),
+ platform: parser.getPlatform(event),
+ language: parser.getLanguage(event),
+ contexts: parser.getContexts(event),
+ extras: parser.getExtras(event),
+ link: parser.getLink(event),
+ tags: parser.getTags(event),
+ level: parser.getLevel(event),
+ type: parser.getType(event),
+ title: parser.getTitle(event),
+ time: parser.getTime(event),
+ user: parser.getUser(event),
+ release: parser.getRelease(event),
+ fileLocation: parser.getFileLocation(event),
+ stackTrace: parser.getStacktrace(event),
+ errorLocation: parser.getErrorLocation(event, 7),
+ errorCodeSnippet: parser.getErrorCodeSnippet(event),
+ message: parser.getMessage(event),
+ });
+
const embed = new EmbedBuilder()
.setColor(getColor(parser.getLevel(event)))
.setAuthor({
- name: event.project_name,
+ name: event.data.triggered_rule,
iconURL: "https://sentrydiscord.dev/icons/sentry.png",
})
.setFooter({
@@ -114,3 +139,131 @@ export default function createMessage(event) {
embeds: [embed.toJSON()],
};
}
+
+export function createLegacyMessage(event) {
+ console.debug("Received legacy event");
+
+ console.debug({
+ event: legacyParser.getEvent(event),
+ project: legacyParser.getProject(event),
+ platform: legacyParser.getPlatform(event),
+ language: legacyParser.getLanguage(event),
+ contexts: legacyParser.getContexts(event),
+ extras: legacyParser.getExtras(event),
+ link: legacyParser.getLink(event),
+ tags: legacyParser.getTags(event),
+ level: legacyParser.getLevel(event),
+ type: legacyParser.getType(event),
+ title: legacyParser.getTitle(event),
+ time: legacyParser.getTime(event),
+ user: legacyParser.getUser(event),
+ release: legacyParser.getRelease(event),
+ fileLocation: legacyParser.getFileLocation(event),
+ stackTrace: legacyParser.getStacktrace(event),
+ errorLocation: legacyParser.getErrorLocation(event, 7),
+ errorCodeSnippet: legacyParser.getErrorCodeSnippet(event),
+ message: legacyParser.getMessage(event),
+ });
+ const embed = new EmbedBuilder()
+ .setColor(getColor(legacyParser.getLevel(event)))
+ .setAuthor({
+ name: event.project_name,
+ iconURL: "https://sentrydiscord.dev/icons/sentry.png",
+ })
+ .setFooter({
+ text: "Please consider sponsoring us!",
+ iconURL: "https://sentrydiscord.dev/sponsor.png",
+ })
+ .setTimestamp(legacyParser.getTime(event));
+
+ const projectName = legacyParser.getProject(event);
+
+ const eventTitle = legacyParser.getTitle(event);
+
+ if (projectName) {
+ const embedTitle = `[${projectName}] ${eventTitle}`;
+ embed.setTitle(cap(embedTitle, 250));
+ } else {
+ embed.setTitle(cap(eventTitle, 250));
+ }
+
+ const link = legacyParser.getLink(event);
+ if (link.startsWith("https://") || link.startsWith("http://")) {
+ embed.setURL(legacyParser.getLink(event));
+ }
+
+ const fileLocation = legacyParser.getFileLocation(event);
+ const snippet = cap(legacyParser.getErrorCodeSnippet(event), 3900);
+
+ if (snippet) {
+ embed.setDescription(
+ `${fileLocation ? `\`📄 ${fileLocation.slice(-95)}\`\n` : ""}\`\`\`${
+ legacyParser.getLanguage(event) ?? legacyParser.getPlatform(event)
+ }\n${snippet}
+ \`\`\``
+ );
+ } else {
+ embed.setDescription("Unable to generate code snippet.");
+ }
+
+ const fields: APIEmbedField[] = [];
+
+ const location = legacyParser.getErrorLocation(event, 7);
+ if (location?.length > 0) {
+ fields.push({
+ name: "Stack",
+ value: `\`\`\`${cap(location.join("\n"), 1000)}\n\`\`\``,
+ });
+ }
+
+ const user = legacyParser.getUser(event);
+ if (user?.username) {
+ fields.push({
+ name: "User",
+ value: cap(`${user.username} ${user.id ? `(${user.id})` : ""}`, 1024),
+ inline: true,
+ });
+ }
+
+ const tags = legacyParser.getTags(event);
+ if (Object.keys(tags).length > 0) {
+ fields.push({
+ name: "Tags",
+ value: cap(
+ tags.map(([key, value]) => `${key}: ${value}`).join("\n"),
+ 1024
+ ),
+ inline: true,
+ });
+ }
+
+ const extras = legacyParser.getExtras(event);
+ if (extras.length > 0) {
+ fields.push({
+ name: "Extras",
+ value: cap(extras.join("\n"), 1024),
+ inline: true,
+ });
+ }
+
+ const contexts = legacyParser.getContexts(event);
+ if (contexts.length > 0) {
+ fields.push({
+ name: "Contexts",
+ value: cap(contexts.join("\n"), 1024),
+ inline: true,
+ });
+ }
+
+ const release = legacyParser.getRelease(event);
+ if (release) {
+ fields.push({ name: "Release", value: cap(release, 1024), inline: true });
+ }
+
+ embed.addFields(fields);
+ return {
+ username: "Sentry",
+ avatar_url: `https://sentrydiscord.dev/icons/sentry.png`,
+ embeds: [embed.toJSON()],
+ };
+}
diff --git a/pages/api/webhooks/[key].ts b/pages/api/webhooks/[key].ts
index 6edd425..3bd277a 100644
--- a/pages/api/webhooks/[key].ts
+++ b/pages/api/webhooks/[key].ts
@@ -1,7 +1,7 @@
import prisma from '../../../lib/database';
import nextConnect from 'next-connect';
import { getPlatform } from '../../../lib/parser';
-import createMessage from '../../../lib/message';
+import { createMessage, createLegacyMessage } from '../../../lib/message';
import type { NextApiRequest, NextApiResponse } from 'next';
const handler = async (request: NextApiRequest, response: NextApiResponse) => {
@@ -10,15 +10,26 @@ const handler = async (request: NextApiRequest, response: NextApiResponse) => {
try {
let { key, thread_id: threadId } = request.query;
+ // Legacy Integrations do not have a 'sentry-hook-resource' key.
+ // Therefore, we can differentiate between legacy and new integrations with this.
+ const sentryHookResource = request.headers['sentry-hook-resource'];
+
+ if (sentryHookResource && sentryHookResource !== 'event_alert') {
+ // This probably needs to be discussed.
+ console.log(`Received ${sentryHookResource} event for ${key}, ignoring.`);
+ if (process.env.NODE_ENV === 'development' || request.query.debug) {
+ console.log({ body: request.body, headers: request.headers });
+ }
+ return response.status(204);
+ }
+
if (Array.isArray(key)) {
key = key[0];
}
+ console.log(`Received event for ${key}`);
if (process.env.NODE_ENV === 'development' || request.query.debug) {
- console.log(`Received event for ${key}`);
- console.log({ body: request.body });
- } else {
- console.log(`Received event for ${key}`);
+ console.log({ body: request.body, headers: request.headers });
}
if (request.body == null) {
@@ -38,7 +49,9 @@ const handler = async (request: NextApiRequest, response: NextApiResponse) => {
return response.status(404);
}
- message = createMessage(request.body);
+ message = (
+ sentryHookResource ? createMessage(request.body) : createLegacyMessage(request.body)
+ )
console.log('Constructed embed');
console.log({ key });
From 6c10baced361dc2d28e5f614dae0c331790d3cea Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Wed, 2 Jul 2025 17:15:28 +0200
Subject: [PATCH 03/12] feat: type SentryEvent and update the parser for new
Integrations
---
lib/message.ts | 32 ++++----
lib/parser.ts | 212 ++++++++++++++++++++++++++++++++++---------------
2 files changed, 161 insertions(+), 83 deletions(-)
diff --git a/lib/message.ts b/lib/message.ts
index cfc4781..9947962 100644
--- a/lib/message.ts
+++ b/lib/message.ts
@@ -11,12 +11,13 @@ function cap(str: string, length: number) {
return str.substr(0, length - 1) + "\u2026";
}
-export function createMessage(event) {
+export function createMessage(requestBody) {
console.debug("Received new event");
+ const event = parser.getEvent(requestBody);
+
console.debug({
event: parser.getEvent(event),
- project: parser.getProject(event),
platform: parser.getPlatform(event),
language: parser.getLanguage(event),
contexts: parser.getContexts(event),
@@ -36,10 +37,12 @@ export function createMessage(event) {
message: parser.getMessage(event),
});
+ const eventLevel = parser.getLevel(event);
+
const embed = new EmbedBuilder()
- .setColor(getColor(parser.getLevel(event)))
+ .setColor(getColor(eventLevel))
.setAuthor({
- name: event.data.triggered_rule,
+ name: requestBody.triggered_rule ?? "Sentry Event",
iconURL: "https://sentrydiscord.dev/icons/sentry.png",
})
.setFooter({
@@ -48,16 +51,7 @@ export function createMessage(event) {
})
.setTimestamp(parser.getTime(event));
- const projectName = parser.getProject(event);
-
- const eventTitle = parser.getTitle(event);
-
- if (projectName) {
- const embedTitle = `[${projectName}] ${eventTitle}`;
- embed.setTitle(cap(embedTitle, 250));
- } else {
- embed.setTitle(cap(eventTitle, 250));
- }
+ embed.setTitle(cap(parser.getTitle(event), 250));
const link = parser.getLink(event);
if (link.startsWith("https://") || link.startsWith("http://")) {
@@ -67,17 +61,19 @@ export function createMessage(event) {
const fileLocation = parser.getFileLocation(event);
const snippet = cap(parser.getErrorCodeSnippet(event), 3900);
+ let descriptionText = `> ${event.culprit ? `**${eventLevel.toUpperCase()}**: \`${event.culprit.slice(-95)}\` ${event.environment ? `on ${event.environment}` : ""}\n` : ""}\n`;
+
if (snippet) {
- embed.setDescription(
- `${fileLocation ? `\`📄 ${fileLocation.slice(-95)}\`\n` : ""}\`\`\`${
+ descriptionText += `${fileLocation ? `\`📄 ${fileLocation.slice(-95)}\`\n` : ""}\`\`\`${
parser.getLanguage(event) ?? parser.getPlatform(event)
}\n${snippet}
\`\`\``
- );
} else {
- embed.setDescription("Unable to generate code snippet.");
+ descriptionText += "Unable to generate code snippet.";
}
+ embed.setDescription(descriptionText);
+
const fields: APIEmbedField[] = [];
const location = parser.getErrorLocation(event, 7);
diff --git a/lib/parser.ts b/lib/parser.ts
index e1e8e0e..e75052c 100644
--- a/lib/parser.ts
+++ b/lib/parser.ts
@@ -1,33 +1,120 @@
type SentryIssue = Record;
-export function getEvent(issue: SentryIssue) {
- return issue?.event ?? issue?.data?.issue ?? issue?.data?.event ?? issue;
-}
-
-export function getProject(issue: SentryIssue) {
- return issue?.project?.project_name ?? getEvent(issue)?.project?.name;
-}
-
-export function getPlatform(issue: SentryIssue) {
- return getEvent(issue)?.platform;
-}
-
-export function getLanguage(issue: SentryIssue) {
- return getEvent(issue)?.location?.split(".")?.slice(-1)?.[0] || "";
-}
-
-export function getContexts(issue: SentryIssue) {
- const contexts = getEvent(issue)?.contexts ?? {};
- const values = Object.values(contexts)
- .map((value: Record) => `${value?.name} ${value?.version}`)
- // TODO: Have a better decision tree here
- .filter((value) => value !== "undefined undefined");
+type SentryEvent = {
+ event_id: string;
+ project: string;
+ release?: string;
+ dist?: string,
+ platform?: string;
+ message?: string,
+ datetime?: string;
+ tags?: Record;
+ _metrics?: Record;
+ _ref?: number;
+ _ref_version?: number;
+ contexts?: Record;
+ culprit?: string;
+ environment?: string;
+ extra?: Record;
+ fingerprint?: Array;
+ grouping_config?: Record;
+ hashes?: Array;
+ level?: string;
+ location?: string;
+ logentry?: {
+ formatted?: string;
+ message?: string;
+ params?: Array;
+ };
+ logger?: string;
+ metadata?: Record;
+ modules?: Record;
+ nodestore_insert?: number;
+ received?: number;
+ request?: {
+ url?: string;
+ method?: string;
+ headers?: Record;
+ data?: string;
+ env?: Record;
+ inferred_content_type?: string;
+ api_target?: string;
+ cookies?: Record;
+ };
+ stacktrace?: {
+ frames?: Array<{
+ function?: string,
+ module?: string,
+ filename?: string,
+ abs_path?: string,
+ lineno?: number,
+ pre_context?: Array,
+ context_line?: string,
+ post_context?: Array,
+ in_app?: boolean,
+ vars?: Record,
+ colno?: number,
+ data?: Record,
+ errors?: Array,
+ raw_function?: string,
+ image_addr?: string,
+ instruction_addr?: string,
+ addr_mode?: string,
+ package?: string,
+ platform?: string,
+ source_link?: string,
+ symbol?: string,
+ symbol_addr?: string,
+ trust?: boolean,
+ lock?: boolean,
+ }>;
+ };
+ timestamp?: number;
+ title?: string;
+ type?: string;
+ user?: {
+ id?: string;
+ email?: string;
+ ip_address?: string;
+ username?: string;
+ name?: string;
+ geo?: {
+ country_code?: string;
+ region?: string;
+ city?: string;
+ };
+ };
+ version?: string;
+ url?: string;
+ web_url?: string;
+ issue_url?: string;
+ issue_id?: string;
+};
+
+
+export function getEvent(issue: SentryIssue): SentryEvent {
+ return issue?.data?.event ?? issue;
+}
+
+export function getPlatform(event: SentryEvent) {
+ return event?.platform || "unknown";
+}
+
+export function getLanguage(event: SentryEvent) {
+ return event?.location?.split(".")?.slice(-1)?.[0] || "";
+}
+
+export function getContexts(event: SentryEvent) {
+ const contexts = getEvent(event)?.contexts ?? {};
+ const values = Object.values(contexts)
+ .map((value: Record) => `${value?.name} ${value?.version}`)
+ .filter((value) => value !== "undefined undefined");
return values ?? [];
}
-export function getExtras(issue: SentryIssue) {
- const extras = getEvent(issue)?.extra ?? {};
+export function getExtras(event: SentryEvent) {
+ const extras = event?.extra ?? {};
const values = Object.entries(extras).map(
([key, value]) => `${key}: ${value}`
);
@@ -35,65 +122,56 @@ export function getExtras(issue: SentryIssue) {
return values ?? [];
}
-export function getLink(issue: SentryIssue) {
- return issue?.url ?? "https://sentry.io";
+export function getLink(event: SentryEvent) {
+ return event?.url ?? "https://sentry.io";
}
-export function getTags(issue: SentryIssue) {
- return getEvent(issue)?.tags ?? [];
+export function getTags(event: SentryEvent) {
+ return event?.tags ?? [];
}
-export function getLevel(issue: SentryIssue) {
- return getEvent(issue)?.level;
+export function getLevel(event: SentryEvent) {
+ return event?.level;
}
-export function getType(issue: SentryIssue) {
- return getEvent(issue)?.type;
+export function getType(event: SentryEvent) {
+ return event?.type;
}
-export function getTitle(issue: SentryIssue) {
- return getEvent(issue)?.title ?? "Sentry Event";
+export function getTitle(event: SentryEvent) {
+ return event?.title ?? "Sentry Event";
}
-export function getTime(issue: SentryIssue) {
- const event = getEvent(issue);
-
+export function getTime(event: SentryEvent) {
if (event?.timestamp) {
- return new Date(getEvent(issue)?.timestamp * 1000);
- }
-
- if (event?.lastSeen != null) {
- return new Date(event?.lastSeen);
- }
-
- if (event?.firstSeen != null) {
- return new Date(event?.firstSeen);
+ return new Date(event?.timestamp * 1000);
}
return new Date();
}
-export function getRelease(issue: SentryIssue) {
- return getEvent(issue)?.release;
+export function getRelease(event: SentryEvent) {
+ return event?.release;
}
-export function getUser(issue: SentryIssue) {
- return getEvent(issue)?.user;
+export function getUser(event: SentryEvent) {
+ return event?.user;
}
-export function getFileLocation(issue: SentryIssue) {
- return getEvent(issue)?.location;
+export function getFileLocation(event: SentryEvent) {
+ return event?.location;
}
-export function getStacktrace(issue: SentryIssue) {
+export function getStacktrace(event: SentryEvent) {
return (
- getEvent(issue)?.stacktrace ??
- getEvent(issue)?.exception?.values[0]?.stacktrace
+ event?.stacktrace || {
+ frames: [],
+ }
);
}
-export function getErrorLocation(issue: SentryIssue, maxLines = Infinity) {
- const stacktrace = getStacktrace(issue);
+export function getErrorLocation(event: SentryEvent, maxLines = Infinity) {
+ const stacktrace = getStacktrace(event);
const locations = stacktrace?.frames; /*.reverse();*/
let files = locations?.map(
@@ -111,22 +189,26 @@ export function getErrorLocation(issue: SentryIssue, maxLines = Infinity) {
return files;
}
-export function getErrorCodeSnippet(issue: SentryIssue) {
- const stacktrace = getStacktrace(issue);
+export function getErrorCodeSnippet(event: SentryEvent) {
+ const stacktrace = getStacktrace(event);
const location = stacktrace?.frames?.reverse()?.[0];
if (!location) {
- const event = getEvent(issue);
return event?.culprit ?? null;
}
- // The spaces below are intentional - they help align the code
- // aorund the additional `>` marker
+ const startingLine = location.lineno - (location.pre_context?.length ?? 0);
+
return ` ${location.pre_context?.join("\n ") ?? ""}\n>${
location.context_line
- }\n${location.post_context?.join("\n") ?? ""}`;
+ }\n ${location.post_context?.join("\n ") ?? ""}`;
+
+ // TODO: Consider adding line numbers to the code snippet
+ return ` ${location.pre_context?.map((line, index) => `${startingLine - location.pre_context?.length ?? 0 + index} | ${line}`).join("\n ") ?? ""}\n>${location.lineno}>| ${
+ location.context_line
+ }\n${location.post_context?.map((line, index) => `${index + location.lineno + 1} | ${line}`).join("\n ") ?? ""}`;
}
-export function getMessage(issue: SentryIssue) {
- return issue?.message ?? getEvent(issue)?.message;
+export function getMessage(event: SentryEvent) {
+ return event?.message;
}
From f078270322af3606f241544fdf7afd6c93b3772e Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Thu, 3 Jul 2025 12:09:24 +0200
Subject: [PATCH 04/12] update native integration faq
---
pages/index.tsx | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/pages/index.tsx b/pages/index.tsx
index bd07688..5f9a78d 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -266,13 +266,11 @@ export default function Home({ events, webhooks }) {
-
- Me too! There's an{" "}
-
- open issue on GitHub
- {" "}
- that you can go and leave reactions on to help get it
- prioritized. If official support lands, this service will
+
+ Unfortunately, the native Sentry integration for Discord is
+ only available for paid Sentry plans. This service provides
+ a free alternative for everyone! If the native integration
+ ever becomes available for free plans, this service will
likely stop allowing new registrations but will remain up so
long as webhooks are receiving events.
From df5ee4a84cb4df3344eb4ddaa37e750f41a2be12 Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Thu, 3 Jul 2025 12:09:49 +0200
Subject: [PATCH 05/12] update integration-creation flow & include demo
---
pages/create.tsx | 25 +++++++++++++++++--------
1 file changed, 17 insertions(+), 8 deletions(-)
diff --git a/pages/create.tsx b/pages/create.tsx
index 1fb9cc4..2c58ebb 100644
--- a/pages/create.tsx
+++ b/pages/create.tsx
@@ -212,15 +212,24 @@ export default function Create() {
Finally, add the Webhook Integration to Sentry
-
- You can find it under Settings →{' '}
- Integrations → Webhooks.
- Add it to your project, and then in the{' '}
- Configure screen add the above link to the{' '}
- Callback URLs. That's it! Save your changes,
- and click "Test plugin" to see it in action.
+
+ -
+ Create a new integration by going to Settings → Custom Integrations → Create New Integration, and selecting Internal Integration.
+
+ -
+ Paste the above link into the Webhook URL field and enable Alert Rule Action.
+
+ -
+ Go to Alerts → Create Alert and set up a new rule to Send a notification via an integration, and choose the integration you just created.
+
+
+
+ Confused? Check out the demo below for a walkthrough!
-
+
+
+
+
If you would like to target a specific thread on Discord, you
can add ?thread_id=123 to the URL you paste
into Sentry (replacing 123 with the thread ID).
From 1cb40d504d0408279fed2e9c710ecae16337384e Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Thu, 3 Jul 2025 12:17:24 +0200
Subject: [PATCH 06/12] chore: clean-up debugging logs
---
lib/message.ts | 46 ----------------------------------------------
1 file changed, 46 deletions(-)
diff --git a/lib/message.ts b/lib/message.ts
index 9947962..24b669d 100644
--- a/lib/message.ts
+++ b/lib/message.ts
@@ -12,31 +12,8 @@ function cap(str: string, length: number) {
}
export function createMessage(requestBody) {
- console.debug("Received new event");
-
const event = parser.getEvent(requestBody);
- console.debug({
- event: parser.getEvent(event),
- platform: parser.getPlatform(event),
- language: parser.getLanguage(event),
- contexts: parser.getContexts(event),
- extras: parser.getExtras(event),
- link: parser.getLink(event),
- tags: parser.getTags(event),
- level: parser.getLevel(event),
- type: parser.getType(event),
- title: parser.getTitle(event),
- time: parser.getTime(event),
- user: parser.getUser(event),
- release: parser.getRelease(event),
- fileLocation: parser.getFileLocation(event),
- stackTrace: parser.getStacktrace(event),
- errorLocation: parser.getErrorLocation(event, 7),
- errorCodeSnippet: parser.getErrorCodeSnippet(event),
- message: parser.getMessage(event),
- });
-
const eventLevel = parser.getLevel(event);
const embed = new EmbedBuilder()
@@ -137,29 +114,6 @@ export function createMessage(requestBody) {
}
export function createLegacyMessage(event) {
- console.debug("Received legacy event");
-
- console.debug({
- event: legacyParser.getEvent(event),
- project: legacyParser.getProject(event),
- platform: legacyParser.getPlatform(event),
- language: legacyParser.getLanguage(event),
- contexts: legacyParser.getContexts(event),
- extras: legacyParser.getExtras(event),
- link: legacyParser.getLink(event),
- tags: legacyParser.getTags(event),
- level: legacyParser.getLevel(event),
- type: legacyParser.getType(event),
- title: legacyParser.getTitle(event),
- time: legacyParser.getTime(event),
- user: legacyParser.getUser(event),
- release: legacyParser.getRelease(event),
- fileLocation: legacyParser.getFileLocation(event),
- stackTrace: legacyParser.getStacktrace(event),
- errorLocation: legacyParser.getErrorLocation(event, 7),
- errorCodeSnippet: legacyParser.getErrorCodeSnippet(event),
- message: legacyParser.getMessage(event),
- });
const embed = new EmbedBuilder()
.setColor(getColor(legacyParser.getLevel(event)))
.setAuthor({
From d40d5c5a488e76e6eed02d0052c4c155cbfb4c3a Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Thu, 3 Jul 2025 12:26:56 +0200
Subject: [PATCH 07/12] fix(index): remove persisting warning icon
---
pages/index.tsx | 15 ---------------
1 file changed, 15 deletions(-)
diff --git a/pages/index.tsx b/pages/index.tsx
index 5f9a78d..1d45157 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -103,21 +103,6 @@ export default function Home({ events, webhooks }) {
-
-
From da6b0c3bd9d2c7cfa3cef9f2e6a3038eb3b8e3f8 Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Thu, 3 Jul 2025 12:34:24 +0200
Subject: [PATCH 08/12] chore: clean-up line number consideration
---
lib/parser.ts | 7 -------
1 file changed, 7 deletions(-)
diff --git a/lib/parser.ts b/lib/parser.ts
index e75052c..fa78a3b 100644
--- a/lib/parser.ts
+++ b/lib/parser.ts
@@ -196,17 +196,10 @@ export function getErrorCodeSnippet(event: SentryEvent) {
if (!location) {
return event?.culprit ?? null;
}
-
- const startingLine = location.lineno - (location.pre_context?.length ?? 0);
return ` ${location.pre_context?.join("\n ") ?? ""}\n>${
location.context_line
}\n ${location.post_context?.join("\n ") ?? ""}`;
-
- // TODO: Consider adding line numbers to the code snippet
- return ` ${location.pre_context?.map((line, index) => `${startingLine - location.pre_context?.length ?? 0 + index} | ${line}`).join("\n ") ?? ""}\n>${location.lineno}>| ${
- location.context_line
- }\n${location.post_context?.map((line, index) => `${index + location.lineno + 1} | ${line}`).join("\n ") ?? ""}`;
}
export function getMessage(event: SentryEvent) {
From fd22cbfa080cd9fedcdb4fafe0c8b1361b00af4b Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Thu, 3 Jul 2025 22:26:31 +0200
Subject: [PATCH 09/12] fix: incorrect type for tags
---
lib/parser.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/parser.ts b/lib/parser.ts
index fa78a3b..8045166 100644
--- a/lib/parser.ts
+++ b/lib/parser.ts
@@ -8,7 +8,7 @@ type SentryEvent = {
platform?: string;
message?: string,
datetime?: string;
- tags?: Record;
+ tags?: Array<[string, string]>;
_metrics?: Record;
_ref?: number;
_ref_version?: number;
From c7d8fa1d1ab254b19595146d59cd6f9d7a31e273 Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Thu, 3 Jul 2025 22:43:14 +0200
Subject: [PATCH 10/12] fix: correctly capture triggered rule
---
lib/message.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/message.ts b/lib/message.ts
index 24b669d..e512bd2 100644
--- a/lib/message.ts
+++ b/lib/message.ts
@@ -19,7 +19,7 @@ export function createMessage(requestBody) {
const embed = new EmbedBuilder()
.setColor(getColor(eventLevel))
.setAuthor({
- name: requestBody.triggered_rule ?? "Sentry Event",
+ name: requestBody?.data?.triggered_rule ?? "Sentry Event",
iconURL: "https://sentrydiscord.dev/icons/sentry.png",
})
.setFooter({
From b183472ca983ade1d7722f12e4344e02c3ed9763 Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Fri, 4 Jul 2025 12:59:57 +0200
Subject: [PATCH 11/12] fix: link pointing to API, not UI
---
lib/parser.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/parser.ts b/lib/parser.ts
index 8045166..d344574 100644
--- a/lib/parser.ts
+++ b/lib/parser.ts
@@ -123,7 +123,7 @@ export function getExtras(event: SentryEvent) {
}
export function getLink(event: SentryEvent) {
- return event?.url ?? "https://sentry.io";
+ return event?.web_url ?? event?.url ?? "https://sentry.io";
}
export function getTags(event: SentryEvent) {
From 69744e9bab867fa75a2991177fb9275def47ff0d Mon Sep 17 00:00:00 2001
From: Nickyux
Date: Fri, 4 Jul 2025 13:28:36 +0200
Subject: [PATCH 12/12] fix: some stacktraces are within the exception field
---
lib/parser.ts | 61 +++++++++++++++++++++++++++------------------------
1 file changed, 32 insertions(+), 29 deletions(-)
diff --git a/lib/parser.ts b/lib/parser.ts
index d344574..ffcf07e 100644
--- a/lib/parser.ts
+++ b/lib/parser.ts
@@ -1,5 +1,22 @@
type SentryIssue = Record;
+type SentryStacktrace = {
+ frames?: Array<{
+ function?: string;
+ module?: string;
+ filename?: string;
+ abs_path?: string;
+ lineno?: number;
+ pre_context?: Array;
+ context_line?: string;
+ post_context?: Array;
+ in_app?: boolean;
+ vars?: Record;
+ colno?: number;
+ data?: Record;
+ }>;
+};
+
type SentryEvent = {
event_id: string;
project: string;
@@ -41,34 +58,7 @@ type SentryEvent = {
api_target?: string;
cookies?: Record;
};
- stacktrace?: {
- frames?: Array<{
- function?: string,
- module?: string,
- filename?: string,
- abs_path?: string,
- lineno?: number,
- pre_context?: Array,
- context_line?: string,
- post_context?: Array,
- in_app?: boolean,
- vars?: Record,
- colno?: number,
- data?: Record,
- errors?: Array,
- raw_function?: string,
- image_addr?: string,
- instruction_addr?: string,
- addr_mode?: string,
- package?: string,
- platform?: string,
- source_link?: string,
- symbol?: string,
- symbol_addr?: string,
- trust?: boolean,
- lock?: boolean,
- }>;
- };
+ stacktrace?: SentryStacktrace;
timestamp?: number;
title?: string;
type?: string;
@@ -89,6 +79,19 @@ type SentryEvent = {
web_url?: string;
issue_url?: string;
issue_id?: string;
+ exception?: {
+ values: Array<{
+ type?: string;
+ value?: string;
+ module?: string;
+ mechanism?: {
+ type?: string;
+ handled?: boolean;
+ data?: Record;
+ };
+ stacktrace?: SentryStacktrace;
+ }>;
+ }
};
@@ -164,7 +167,7 @@ export function getFileLocation(event: SentryEvent) {
export function getStacktrace(event: SentryEvent) {
return (
- event?.stacktrace || {
+ event?.stacktrace || event?.exception?.values?.[0]?.stacktrace || {
frames: [],
}
);