Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions lib/legacyParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
type SentryIssue = Record<string, any>;

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<string, unknown>) => `${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;
}
135 changes: 119 additions & 16 deletions lib/message.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -10,11 +11,15 @@ function cap(str: string, length: number) {
return str.substr(0, length - 1) + "\u2026";
}

export default function createMessage(event) {
export function createMessage(requestBody) {
const event = parser.getEvent(requestBody);

const eventLevel = parser.getLevel(event);

const embed = new EmbedBuilder()
.setColor(getColor(parser.getLevel(event)))
.setColor(getColor(eventLevel))
.setAuthor({
name: event.project_name,
name: requestBody?.data?.triggered_rule ?? "Sentry Event",
iconURL: "https://sentrydiscord.dev/icons/sentry.png",
})
.setFooter({
Expand All @@ -23,9 +28,107 @@ export default function createMessage(event) {
})
.setTimestamp(parser.getTime(event));

const projectName = parser.getProject(event);
embed.setTitle(cap(parser.getTitle(event), 250));

const link = parser.getLink(event);
if (link.startsWith("https://") || link.startsWith("http://")) {
embed.setURL(parser.getLink(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) {
descriptionText += `${fileLocation ? `\`📄 ${fileLocation.slice(-95)}\`\n` : ""}\`\`\`${
parser.getLanguage(event) ?? parser.getPlatform(event)
}\n${snippet}
\`\`\``
} else {
descriptionText += "Unable to generate code snippet.";
}

embed.setDescription(descriptionText);

const fields: APIEmbedField[] = [];

const location = parser.getErrorLocation(event, 7);
if (location?.length > 0) {
fields.push({
name: "Stack",
value: `\`\`\`${cap(location.join("\n"), 1000)}\n\`\`\``,
});
}

const user = parser.getUser(event);
if (user?.username) {
fields.push({
name: "User",
value: cap(`${user.username} ${user.id ? `(${user.id})` : ""}`, 1024),
inline: true,
});
}

const tags = parser.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 = parser.getExtras(event);
if (extras.length > 0) {
fields.push({
name: "Extras",
value: cap(extras.join("\n"), 1024),
inline: true,
});
}

const contexts = parser.getContexts(event);
if (contexts.length > 0) {
fields.push({
name: "Contexts",
value: cap(contexts.join("\n"), 1024),
inline: true,
});
}

const release = parser.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()],
};
}

export function createLegacyMessage(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 = parser.getTitle(event);
const eventTitle = legacyParser.getTitle(event);

if (projectName) {
const embedTitle = `[${projectName}] ${eventTitle}`;
Expand All @@ -34,18 +137,18 @@ export default function createMessage(event) {
embed.setTitle(cap(eventTitle, 250));
}

const link = parser.getLink(event);
const link = legacyParser.getLink(event);
if (link.startsWith("https://") || link.startsWith("http://")) {
embed.setURL(parser.getLink(event));
embed.setURL(legacyParser.getLink(event));
}

const fileLocation = parser.getFileLocation(event);
const snippet = cap(parser.getErrorCodeSnippet(event), 3900);
const fileLocation = legacyParser.getFileLocation(event);
const snippet = cap(legacyParser.getErrorCodeSnippet(event), 3900);

if (snippet) {
embed.setDescription(
`${fileLocation ? `\`📄 ${fileLocation.slice(-95)}\`\n` : ""}\`\`\`${
parser.getLanguage(event) ?? parser.getPlatform(event)
legacyParser.getLanguage(event) ?? legacyParser.getPlatform(event)
}\n${snippet}
\`\`\``
);
Expand All @@ -55,15 +158,15 @@ export default function createMessage(event) {

const fields: APIEmbedField[] = [];

const location = parser.getErrorLocation(event, 7);
const location = legacyParser.getErrorLocation(event, 7);
if (location?.length > 0) {
fields.push({
name: "Stack",
value: `\`\`\`${cap(location.join("\n"), 1000)}\n\`\`\``,
});
}

const user = parser.getUser(event);
const user = legacyParser.getUser(event);
if (user?.username) {
fields.push({
name: "User",
Expand All @@ -72,7 +175,7 @@ export default function createMessage(event) {
});
}

const tags = parser.getTags(event);
const tags = legacyParser.getTags(event);
if (Object.keys(tags).length > 0) {
fields.push({
name: "Tags",
Expand All @@ -84,7 +187,7 @@ export default function createMessage(event) {
});
}

const extras = parser.getExtras(event);
const extras = legacyParser.getExtras(event);
if (extras.length > 0) {
fields.push({
name: "Extras",
Expand All @@ -93,7 +196,7 @@ export default function createMessage(event) {
});
}

const contexts = parser.getContexts(event);
const contexts = legacyParser.getContexts(event);
if (contexts.length > 0) {
fields.push({
name: "Contexts",
Expand All @@ -102,7 +205,7 @@ export default function createMessage(event) {
});
}

const release = parser.getRelease(event);
const release = legacyParser.getRelease(event);
if (release) {
fields.push({ name: "Release", value: cap(release, 1024), inline: true });
}
Expand Down
Loading