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.
11 kB ยท 394 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395import { 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, approvePostmortem, draftPostmortem, generatePostmortemDraft, getPostmortem, afterIncidentClosed, afterIncidentDeclared, afterIncidentDeleted, afterIncidentStatusChanged, afterIncidentUpdated, afterPostmortemApproved, bindIncidentSlackChannel, closeIncident, declareIncident, deleteIncident, getIncident, getIncidentForStatusReport, getIncidentOrThrow, linkIncidentStatusReport, listIncidentEvents, listIncidents, listLinkedStatusReportIds, type IncidentEffects, incidentEffects, resolveDashboardUrl, setIncidentStatus, unbindIncidentSlackChannel, unlinkIncidentStatusReport, updateIncident,} from "@openstatus/services/incident";import { WebClient } from "@slack/web-api";import { TRPCError } from "@trpc/server";import { generateText } from "ai";import { z } from "zod";
import { createAfterResponse } from "../lib/after-response";import { chatRateLimit } from "../lib/chat-rate-limit";import { toServiceCtx, toTRPCError } from "../service-adapter";import { createTRPCRouter, protectedProcedure } from "../trpc";
const DASHBOARD_URL = resolveDashboardUrl({ nodeEnv: process.env.NODE_ENV, override: process.env.DASHBOARD_URL,});
type AuthedCtx = Parameters<typeof toServiceCtx>[0];
/** Follow-ups run after the response; the incident is already saved. */const afterResponse = createAfterResponse("incident follow-up");
function effectsFor(ctx: AuthedCtx): IncidentEffects { return incidentEffects({ clientFor: (token) => new WebClient(token), dashboardUrl: DASHBOARD_URL, sendCommanderEmail: sendIncidentCommander, name: ctx.user.name || ctx.user.email || "A teammate", });}
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 })).items; } catch (err) { toTRPCError(err); } }),
get: protectedProcedure .input(IncidentIdInput) .query(async ({ ctx, input }) => { try { return await getIncidentOrThrow({ ctx: toServiceCtx(ctx), input }); } 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); } }),
linkedStatusReportIds: protectedProcedure.query(async ({ ctx }) => { try { return await listLinkedStatusReportIds({ ctx: toServiceCtx(ctx) }); } 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, }); afterResponse(() => afterIncidentDeclared({ ctx: toServiceCtx(ctx), effects: effectsFor(ctx), incident: row, openSlackChannel: openSlackChannel ?? false, }), ); 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 }); if (before) { afterResponse(() => afterIncidentUpdated({ ctx: toServiceCtx(ctx), effects: effectsFor(ctx), before, after: row, }), ); } 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 }); afterResponse(() => afterIncidentStatusChanged({ ctx: toServiceCtx(ctx), effects: effectsFor(ctx), incidentId: row.id, status: row.status, note: input.note, }), ); return row; } catch (err) { toTRPCError(err); } }),
addNote: protectedProcedure .meta({ track: Events.AddManagedIncidentNote }) // `createdBy` is for trusted callers copying a note in (Slack pins). .input(AddIncidentNoteInput.omit({ createdBy: true })) .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 }) // An agent draft goes through `draftPostmortemWithAgent`; this is a person typing. .input(DraftPostmortemInput.omit({ draftedBy: true })) .mutation(async ({ ctx, input }) => { try { return await draftPostmortem({ ctx: toServiceCtx(ctx), input: { ...input, draftedBy: "user" }, }); } 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 }); afterResponse(() => afterPostmortemApproved({ ctx: toServiceCtx(ctx), effects: effectsFor(ctx), incidentId: input.id, closed: !!before && !before.closedAt, }), ); 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 }); afterResponse(() => afterIncidentClosed({ ctx: toServiceCtx(ctx), effects: effectsFor(ctx), incidentId: row.id, }), ); 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) { afterResponse(() => afterIncidentDeleted({ ctx: toServiceCtx(ctx), effects: effectsFor(ctx), before, }), ); } } catch (err) { toTRPCError(err); } }),});