Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: add checkly tool #20

Merged
merged 8 commits into from
Nov 22, 2024
45 changes: 4 additions & 41 deletions src/aggregator/checkly-aggregator.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,10 @@
import { CheckContext, ContextKey } from "./ContextAggregator";
import { checkly } from "../checkly/client";
import { WebhookAlertDto } from "../checkly/alertDTO";
import { Check, CheckResult } from "../checkly/models";

const getCheckLogs = async (checkId: string, checkResultId: string) => {
const logs = await checkly.getCheckResult(checkId, checkResultId);
console.log("logs");
console.log(logs);

return logs;
};

const mapCheckToContextValue = (check: Check) => {
return {
checkId: check.id,
type: check.checkType,
frequency: check.frequency,
frequencyOffset: check.frequencyOffset,
shouldFail: check.shouldFail,
locations: check.locations,
tags: check.tags,
maxResponseTime: check.maxResponseTime,
sslCheckDomain: check.sslCheckDomain,
retryStrategy: check.retryStrategy,
};
};

const mapCheckResultToContextValue = (result: CheckResult) => {
return {
resultId: result.id,
hasErrors: result.hasErrors,
hasFailures: result.hasFailures,
runLocation: result.runLocation,
startedAt: result.startedAt,
stoppedAt: result.stoppedAt,
responseTime: result.responseTime,
checkId: result.checkId,
attempts: result.attempts,
isDegraded: result.isDegraded,
overMaxResponseTime: result.overMaxResponseTime,
resultType: result.resultType,
};
};
import {
mapCheckResultToContextValue,
mapCheckToContextValue,
} from "../checkly/utils";

export const checklyAggregator = {
fetchContext: async (alert: WebhookAlertDto): Promise<CheckContext[]> => {
Expand Down
2 changes: 0 additions & 2 deletions src/ai/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export const formatToolOutput = (
toolCallId: string,
output: unknown
): RunSubmitToolOutputsParams.ToolOutput => {
console.log("output", output);

return {
output: JSON.stringify(output),
tool_call_id: toolCallId,
Expand Down
33 changes: 33 additions & 0 deletions src/checkly/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Check, CheckResult } from "./models";

export const mapCheckToContextValue = (check: Check) => {
return {
checkId: check.id,
type: check.checkType,
frequency: check.frequency,
frequencyOffset: check.frequencyOffset,
shouldFail: check.shouldFail,
locations: check.locations,
tags: check.tags,
maxResponseTime: check.maxResponseTime,
sslCheckDomain: check.sslCheckDomain,
retryStrategy: check.retryStrategy,
};
};

export const mapCheckResultToContextValue = (result: CheckResult) => {
return {
resultId: result.id,
hasErrors: result.hasErrors,
hasFailures: result.hasFailures,
runLocation: result.runLocation,
startedAt: result.startedAt,
stoppedAt: result.stoppedAt,
responseTime: result.responseTime,
checkId: result.checkId,
attempts: result.attempts,
isDegraded: result.isDegraded,
overMaxResponseTime: result.overMaxResponseTime,
resultType: result.resultType,
};
};
2 changes: 1 addition & 1 deletion src/routes/checklywebhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ router.post("/", async (req: Request, res: Response) => {
});

await app.client.chat.postMessage({
channel: "C07V9GNU9L6",
channel: process.env.SLACK_ALERT_CHANNEL_ID as string,
metadata: {
event_type: "alert",
event_payload: {
Expand Down
190 changes: 98 additions & 92 deletions src/slackbot/app.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,26 @@
import { App, LogLevel } from "@slack/bolt";
import { App } from "@slack/bolt";
import { getOpenaiClient, getOpenaiSDKClient } from "../ai/openai";
import { getRunMessages } from "../ai/utils";
import { SreAssistant } from "../sre-assistant/SreAssistant";
import { getSlackConfig, validateConfig } from "./config";
import { getThreadMetadata } from "./utils";
import GitHubAPI from "../github/github";
import { GithubAgent } from "../github/agent";
import moment from "moment";
import { createReleaseBlock, divider as releaseDivider, releaseHeader } from "../github/slackBlock";

export const app = new App({
signingSecret: process.env.SLACK_SIGNING_SECRET,
token: process.env.SLACK_AUTH_TOKEN,
appToken: process.env.SLACK_APP_TOKEN,
socketMode: true,
logLevel:
process.env.NODE_ENV !== "production" ? LogLevel.DEBUG : LogLevel.INFO,
});

app.command("/help123", async ({ command, ack }) => {
await ack();
await app.client.chat.postEphemeral({
channel: command.channel_id,
text: "hey",
user: command.user_id,
});
});

app.message(`hey help`, async ({ message, context }) => {
await app.client.chat.postEphemeral({
channel: message.channel,
text: "e",
user: context.userId!,
});
});

app.message("Hey SREBot", async ({ say }) => {
await say("helloworld");
});
import {
createReleaseBlock,
divider as releaseDivider,
releaseHeader,
} from "../github/slackBlock";

// Initialize Slack app with validated configuration
const initializeSlackApp = () => {
const config = getSlackConfig();
validateConfig(config);
return new App(config);
};

app.message("whatismyuserid", async ({ context, say }) => {
await say(context.userId!);
});
export const app = initializeSlackApp();

let setupAgent = () => {
const CHECKLY_GITHUB_TOKEN = process.env.CHECKLY_GITHUB_TOKEN!;
Expand All @@ -53,70 +34,81 @@ let setupAgent = () => {
const githubAgent = setupAgent();

app.command("/srebot-releases", async ({ command, ack, respond }) => {
await ack();
let summaries = await githubAgent.summarizeReleases(command.text, 'checkly');
if (summaries.releases.length === 0) {
await respond({ text: `No releases found in repo ${summaries.repo} since ${summaries.since}`});
}

let releases = summaries.releases.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime()); let response = [releaseHeader].concat(releases.map(summary => {
const date = moment(summary.release_date).fromNow();
const authors = summary.authors.filter(author => author !== null).map(author => author.login)
return createReleaseBlock({
release: summary.id,
releaseUrl: summary.link,
diffUrl: summary.diffLink,
date,
repo: summaries.repo.name,
repoUrl: summaries.repo.link,
authors,
summary: summary.summary
}).blocks as any;
}).reduce((prev, curr) => {
if (!prev) {
return curr;
}

return prev.concat([releaseDivider]).concat(curr);
}));

await respond({
blocks: response
});
})
await ack();
let summaries = await githubAgent.summarizeReleases(command.text, "checkly");
if (summaries.releases.length === 0) {
await respond({
text: `No releases found in repo ${summaries.repo} since ${summaries.since}`,
});
}

let releases = summaries.releases.sort(
(a, b) =>
new Date(b.release_date).getTime() - new Date(a.release_date).getTime()
);
let response = [releaseHeader].concat(
releases
.map((summary) => {
const date = moment(summary.release_date).fromNow();
const authors = summary.authors
.filter((author) => author !== null)
.map((author) => author.login);
return createReleaseBlock({
release: summary.id,
releaseUrl: summary.link,
diffUrl: summary.diffLink,
date,
repo: summaries.repo.name,
repoUrl: summaries.repo.link,
authors,
summary: summary.summary,
}).blocks as any;
})
.reduce((prev, curr) => {
if (!prev) {
return curr;
}

return prev.concat([releaseDivider]).concat(curr);
})
);

await respond({
blocks: response,
});
});

app.event("app_mention", async ({ event, context }) => {
try {
let threadId;
let alertId = "test";
let threadId, alertId;
const threadTs = (event as any).thread_ts || event.ts;

// Handle threaded conversations
if ((event as any).thread_ts) {
try {
const result = await app.client.conversations.replies({
channel: event.channel,
ts: (event as any).thread_ts,
limit: 1,
include_all_metadata: true,
});
if (result.messages && result.messages.length > 0) {
const metadata = result.messages[0].metadata?.event_payload as {
threadId: string;
alertId: string;
};

threadId = metadata?.threadId;
alertId = metadata?.alertId;
}

const { threadId: existingThreadId, alertId: existingAlertId } =
await getThreadMetadata(result.messages || []);

threadId = existingThreadId;
alertId = existingAlertId;
} catch (error) {
console.error("Error fetching parent message:", error);
console.error("Error fetching thread replies:", error);
}
}

// Create new thread if needed
if (!threadId) {
const thread = await getOpenaiClient().beta.threads.create();
threadId = thread.id;
}

// Initialize assistant and process message
const assistant = new SreAssistant(threadId, alertId, {
username:
event.user_profile?.display_name ||
Expand All @@ -125,28 +117,42 @@ app.event("app_mention", async ({ event, context }) => {
"Unknown User",
date: new Date().toISOString(),
});
const userMessage = await assistant.addMessage(event.text);
const responseMessages = await assistant
.runSync()
.then((run) => getRunMessages(threadId, run.id));

const send = async (msg: string) => {
await assistant.addMessage(event.text);
const run = await assistant.runSync();
const responseMessages = await getRunMessages(threadId, run.id);

const sendMessage = (msg: string) =>
app.client.chat.postMessage({
token: context.botToken,
channel: event.channel,
text: msg,
thread_ts: (event as any).thread_ts || event.ts,
thread_ts: threadTs,
...(threadId && {
metadata: {
event_type: "alert",
event_payload: { threadId },
},
}),
});
};

await responseMessages.map((msg) =>
send(
msg.content
.map((c) => (c.type === "text" ? c.text.value : ""))
.join("\n")
await Promise.all(
responseMessages.map((msg) =>
sendMessage(
msg.content
.filter((c) => c.type === "text")
.map((c) => (c as any).text.value)
.join("")
)
)
);
} catch (error) {
console.error("Error reacting to mention:", error);
console.error("Error processing app mention:", error);
await app.client.chat.postMessage({
token: context.botToken,
channel: event.channel,
text: "Sorry, I encountered an error while processing your request.",
thread_ts: (event as any).thread_ts || event.ts,
});
}
});
36 changes: 36 additions & 0 deletions src/slackbot/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { LogLevel } from "@slack/bolt";

interface SlackConfig {
signingSecret: string;
token: string;
appToken: string;
socketMode: boolean;
logLevel: LogLevel;
}

export const getSlackConfig = (): SlackConfig => ({
signingSecret: process.env.SLACK_SIGNING_SECRET!,
token: process.env.SLACK_AUTH_TOKEN!,
appToken: process.env.SLACK_APP_TOKEN!,
socketMode: true,
logLevel:
process.env.NODE_ENV !== "production" ? LogLevel.DEBUG : LogLevel.INFO,
});

export const validateConfig = (config: SlackConfig): void => {
const requiredEnvVars = [
"SLACK_SIGNING_SECRET",
"SLACK_AUTH_TOKEN",
"SLACK_APP_TOKEN",
];

const missingVars = requiredEnvVars.filter(
(varName) => !process.env[varName]
);

if (missingVars.length > 0) {
throw new Error(
`Missing required environment variables: ${missingVars.join(", ")}`
);
}
};
Loading