Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
14 kB ยท 462 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463import { resolveChatModel } from "@openstatus/ai";import { Events } from "@openstatus/analytics";import { incidentStatus } from "@openstatus/db/src/schema/incidents/constants";import { sendIncidentCommander } from "@openstatus/emails";import { AddIncidentNoteInput, ApprovePostmortemInput, BindIncidentSlackChannelInput, CloseIncidentInput, DeclareIncidentInput, DraftPostmortemInput, IncidentIdInput, LinkIncidentStatusReportInput, SetIncidentStatusInput, UpdateIncidentInput, addIncidentNote, allowedTransitions, approvePostmortem, draftPostmortem, generatePostmortemDraft, getPostmortem, announceIncidentChange, announceInChannel, bindIncidentSlackChannel, closeIncident, declareIncident, deleteIncident, displayName, escapeMrkdwn, getIncident, getIncidentForStatusReport, isDeletable, linkIncidentStatusReport, listIncidentEvents, listIncidents, openIncidentSlackChannel, setIncidentStatus, unbindIncidentSlackChannel, unlinkIncidentStatusReport, updateIncident,} from "@openstatus/services/incident";import { WebClient } from "@slack/web-api";import { TRPCError } from "@trpc/server";import { generateText } from "ai";import { after } from "next/server.js";import { z } from "zod";
import { chatRateLimit } from "../lib/chat-rate-limit";import { toServiceCtx, toTRPCError } from "../service-adapter";import { createTRPCRouter, protectedProcedure } from "../trpc";
const DASHBOARD_URL = process.env.NODE_ENV === "production" ? "https://app.openstatus.dev" : "http://localhost:3001";
const clientFor = (token: string) => new WebClient(token);
type AuthedCtx = Parameters<typeof toServiceCtx>[0];
/** Slack follow-ups run after the response; the incident is already saved. */function afterResponse(task: () => Promise<void>) { const run = () => task().catch((err) => console.warn("incident slack follow-up failed", err)); try { after(run); } catch { void run(); }}
function announce( ctx: AuthedCtx, incidentId: number, text: string, archive = false,) { afterResponse(() => announceIncidentChange({ ctx: toServiceCtx(ctx), incidentId, text, clientFor, dashboardUrl: DASHBOARD_URL, archive, }), );}
function actorName(ctx: AuthedCtx): string { return escapeMrkdwn(ctx.user.name || ctx.user.email || "A teammate");}
function quote(note: string | undefined): string { return note ? `\n>${escapeMrkdwn(note).replaceAll("\n", "\n>")}` : "";}
/** Best effort: a failed email never fails the mutation that assigned them. */async function notifyCommander(ctx: AuthedCtx, incidentId: number) { try { const row = await getIncident({ ctx: toServiceCtx(ctx), input: { id: incidentId }, }); const commander = row?.commander; if (!row || !commander?.email || commander.id === ctx.user.id) return; await sendIncidentCommander({ to: commander.email, incidentTitle: row.title, severity: row.severity, workspaceName: ctx.workspace.name ?? ctx.workspace.slug, assignedBy: actorName(ctx), url: `${DASHBOARD_URL}/incidents/${row.id}`, idempotencyKey: `incident-commander:${row.id}:${commander.id}:${row.updatedAt.getTime()}`, }); } catch (err) { console.warn("incident commander email failed", { incidentId, err }); }}
export const incidentRouter = createTRPCRouter({ list: protectedProcedure .input( z .object({ status: z.array(z.enum(incidentStatus)).optional(), limit: z.number().int().min(1).max(100).optional(), offset: z.number().int().min(0).optional(), }) .optional(), ) .query(async ({ ctx, input }) => { try { return await listIncidents({ ctx: toServiceCtx(ctx), input }); } catch (err) { toTRPCError(err); } }),
get: protectedProcedure .input(IncidentIdInput) .query(async ({ ctx, input }) => { try { const row = await getIncident({ ctx: toServiceCtx(ctx), input }); if (!row) { throw new TRPCError({ code: "NOT_FOUND", message: "Incident not found", }); } return { ...row, allowedTransitions: allowedTransitions(row), deletable: isDeletable(row), }; } catch (err) { toTRPCError(err); } }),
forStatusReport: protectedProcedure .input(z.object({ statusReportId: z.number().int() })) .query(async ({ ctx, input }) => { try { return ( (await getIncidentForStatusReport({ ctx: toServiceCtx(ctx), input, })) ?? null ); } catch (err) { toTRPCError(err); } }),
listEvents: protectedProcedure .input(IncidentIdInput) .query(async ({ ctx, input }) => { try { return await listIncidentEvents({ ctx: toServiceCtx(ctx), input }); } catch (err) { toTRPCError(err); } }),
declare: protectedProcedure .meta({ track: Events.DeclareManagedIncident, trackProps: ["severity"] }) .input( DeclareIncidentInput.extend({ openSlackChannel: z.boolean().optional() }), ) .mutation(async ({ ctx, input }) => { try { const { openSlackChannel, ...declare } = input; const row = await declareIncident({ ctx: toServiceCtx(ctx), input: declare, }); if (row.commanderId !== null) await notifyCommander(ctx, row.id); if (openSlackChannel) { afterResponse(async () => { await openIncidentSlackChannel({ ctx: toServiceCtx(ctx), incidentId: row.id, clientFor, dashboardUrl: DASHBOARD_URL, }); }); } return row; } catch (err) { toTRPCError(err); } }),
update: protectedProcedure .meta({ track: Events.UpdateManagedIncident }) .input(UpdateIncidentInput) .mutation(async ({ ctx, input }) => { try { const before = await getIncident({ ctx: toServiceCtx(ctx), input: { id: input.id }, }); const row = await updateIncident({ ctx: toServiceCtx(ctx), input }); const commanderChanged = row.commanderId !== before?.commanderId; if (row.commanderId !== null && commanderChanged) { await notifyCommander(ctx, row.id); } const changes: string[] = []; if (before && row.title !== before.title) { changes.push(`title is now *${escapeMrkdwn(row.title)}*`); } if (before && row.summary !== before.summary) { changes.push( row.summary ? "summary was updated" : "summary was removed", ); } if (before && row.startedAt.getTime() !== before.startedAt.getTime()) { const seconds = Math.floor(row.startedAt.getTime() / 1000); changes.push( `start time is now <!date^${seconds}^{date_short_pretty} {time}|${row.startedAt.toISOString()}>`, ); } if (before && row.severity !== before.severity) { changes.push(`severity is now *${row.severity}*`); } if (before && commanderChanged) { const after = await getIncident({ ctx: toServiceCtx(ctx), input: { id: row.id }, }); changes.push( after?.commander ? `${escapeMrkdwn(displayName(after.commander))} is now commander` : "there is no commander", ); } if (changes.length) { announce(ctx, row.id, `${actorName(ctx)}: ${changes.join(", ")}.`); } return row; } catch (err) { toTRPCError(err); } }),
setStatus: protectedProcedure .meta({ track: Events.ChangeManagedIncidentStatus, trackProps: ["status"] }) .input(SetIncidentStatusInput) .mutation(async ({ ctx, input }) => { try { const row = await setIncidentStatus({ ctx: toServiceCtx(ctx), input }); announce( ctx, row.id, `${actorName(ctx)} marked the incident *${row.status}*.${quote(input.note)}`, row.status === "canceled", ); return row; } catch (err) { toTRPCError(err); } }),
addNote: protectedProcedure .meta({ track: Events.AddManagedIncidentNote }) .input(AddIncidentNoteInput) .mutation(async ({ ctx, input }) => { try { return await addIncidentNote({ ctx: toServiceCtx(ctx), input }); } catch (err) { toTRPCError(err); } }),
linkStatusReport: protectedProcedure .meta({ track: Events.LinkManagedIncidentReport }) .input(LinkIncidentStatusReportInput) .mutation(async ({ ctx, input }) => { try { return await linkIncidentStatusReport({ ctx: toServiceCtx(ctx), input, }); } catch (err) { toTRPCError(err); } }),
unlinkStatusReport: protectedProcedure .input(IncidentIdInput) .mutation(async ({ ctx, input }) => { try { return await unlinkIncidentStatusReport({ ctx: toServiceCtx(ctx), input, }); } catch (err) { toTRPCError(err); } }),
bindSlackChannel: protectedProcedure .input(BindIncidentSlackChannelInput) .mutation(async ({ ctx, input }) => { try { return await bindIncidentSlackChannel({ ctx: toServiceCtx(ctx), input, }); } catch (err) { toTRPCError(err); } }),
unbindSlackChannel: protectedProcedure .input(IncidentIdInput) .mutation(async ({ ctx, input }) => { try { return await unbindIncidentSlackChannel({ ctx: toServiceCtx(ctx), input, }); } catch (err) { toTRPCError(err); } }),
getPostmortem: protectedProcedure .input(IncidentIdInput) .query(async ({ ctx, input }) => { try { return (await getPostmortem({ ctx: toServiceCtx(ctx), input })) ?? null; } catch (err) { toTRPCError(err); } }),
draftPostmortemWithAgent: protectedProcedure .meta({ track: Events.DraftManagedPostmortem }) .input(IncidentIdInput) .mutation(async ({ ctx, input }) => { const model = resolveChatModel({ plan: ctx.workspace.plan }); if (!model) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "The agent is not configured on this server", }); } // Shares the chat's per-user daily budget: one draft is several calls. const limit = await chatRateLimit({ ctx: toServiceCtx(ctx) }); if (!limit.success) { throw new TRPCError({ code: "TOO_MANY_REQUESTS", message: `Rate limit exceeded. Reset at ${new Date(limit.reset).toISOString()}`, }); } try { return await generatePostmortemDraft({ ctx: toServiceCtx(ctx), incidentId: input.id, generate: async ({ system, prompt }) => (await generateText({ model, system, prompt })).text, slackFor: (token) => new WebClient(token), }); } catch (err) { toTRPCError(err); } }),
draftPostmortem: protectedProcedure .meta({ track: Events.DraftManagedPostmortem }) .input(DraftPostmortemInput) .mutation(async ({ ctx, input }) => { try { return await draftPostmortem({ ctx: toServiceCtx(ctx), input }); } catch (err) { toTRPCError(err); } }),
approvePostmortem: protectedProcedure .meta({ track: Events.ApproveManagedPostmortem }) .input(ApprovePostmortemInput) .mutation(async ({ ctx, input }) => { try { const before = input.close ? await getIncident({ ctx: toServiceCtx(ctx), input }) : undefined; const row = await approvePostmortem({ ctx: toServiceCtx(ctx), input }); // Only announce (and archive) when this approval actually closed it. if (before && !before.closedAt) { announce( ctx, input.id, `${actorName(ctx)} approved the postmortem and closed the incident.`, true, ); } return row; } catch (err) { toTRPCError(err); } }),
close: protectedProcedure .meta({ track: Events.CloseManagedIncident }) .input(CloseIncidentInput) .mutation(async ({ ctx, input }) => { try { const row = await closeIncident({ ctx: toServiceCtx(ctx), input }); announce(ctx, row.id, `${actorName(ctx)} closed the incident.`, true); return row; } catch (err) { toTRPCError(err); } }),
delete: protectedProcedure .meta({ track: Events.DeleteManagedIncident }) .input(IncidentIdInput) .mutation(async ({ ctx, input }) => { try { const before = await getIncident({ ctx: toServiceCtx(ctx), input }); await deleteIncident({ ctx: toServiceCtx(ctx), input }); if (before?.slackChannelId) { afterResponse(() => announceInChannel({ ctx: toServiceCtx(ctx), incident: before, text: `${actorName(ctx)} deleted this incident: it was declared by mistake.`, clientFor, dashboardUrl: DASHBOARD_URL, archive: true, }), ); } } catch (err) { toTRPCError(err); } }),});