diff --git a/apps/docs/images/notification/pagerduty/pagerduty-1.png b/apps/docs/images/notification/pagerduty/pagerduty-1.png
new file mode 100644
index 0000000000..f91f8f5319
Binary files /dev/null and b/apps/docs/images/notification/pagerduty/pagerduty-1.png differ
diff --git a/apps/docs/images/notification/pagerduty/pagerduty-2.png b/apps/docs/images/notification/pagerduty/pagerduty-2.png
new file mode 100644
index 0000000000..ac2f113717
Binary files /dev/null and b/apps/docs/images/notification/pagerduty/pagerduty-2.png differ
diff --git a/apps/docs/images/notification/pagerduty/pagerduty-3.png b/apps/docs/images/notification/pagerduty/pagerduty-3.png
new file mode 100644
index 0000000000..3b00e96fe4
Binary files /dev/null and b/apps/docs/images/notification/pagerduty/pagerduty-3.png differ
diff --git a/apps/docs/mint.json b/apps/docs/mint.json
index ddac9e143e..8af1ed9aed 100644
--- a/apps/docs/mint.json
+++ b/apps/docs/mint.json
@@ -93,6 +93,7 @@
"group": "Notification Channels",
"pages": [
"synthetic/features/notification/discord",
+ "synthetic/features/notification/pagerduty",
"synthetic/features/notification/phone-call",
"synthetic/features/notification/slack",
"synthetic/features/notification/sms",
diff --git a/apps/docs/synthetic/features/notification/pagerduty.mdx b/apps/docs/synthetic/features/notification/pagerduty.mdx
new file mode 100644
index 0000000000..ff28b52f3e
--- /dev/null
+++ b/apps/docs/synthetic/features/notification/pagerduty.mdx
@@ -0,0 +1,41 @@
+---
+title: PagerDuty
+---
+
+Get Notified on PagerDuty when we create an incident.
+
+## How to connect PagerDuty
+
+Go to the Alerts Page . Select `PagerDuty` from the list of available integrations.
+
+
+
+
+
+
+
+You will be redirected to the PagerDuty website to authorize OpenStatus to send notifications to your account.
+
+
+
+
+
+
+Select the service you want to use to send notifications. You can create a new service if you don't have one.
+
+
+
+
+
+You are now connected to PagerDuty. Give your integration a name and save it.
+
+You will receive some notifications if we detect an incident
\ No newline at end of file
diff --git a/apps/server/package.json b/apps/server/package.json
index e541a1e122..76a0032711 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -19,6 +19,7 @@
"@openstatus/error": "workspace:*",
"@openstatus/notification-discord": "workspace:*",
"@openstatus/notification-emails": "workspace:*",
+ "@openstatus/notification-pagerduty": "workspace:*",
"@openstatus/notification-slack": "workspace:*",
"@openstatus/notification-twillio-sms": "workspace:*",
"@openstatus/plans": "workspace:*",
diff --git a/apps/server/src/checker/alerting.ts b/apps/server/src/checker/alerting.ts
index ce597a4223..a67fc172af 100644
--- a/apps/server/src/checker/alerting.ts
+++ b/apps/server/src/checker/alerting.ts
@@ -16,11 +16,13 @@ export const triggerNotifications = async ({
statusCode,
message,
notifType,
+ incidentId,
}: {
monitorId: string;
statusCode?: number;
message?: string;
notifType: "alert" | "recovery";
+ incidentId?: string;
}) => {
console.log(`💌 triggerAlerting for ${monitorId}`);
const notifications = await db
@@ -28,17 +30,17 @@ export const triggerNotifications = async ({
.from(schema.notificationsToMonitors)
.innerJoin(
schema.notification,
- eq(schema.notification.id, schema.notificationsToMonitors.notificationId),
+ eq(schema.notification.id, schema.notificationsToMonitors.notificationId)
)
.innerJoin(
schema.monitor,
- eq(schema.monitor.id, schema.notificationsToMonitors.monitorId),
+ eq(schema.monitor.id, schema.notificationsToMonitors.monitorId)
)
.where(eq(schema.monitor.id, Number(monitorId)))
.all();
for (const notif of notifications) {
console.log(
- `💌 sending notification for ${monitorId} and chanel ${notif.notification.provider} for ${notifType}`,
+ `💌 sending notification for ${monitorId} and chanel ${notif.notification.provider} for ${notifType}`
);
const monitor = selectMonitorSchema.parse(notif.monitor);
switch (notifType) {
@@ -48,6 +50,7 @@ export const triggerNotifications = async ({
notification: selectNotificationSchema.parse(notif.notification),
statusCode,
message,
+ incidentId,
});
break;
case "recovery":
@@ -56,6 +59,7 @@ export const triggerNotifications = async ({
notification: selectNotificationSchema.parse(notif.notification),
statusCode,
message,
+ incidentId,
});
break;
}
diff --git a/apps/server/src/checker/utils.ts b/apps/server/src/checker/utils.ts
index e568f4c977..b9d3de05d5 100644
--- a/apps/server/src/checker/utils.ts
+++ b/apps/server/src/checker/utils.ts
@@ -20,16 +20,23 @@ import {
sendRecovery as sendSmsRecovery,
} from "@openstatus/notification-twillio-sms";
+import {
+ sendAlert as sendPagerdutyAlert,
+ sendRecovery as sendPagerDutyRecovery,
+} from "@openstatus/notification-pagerduty";
+
type SendNotification = ({
monitor,
notification,
statusCode,
message,
+ incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
+ incidentId?: string;
}) => Promise;
type Notif = {
@@ -47,4 +54,8 @@ export const providerToFunction = {
},
discord: { sendAlert: sendDiscordAlert, sendRecovery: sendDiscordRecovery },
sms: { sendAlert: sendSmsAlert, sendRecovery: sendSmsRecovery },
+ pagerduty: {
+ sendAlert: sendPagerdutyAlert,
+ sendRecovery: sendPagerDutyRecovery,
+ },
} satisfies Record;
diff --git a/apps/web/.env.example b/apps/web/.env.example
index d2c8cd5c14..0d92f6b843 100644
--- a/apps/web/.env.example
+++ b/apps/web/.env.example
@@ -19,8 +19,8 @@ DATABASE_URL=file:./../../openstatus-dev.db
DATABASE_AUTH_TOKEN=any-token
# JITSU - no need to touch on local development
-JITSU_HOST="https://your-jitsu-domain.com"
-JITSU_WRITE_KEY="jitsu-key:jitsu-secret"
+# JITSU_HOST="https://your-jitsu-domain.com"
+# JITSU_WRITE_KEY="jitsu-key:jitsu-secret"
# Solves 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', see https://github.com/nextauthjs/next-auth/issues/3580
# NODE_TLS_REJECT_UNAUTHORIZED="0"
@@ -72,4 +72,6 @@ AUTH_GITHUB_SECRET=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
-PLAIN_API_KEY=
\ No newline at end of file
+PLAIN_API_KEY=
+
+PAGERDUTY_APP_ID=
diff --git a/apps/web/package.json b/apps/web/package.json
index 741dd455e5..890ab852bb 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -27,6 +27,7 @@
"@openstatus/notification-discord": "workspace:*",
"@openstatus/notification-emails": "workspace:*",
"@openstatus/notification-slack": "workspace:*",
+ "@openstatus/notification-pagerduty": "workspace:*",
"@openstatus/plans": "workspace:*",
"@openstatus/react": "workspace:*",
"@openstatus/rum": "workspace:*",
diff --git a/apps/web/public/assets/changelog/pagerduty-integration.png b/apps/web/public/assets/changelog/pagerduty-integration.png
new file mode 100644
index 0000000000..f91f8f5319
Binary files /dev/null and b/apps/web/public/assets/changelog/pagerduty-integration.png differ
diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/_components/channel-table.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/_components/channel-table.tsx
new file mode 100644
index 0000000000..482f44f49a
--- /dev/null
+++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/_components/channel-table.tsx
@@ -0,0 +1,85 @@
+import type { Workspace } from "@openstatus/db/src/schema";
+import { getLimit } from "@openstatus/plans";
+import { Button, Separator } from "@openstatus/ui";
+import Link from "next/link";
+
+// FIXME: create a Channel Component within the file to avoid code duplication
+
+interface ChannelTable {
+ workspace: Workspace;
+ disabled?: boolean;
+}
+
+export default function ChannelTable({ workspace, disabled }: ChannelTable) {
+ const isPagerDutyAllowed = getLimit(workspace.plan, "pagerduty");
+ const isSMSAllowed = getLimit(workspace.plan, "sms");
+ return (
+
+
Channels
+
Connect all your channels
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+interface ChannelProps {
+ title: string;
+ description: string;
+ href: string;
+ disabled?: boolean;
+}
+
+function Channel({ title, description, href, disabled }: ChannelProps) {
+ return (
+
+
+
{title}
+
{description}
+
+
+
+ {disabled ? "Create" : Create}
+
+
+
+ );
+}
diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/layout.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/layout.tsx
index 1dee24c143..88f8516bea 100644
--- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/layout.tsx
+++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/layout.tsx
@@ -11,22 +11,11 @@ export default async function Layout({
}: {
children: React.ReactNode;
}) {
- const isLimitReached =
- await api.notification.isNotificationLimitReached.query();
return (
- Create
-
- }
/>
{children}
diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/page.tsx
index 1a90187b2c..16a343f0d7 100644
--- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/page.tsx
+++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/page.tsx
@@ -8,8 +8,10 @@ import { Limit } from "@/components/dashboard/limit";
import { columns } from "@/components/data-table/notification/columns";
import { DataTable } from "@/components/data-table/notification/data-table";
import { api } from "@/trpc/server";
+import ChannelTable from "./_components/channel-table";
export default async function NotificationPage() {
+ const workspace = await api.workspace.getWorkspace.query();
const notifications =
await api.notification.getNotificationsByWorkspace.query();
const isLimitReached =
@@ -17,16 +19,14 @@ export default async function NotificationPage() {
if (notifications.length === 0) {
return (
-
- Create
-
- }
- />
+ <>
+
+
+ >
);
}
@@ -34,6 +34,7 @@ export default async function NotificationPage() {
<>
{isLimitReached ? : null}
+
>
);
}
diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/edit/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/edit/page.tsx
index dae17eb59c..e370760640 100644
--- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/edit/page.tsx
+++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/edit/page.tsx
@@ -1,4 +1,4 @@
-import { NotificationForm } from "@/components/forms/notification/form";
+import { NotificationForm } from "@/components/forms/notification-form";
import { api } from "@/trpc/server";
export default async function EditPage({
@@ -16,6 +16,7 @@ export default async function EditPage({
);
}
diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/[channel]/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/[channel]/page.tsx
new file mode 100644
index 0000000000..c8461ed0a9
--- /dev/null
+++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/[channel]/page.tsx
@@ -0,0 +1,41 @@
+import { ProFeatureAlert } from "@/components/billing/pro-feature-alert";
+import { NotificationForm } from "@/components/forms/notification-form";
+import { api } from "@/trpc/server";
+import { notificationProviderSchema } from "@openstatus/db/src/schema";
+import { getLimit } from "@openstatus/plans";
+import { notFound } from "next/navigation";
+
+export default async function ChannelPage({
+ params,
+}: {
+ params: { channel: string };
+}) {
+ const validation = notificationProviderSchema
+ .exclude(["pagerduty"])
+ .safeParse(params.channel);
+
+ if (!validation.success) notFound();
+
+ const workspace = await api.workspace.getWorkspace.query();
+
+ const provider = validation.data;
+
+ const allowed =
+ provider === "sms" ? getLimit(workspace.plan, provider) : true;
+
+ if (!allowed) return ;
+
+ const isLimitReached =
+ await api.notification.isNotificationLimitReached.query();
+
+ if (isLimitReached)
+ return ;
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/layout.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/layout.tsx
index 18fcbe75ad..6de07383db 100644
--- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/layout.tsx
+++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/layout.tsx
@@ -8,7 +8,10 @@ export default async function Layout({
}) {
return (
-
+
{children}
);
diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/page.tsx
deleted file mode 100644
index 6dbccb5eb9..0000000000
--- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/page.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { NotificationForm } from "@/components/forms/notification/form";
-import { api } from "@/trpc/server";
-
-export default async function EditPage() {
- const workspace = await api.workspace.getWorkspace.query();
-
- return ;
-}
diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/pagerduty/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/pagerduty/page.tsx
new file mode 100644
index 0000000000..42b770c51c
--- /dev/null
+++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/pagerduty/page.tsx
@@ -0,0 +1,45 @@
+import { ProFeatureAlert } from "@/components/billing/pro-feature-alert";
+import { NotificationForm } from "@/components/forms/notification-form";
+import { api } from "@/trpc/server";
+import { PagerDutySchema } from "@openstatus/notification-pagerduty";
+import { getLimit } from "@openstatus/plans";
+import { z } from "zod";
+
+// REMINDER: PagerDuty requires a different workflow, thus the separate page
+
+const searchParamsSchema = z.object({
+ config: z.string().optional(),
+});
+
+export default async function PagerDutyPage({
+ searchParams,
+}: {
+ searchParams: { [key: string]: string | string[] | undefined };
+}) {
+ const workspace = await api.workspace.getWorkspace.query();
+ const params = searchParamsSchema.parse(searchParams);
+
+ if (!params.config) {
+ return Invalid data
;
+ }
+
+ const data = PagerDutySchema.parse(JSON.parse(params.config));
+ if (!data) {
+ return Invalid data
;
+ }
+
+ const allowed = getLimit(workspace.plan, "pagerduty");
+
+ if (!allowed) return ;
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/apps/web/src/components/billing/pro-feature-alert.tsx b/apps/web/src/components/billing/pro-feature-alert.tsx
index 78ec3cd378..dd489ed64b 100644
--- a/apps/web/src/components/billing/pro-feature-alert.tsx
+++ b/apps/web/src/components/billing/pro-feature-alert.tsx
@@ -5,21 +5,26 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { Alert, AlertDescription, AlertTitle } from "@openstatus/ui";
+import type { WorkspacePlan } from "@openstatus/plans";
interface Props {
feature: string;
+ workspacePlan?: WorkspacePlan;
}
-export function ProFeatureAlert({ feature }: Props) {
+export function ProFeatureAlert({ feature, workspacePlan = "pro" }: Props) {
const params = useParams<{ workspaceSlug: string }>();
return (
- {feature} is a Pro feature.
+
+ {feature} is a {workspacePlan} {" "}
+ feature.
+
If you want to use{" "}
- {feature} , please
- upgrade your plan. Go to{" "}
+ {feature}
+ , please upgrade your plan. Go to{" "}
-
-
- Add Notification
-
- Get alerted when your endpoint is down.
-
-
- setOpenDialog(false)}
- workspacePlan={plan}
- />
-
= notificationLimit
- : false;
return (
{/*
@@ -86,8 +80,8 @@ export function SectionNotifications({ form, plan, notifications }: Props) {
])
: field.onChange(
field.value?.filter(
- (value) => value !== item.id,
- ),
+ (value) => value !== item.id
+ )
);
}}
>
@@ -106,34 +100,15 @@ export function SectionNotifications({ form, plan, notifications }: Props) {
))}
{!notifications || notifications.length === 0 ? (
-
Missing notifications.
+
+ Create some notifications first.
+
) : null}
);
}}
/>
-
setOpenDialog(val)}>
-
-
-
- Add Notification Channel
-
-
-
-
-
- Add Notification
-
- Get alerted when your endpoint is down.
-
-
- setOpenDialog(false)}
- workspacePlan={plan}
- />
-
-
);
}
diff --git a/apps/web/src/components/forms/notification-form.tsx b/apps/web/src/components/forms/notification-form.tsx
index 220cf099f5..2b583b6d6a 100644
--- a/apps/web/src/components/forms/notification-form.tsx
+++ b/apps/web/src/components/forms/notification-form.tsx
@@ -10,11 +10,7 @@ import type {
NotificationProvider,
WorkspacePlan,
} from "@openstatus/db/src/schema";
-import {
- insertNotificationSchema,
- notificationProvider,
- notificationProviderSchema,
-} from "@openstatus/db/src/schema";
+import { insertNotificationSchema } from "@openstatus/db/src/schema";
import { sendTestDiscordMessage } from "@openstatus/notification-discord";
import { sendTestSlackMessage } from "@openstatus/notification-slack";
import {
@@ -27,11 +23,6 @@ import {
FormLabel,
FormMessage,
Input,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
} from "@openstatus/ui";
import { LoadingAnimation } from "@/components/loading-animation";
@@ -85,6 +76,15 @@ function getProviderMetaData(provider: NotificationProvider) {
sendTest: null,
plans: ["pro", "team"],
};
+ case "pagerduty":
+ return {
+ dataType: null,
+ placeholder: "",
+ setupDocLink:
+ "https://docs.openstatus.dev/synthetic/features/notification/pagerduty",
+ sendTest: null,
+ plans: ["starter", "pro", "team"],
+ };
default:
return {
@@ -102,6 +102,8 @@ interface Props {
onSubmit?: () => void;
workspacePlan: WorkspacePlan;
nextUrl?: string;
+ provider: NotificationProvider;
+ callbackData?: string;
}
export function NotificationForm({
@@ -109,6 +111,8 @@ export function NotificationForm({
onSubmit: onExternalSubmit,
workspacePlan,
nextUrl,
+ provider,
+ callbackData,
}: Props) {
const [isPending, startTransition] = useTransition();
const [isTestPending, startTestTransition] = useTransition();
@@ -117,20 +121,22 @@ export function NotificationForm({
resolver: zodResolver(insertNotificationSchema),
defaultValues: {
...defaultValues,
+ provider,
name: defaultValues?.name || "",
data: getDefaultProviderData(defaultValues),
},
});
- const watchProvider = form.watch("provider");
const watchWebhookUrl = form.watch("data");
- const providerMetaData = useMemo(
- () => getProviderMetaData(watchProvider),
- [watchProvider],
- );
+ const providerMetaData = getProviderMetaData(provider);
async function onSubmit({ provider, data, ...rest }: InsertNotification) {
startTransition(async () => {
try {
+ if (provider === "pagerduty") {
+ if (callbackData) {
+ data = callbackData;
+ }
+ }
if (data === "") {
form.setError("data", { message: "This field is required" });
return;
@@ -189,49 +195,6 @@ export function NotificationForm({
-
(
-
- Provider
-
- field.onChange(notificationProviderSchema.parse(value))
- }
- defaultValue={field.value}
- >
-
-
-
-
-
-
- {notificationProvider.map((provider) => {
- const isIncluded =
- getProviderMetaData(provider).plans?.includes(
- workspacePlan,
- );
- return (
-
- {provider}
-
- );
- })}
-
-
-
- What channel/provider to send a notification.
-
-
-
- )}
- />
)}
/>
- {watchProvider && (
+
+ {providerMetaData.dataType && (
{/* make the first letter capital */}
- {toCapitalize(watchProvider)}
+ {toCapitalize(provider)}
- How to setup your {toCapitalize(watchProvider)}{" "}
- webhook
+ How to setup your {toCapitalize(provider)} webhook
)}
diff --git a/apps/web/src/content/changelog/pagerduty-integration.mdx b/apps/web/src/content/changelog/pagerduty-integration.mdx
new file mode 100644
index 0000000000..a998bd8943
--- /dev/null
+++ b/apps/web/src/content/changelog/pagerduty-integration.mdx
@@ -0,0 +1,8 @@
+---
+title: PagerDuty Integration
+description: Get notified via PagerDuty if we detect an incident.
+image: /assets/changelog/pagerduty-integration.png
+publishedAt: 2024-06-25
+---
+
+We've added a PagerDuty integration to the notification feature. This allows you to receive incident alerts through PagerDuty.
\ No newline at end of file
diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts
index 728bccfe75..5a30024238 100644
--- a/apps/web/src/env.ts
+++ b/apps/web/src/env.ts
@@ -24,6 +24,7 @@ export const env = createEnv({
CLICKHOUSE_USERNAME: z.string(),
CLICKHOUSE_PASSWORD: z.string(),
PLAIN_API_KEY: z.string().optional(),
+ PAGERDUTY_APP_ID: z.string().optional(),
},
client: {
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string(),
@@ -57,6 +58,7 @@ export const env = createEnv({
CLICKHOUSE_USERNAME: process.env.CLICKHOUSE_USERNAME,
CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD,
PLAIN_API_KEY: process.env.PLAIN_API_KEY,
+ PAGERDUTY_APP_ID: process.env.PAGERDUTY_APP_ID,
},
skipValidation: true,
});
diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts
index 8bf4ba54f9..9766b1296d 100644
--- a/packages/analytics/src/index.ts
+++ b/packages/analytics/src/index.ts
@@ -11,4 +11,7 @@ export const analytics =
})
: emptyAnalytics;
-export const trackAnalytics = (args: AnalyticsEvents) => analytics.track(args);
+export const trackAnalytics = (args: AnalyticsEvents) =>
+ env.JITSU_HOST && env.JITSU_WRITE_KEY
+ ? analytics.track(args)
+ : emptyAnalytics.track(args);
diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts
index 5ae3f10887..45f1e54d54 100644
--- a/packages/api/src/env.ts
+++ b/packages/api/src/env.ts
@@ -8,6 +8,8 @@ export const env = createEnv({
TEAM_ID_VERCEL: z.string(),
VERCEL_AUTH_BEARER_TOKEN: z.string(),
TINY_BIRD_API_KEY: z.string(),
+ JITSU_HOST: z.string().optional(),
+ JITSU_WRITE_KEY: z.string().optional(),
},
runtimeEnv: {
@@ -16,6 +18,8 @@ export const env = createEnv({
TEAM_ID_VERCEL: process.env.TEAM_ID_VERCEL,
VERCEL_AUTH_BEARER_TOKEN: process.env.VERCEL_AUTH_BEARER_TOKEN,
TINY_BIRD_API_KEY: process.env.TINY_BIRD_API_KEY,
+ JITSU_HOST: process.env.JITSU_HOST,
+ JITSU_WRITE_KEY: process.env.JITSU_WRITE_KEY,
},
skipValidation: process.env.NODE_ENV === "test",
});
diff --git a/packages/api/src/router/monitor.ts b/packages/api/src/router/monitor.ts
index 8c8bf9fa7a..19e0460146 100644
--- a/packages/api/src/router/monitor.ts
+++ b/packages/api/src/router/monitor.ts
@@ -19,7 +19,6 @@ import {
notification,
notificationsToMonitors,
page,
- selectMaintenanceSchema,
selectMonitorSchema,
selectMonitorTagSchema,
selectNotificationSchema,
diff --git a/packages/api/src/router/notification.ts b/packages/api/src/router/notification.ts
index 706a58f7eb..7b0ac45952 100644
--- a/packages/api/src/router/notification.ts
+++ b/packages/api/src/router/notification.ts
@@ -13,6 +13,7 @@ import { getLimit } from "@openstatus/plans";
import { SchemaError } from "@openstatus/error";
import { trackNewNotification } from "../analytics";
import { createTRPCRouter, protectedProcedure } from "../trpc";
+import { env } from "../env";
export const notificationRouter = createTRPCRouter({
create: protectedProcedure
@@ -22,7 +23,7 @@ export const notificationRouter = createTRPCRouter({
const notificationLimit = getLimit(
opts.ctx.workspace.plan,
- "notification-channels",
+ "notification-channels"
);
const notificationNumber = (
@@ -40,7 +41,6 @@ export const notificationRouter = createTRPCRouter({
}
const _data = NotificationDataSchema.safeParse(JSON.parse(props.data));
-
if (!_data.success) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -54,9 +54,11 @@ export const notificationRouter = createTRPCRouter({
.returning()
.get();
- await trackNewNotification(opts.ctx.user, {
- provider: _notification.provider,
- });
+ if (env.JITSU_HOST !== undefined && env.JITSU_WRITE_KEY !== undefined) {
+ await trackNewNotification(opts.ctx.user, {
+ provider: _notification.provider,
+ });
+ }
return _notification;
}),
@@ -83,8 +85,8 @@ export const notificationRouter = createTRPCRouter({
.where(
and(
eq(notification.id, opts.input.id),
- eq(notification.workspaceId, opts.ctx.workspace.id),
- ),
+ eq(notification.workspaceId, opts.ctx.workspace.id)
+ )
)
.returning()
.get();
@@ -98,8 +100,8 @@ export const notificationRouter = createTRPCRouter({
.where(
and(
eq(notification.id, opts.input.id),
- eq(notification.id, opts.input.id),
- ),
+ eq(notification.id, opts.input.id)
+ )
)
.run();
}),
@@ -114,8 +116,8 @@ export const notificationRouter = createTRPCRouter({
and(
eq(notification.id, opts.input.id),
eq(notification.id, opts.input.id),
- eq(notification.workspaceId, opts.ctx.workspace.id),
- ),
+ eq(notification.workspaceId, opts.ctx.workspace.id)
+ )
)
.get();
@@ -135,7 +137,7 @@ export const notificationRouter = createTRPCRouter({
isNotificationLimitReached: protectedProcedure.query(async (opts) => {
const notificationLimit = getLimit(
opts.ctx.workspace.plan,
- "notification-channels",
+ "notification-channels"
);
const notificationNumbers = (
await opts.ctx.db.query.notification.findMany({
diff --git a/packages/db/src/schema/notifications/constants.ts b/packages/db/src/schema/notifications/constants.ts
index 90d0790faa..573d31a710 100644
--- a/packages/db/src/schema/notifications/constants.ts
+++ b/packages/db/src/schema/notifications/constants.ts
@@ -3,4 +3,5 @@ export const notificationProvider = [
"discord",
"slack",
"sms",
+ "pagerduty",
] as const;
diff --git a/packages/db/src/schema/notifications/validation.ts b/packages/db/src/schema/notifications/validation.ts
index c7facf39c0..0c9d679166 100644
--- a/packages/db/src/schema/notifications/validation.ts
+++ b/packages/db/src/schema/notifications/validation.ts
@@ -13,14 +13,14 @@ export const selectNotificationSchema = createSelectSchema(notification).extend(
return String(val);
}, z.string())
.default("{}"),
- },
+ }
);
// we need to extend, otherwise data can be `null` or `undefined` - default is not
export const insertNotificationSchema = createInsertSchema(notification).extend(
{
data: z.string().default("{}"),
- },
+ }
);
export type InsertNotification = z.infer;
@@ -28,7 +28,7 @@ export type Notification = z.infer;
export type NotificationProvider = z.infer;
const phoneRegex = new RegExp(
- /^([+]?[\s0-9]+)?(\d{3}|[(]?[0-9]+[)])?([-]?[\s]?[0-9])+$/,
+ /^([+]?[\s0-9]+)?(\d{3}|[(]?[0-9]+[)])?([-]?[\s]?[0-9])+$/
);
export const phoneSchema = z.string().regex(phoneRegex, "Invalid Number!");
@@ -40,4 +40,7 @@ export const NotificationDataSchema = z.union([
z.object({ email: emailSchema }),
z.object({ slack: urlSchema }),
z.object({ discord: urlSchema }),
+ z.object({
+ pagerduty: z.string(),
+ }),
]);
diff --git a/packages/notifications/discord/src/index.ts b/packages/notifications/discord/src/index.ts
index d5e5a49605..b4184ce638 100644
--- a/packages/notifications/discord/src/index.ts
+++ b/packages/notifications/discord/src/index.ts
@@ -20,11 +20,13 @@ export const sendAlert = async ({
notification,
statusCode,
message,
+ incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
+ incidentId?: string;
}) => {
const notificationData = JSON.parse(notification.data);
const { discord: webhookUrl } = notificationData; // webhook url
@@ -37,7 +39,7 @@ export const sendAlert = async ({
Your monitor with url ${monitor.url} is down with ${
statusCode ? `status code ${statusCode}` : `error message ${message}`
}.`,
- webhookUrl,
+ webhookUrl
);
} catch (err) {
console.error(err);
@@ -52,11 +54,14 @@ export const sendRecovery = async ({
statusCode,
// biome-ignore lint/correctness/noUnusedVariables:
message,
+ // biome-ignore lint/correctness/noUnusedVariables:
+ incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
+ incidentId?: string;
}) => {
const notificationData = JSON.parse(notification.data);
const { discord: webhookUrl } = notificationData; // webhook url
@@ -65,7 +70,7 @@ export const sendRecovery = async ({
try {
await postToWebhook(
`Your monitor ${name}|${monitor.url} is up again 🎉`,
- webhookUrl,
+ webhookUrl
);
} catch (err) {
console.error(err);
@@ -80,7 +85,7 @@ export const sendTestDiscordMessage = async (webhookUrl: string) => {
try {
await postToWebhook(
"This is a test notification from OpenStatus. \nIf you see this, it means that your webhook is working! 🎉",
- webhookUrl,
+ webhookUrl
);
return true;
} catch (_err) {
diff --git a/packages/notifications/email/src/index.ts b/packages/notifications/email/src/index.ts
index b3ffd603d2..82f9b98ebe 100644
--- a/packages/notifications/email/src/index.ts
+++ b/packages/notifications/email/src/index.ts
@@ -8,11 +8,14 @@ export const sendAlert = async ({
notification,
statusCode,
message,
+ // biome-ignore lint/correctness/noUnusedVariables:
+ incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
+ incidentId?: string;
}) => {
// FIXME:
const config = EmailConfigurationSchema.parse(JSON.parse(notification.data));
@@ -56,11 +59,14 @@ export const sendRecovery = async ({
statusCode,
// biome-ignore lint/correctness/noUnusedVariables:
message,
+ // biome-ignore lint/correctness/noUnusedVariables:
+ incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
+ incidentId?: string;
}) => {
// FIXME:
const config = EmailConfigurationSchema.parse(JSON.parse(notification.data));
diff --git a/packages/notifications/pagerduty/.env.example b/packages/notifications/pagerduty/.env.example
new file mode 100644
index 0000000000..cb102e1ef7
--- /dev/null
+++ b/packages/notifications/pagerduty/.env.example
@@ -0,0 +1 @@
+PAGERDUTY_APP_ID=your_auth_token
diff --git a/packages/notifications/pagerduty/package.json b/packages/notifications/pagerduty/package.json
new file mode 100644
index 0000000000..0ac5aafd62
--- /dev/null
+++ b/packages/notifications/pagerduty/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@openstatus/notification-pagerduty",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "dependencies": {
+ "@openstatus/db": "workspace:*",
+ "@t3-oss/env-core": "0.7.1",
+ "@types/validator": "13.11.6",
+ "validator": "13.11.0",
+ "zod": "3.22.4"
+ },
+ "devDependencies": {
+ "@openstatus/tsconfig": "workspace:*",
+ "@types/node": "20.8.0",
+ "@types/react": "18.2.64",
+ "@types/react-dom": "18.2.21",
+ "typescript": "5.4.5"
+ }
+}
diff --git a/packages/notifications/pagerduty/src/env.ts b/packages/notifications/pagerduty/src/env.ts
new file mode 100644
index 0000000000..a778704b80
--- /dev/null
+++ b/packages/notifications/pagerduty/src/env.ts
@@ -0,0 +1,29 @@
+import { createEnv } from "@t3-oss/env-core";
+import { z } from "zod";
+
+export const env = createEnv({
+ server: {
+ PAGERDUTY_APP_ID: z.string(),
+ },
+
+ /**
+ * What object holds the environment variables at runtime. This is usually
+ * `process.env` or `import.meta.env`.
+ */
+ runtimeEnv: process.env,
+
+ /**
+ * By default, this library will feed the environment variables directly to
+ * the Zod validator.
+ *
+ * This means that if you have an empty string for a value that is supposed
+ * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag
+ * it as a type mismatch violation. Additionally, if you have an empty string
+ * for a value that is supposed to be a string with a default value (e.g.
+ * `DOMAIN=` in an ".env" file), the default value will never be applied.
+ *
+ * In order to solve these issues, we recommend that all new projects
+ * explicitly specify this option as true.
+ */
+ skipValidation: true,
+});
diff --git a/packages/notifications/pagerduty/src/index.ts b/packages/notifications/pagerduty/src/index.ts
new file mode 100644
index 0000000000..f47c6bd183
--- /dev/null
+++ b/packages/notifications/pagerduty/src/index.ts
@@ -0,0 +1,92 @@
+import type { Monitor, Notification } from "@openstatus/db/src/schema";
+
+import {
+ PagerDutySchema,
+ resolveEventPayloadSchema,
+ triggerEventPayloadSchema,
+} from "./schema/config";
+
+export const sendAlert = async ({
+ monitor,
+ notification,
+ statusCode,
+ message,
+ incidentId,
+}: {
+ monitor: Monitor;
+ notification: Notification;
+ statusCode?: number;
+ message?: string;
+ incidentId?: string;
+}) => {
+ const notificationData = PagerDutySchema.parse(JSON.parse(notification.data));
+ const { name } = monitor;
+
+ const event = triggerEventPayloadSchema.parse({
+ rounting_key: notificationData.integration_keys[0].integration_key,
+ dedup_key: `${monitor.id}}-${incidentId}`,
+ event_action: "trigger",
+ payload: {
+ summary: `${name} is down`,
+ source: "Open Status",
+ severity: "error",
+ timestamp: new Date().toISOString(),
+ custom_details: {
+ statusCode,
+ message,
+ },
+ },
+ });
+
+ try {
+ for await (const integrationKey of notificationData.integration_keys) {
+ // biome-ignore lint/correctness/noUnusedVariables:
+ const { integration_key, type } = integrationKey;
+
+ await fetch("https://events.pagerduty.com/v2/enqueue", {
+ method: "POST",
+ body: JSON.stringify(event),
+ });
+ }
+ } catch (err) {
+ console.log(err);
+ // Do something
+ }
+};
+
+export const sendRecovery = async ({
+ monitor,
+ notification,
+ // biome-ignore lint/correctness/noUnusedVariables:
+ statusCode,
+ // biome-ignore lint/correctness/noUnusedVariables:
+ message,
+ incidentId,
+}: {
+ monitor: Monitor;
+ notification: Notification;
+ statusCode?: number;
+ message?: string;
+ incidentId?: string;
+}) => {
+ const notificationData = PagerDutySchema.parse(JSON.parse(notification.data));
+
+ try {
+ for await (const integrationKey of notificationData.integration_keys) {
+ const event = resolveEventPayloadSchema.parse({
+ rounting_key: integrationKey.integration_key,
+ dedup_key: `${monitor.id}}-${incidentId}`,
+ event_action: "resolve",
+ });
+ await fetch("https://events.pagerduty.com/v2/enqueue", {
+ method: "POST",
+ body: JSON.stringify(event),
+ });
+ }
+ } catch (err) {
+ console.log(err);
+ // Do something
+ }
+};
+
+export { PagerDutySchema };
diff --git a/packages/notifications/pagerduty/src/schema/config.ts b/packages/notifications/pagerduty/src/schema/config.ts
new file mode 100644
index 0000000000..9ece859d6b
--- /dev/null
+++ b/packages/notifications/pagerduty/src/schema/config.ts
@@ -0,0 +1,78 @@
+import { z } from "zod";
+
+export const PagerDutySchema = z.object({
+ integration_keys: z.array(
+ z.object({
+ integration_key: z.string(),
+ name: z.string(),
+ id: z.string(),
+ type: z.string(),
+ })
+ ),
+ account: z.object({ subdomain: z.string(), name: z.string() }),
+});
+
+export const actionSchema = z.union([
+ z.literal("trigger"),
+ z.literal("acknowledge"),
+ z.literal("resolve"),
+]);
+
+export const severitySchema = z.union([
+ z.literal("critical"),
+ z.literal("error"),
+ z.literal("warning"),
+ z.literal("info"),
+]);
+
+export const imageSchema = z.object({
+ src: z.string(),
+ href: z.string().optional(),
+ alt: z.string().optional(),
+});
+
+export const linkSchema = z.object({
+ href: z.string(),
+ text: z.string().optional(),
+});
+
+const baseEventPayloadSchema = z.object({
+ rounting_key: z.string(),
+ dedup_key: z.string(),
+});
+
+export const triggerEventPayloadSchema = baseEventPayloadSchema.merge(
+ z.object({
+ event_action: z.literal("trigger"),
+ payload: z.object({
+ summary: z.string(),
+ source: z.string(),
+ severity: severitySchema,
+ timestamp: z.string().optional(),
+ component: z.string().optional(),
+ group: z.string().optional(),
+ class: z.string().optional(),
+ custom_details: z.any().optional(),
+ }),
+ images: z.array(imageSchema).optional(),
+ links: z.array(linkSchema).optional(),
+ })
+);
+
+export const acknowledgeEventPayloadSchema = baseEventPayloadSchema.merge(
+ z.object({
+ event_action: z.literal("acknowledge"),
+ })
+);
+
+export const resolveEventPayloadSchema = baseEventPayloadSchema.merge(
+ z.object({
+ event_action: z.literal("resolve"),
+ })
+);
+
+export const eventPayloadV2Schema = z.discriminatedUnion("event_action", [
+ triggerEventPayloadSchema,
+ acknowledgeEventPayloadSchema,
+ resolveEventPayloadSchema,
+]);
diff --git a/packages/notifications/pagerduty/tsconfig.json b/packages/notifications/pagerduty/tsconfig.json
new file mode 100644
index 0000000000..a8a8c93096
--- /dev/null
+++ b/packages/notifications/pagerduty/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "@openstatus/tsconfig/nextjs.json",
+ "include": ["src", "*.ts"]
+}
diff --git a/packages/notifications/slack/src/index.ts b/packages/notifications/slack/src/index.ts
index 4909388493..bd2781a4a9 100644
--- a/packages/notifications/slack/src/index.ts
+++ b/packages/notifications/slack/src/index.ts
@@ -18,11 +18,14 @@ export const sendAlert = async ({
notification,
statusCode,
message,
+ // biome-ignore lint/correctness/noUnusedVariables:
+ incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
+ incidentId?: string;
}) => {
const notificationData = JSON.parse(notification.data);
const { slack: webhookUrl } = notificationData; // webhook url
@@ -55,7 +58,7 @@ export const sendAlert = async ({
},
],
},
- webhookUrl,
+ webhookUrl
);
} catch (err) {
console.log(err);
@@ -70,11 +73,14 @@ export const sendRecovery = async ({
statusCode,
// biome-ignore lint/correctness/noUnusedVariables:
message,
+ // biome-ignore lint/correctness/noUnusedVariables:
+ incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
+ incidentId?: string;
}) => {
const notificationData = JSON.parse(notification.data);
const { slack: webhookUrl } = notificationData; // webhook url
@@ -104,7 +110,7 @@ export const sendRecovery = async ({
},
],
},
- webhookUrl,
+ webhookUrl
);
} catch (err) {
console.log(err);
@@ -126,7 +132,7 @@ export const sendTestSlackMessage = async (webhookUrl: string) => {
},
],
},
- webhookUrl,
+ webhookUrl
);
return true;
} catch (_err) {
diff --git a/packages/notifications/twillio-sms/src/index.ts b/packages/notifications/twillio-sms/src/index.ts
index a6cfc53601..a9cb22aad7 100644
--- a/packages/notifications/twillio-sms/src/index.ts
+++ b/packages/notifications/twillio-sms/src/index.ts
@@ -8,14 +8,17 @@ export const sendAlert = async ({
notification,
statusCode,
message,
+ // biome-ignore lint/correctness/noUnusedVariables:
+ incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
+ incidentId?: string;
}) => {
const notificationData = SmsConfigurationSchema.parse(
- JSON.parse(notification.data),
+ JSON.parse(notification.data)
);
const { name } = monitor;
@@ -26,7 +29,7 @@ export const sendAlert = async ({
"Body",
`Your monitor ${name} / ${monitor.url} is down with ${
statusCode ? `status code ${statusCode}` : `error: ${message}`
- }`,
+ }`
);
try {
@@ -37,10 +40,10 @@ export const sendAlert = async ({
body,
headers: {
Authorization: `Basic ${btoa(
- `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`,
+ `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`
)}`,
},
- },
+ }
);
} catch (err) {
console.log(err);
@@ -55,14 +58,17 @@ export const sendRecovery = async ({
statusCode,
// biome-ignore lint/correctness/noUnusedVariables:
message,
+ // biome-ignore lint/correctness/noUnusedVariables:
+ incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
+ incidentId?: string;
}) => {
const notificationData = SmsConfigurationSchema.parse(
- JSON.parse(notification.data),
+ JSON.parse(notification.data)
);
const { name } = monitor;
@@ -79,10 +85,10 @@ export const sendRecovery = async ({
body,
headers: {
Authorization: `Basic ${btoa(
- `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`,
+ `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`
)}`,
},
- },
+ }
);
} catch (err) {
console.log(err);
diff --git a/packages/plans/src/config.ts b/packages/plans/src/config.ts
index 9ed23591ce..decdebdba6 100644
--- a/packages/plans/src/config.ts
+++ b/packages/plans/src/config.ts
@@ -30,6 +30,7 @@ export const allPlans: Record<
"white-label": false,
notifications: true,
sms: false,
+ pagerduty: false,
"notification-channels": 1,
members: 1,
"audit-log": false,
@@ -53,6 +54,7 @@ export const allPlans: Record<
"password-protection": true,
"white-label": false,
notifications: true,
+ pagerduty: true,
sms: true,
"notification-channels": 10,
members: "Unlimited",
@@ -114,6 +116,7 @@ export const allPlans: Record<
"white-label": false,
notifications: true,
sms: true,
+ pagerduty: true,
"notification-channels": 20,
members: "Unlimited",
"audit-log": true,
@@ -174,6 +177,7 @@ export const allPlans: Record<
"white-label": true,
notifications: true,
sms: true,
+ pagerduty: true,
"notification-channels": 50,
members: "Unlimited",
"audit-log": true,
diff --git a/packages/plans/src/types.ts b/packages/plans/src/types.ts
index 3bf835aa43..846e78969b 100644
--- a/packages/plans/src/types.ts
+++ b/packages/plans/src/types.ts
@@ -19,6 +19,7 @@ export type Limits = {
"white-label": boolean;
// alerts
notifications: boolean;
+ pagerduty: boolean;
sms: boolean;
"notification-channels": number;
// collaboration
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c262363c6a..8a888813fe 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -169,6 +169,9 @@ importers:
'@openstatus/notification-emails':
specifier: workspace:*
version: link:../../packages/notifications/email
+ '@openstatus/notification-pagerduty':
+ specifier: workspace:*
+ version: link:../../packages/notifications/pagerduty
'@openstatus/notification-slack':
specifier: workspace:*
version: link:../../packages/notifications/slack
@@ -278,6 +281,9 @@ importers:
'@openstatus/notification-emails':
specifier: workspace:*
version: link:../../packages/notifications/email
+ '@openstatus/notification-pagerduty':
+ specifier: workspace:*
+ version: link:../../packages/notifications/pagerduty
'@openstatus/notification-slack':
specifier: workspace:*
version: link:../../packages/notifications/slack
@@ -806,6 +812,40 @@ importers:
specifier: 5.5.2
version: 5.5.2
+ packages/notifications/pagerduty:
+ dependencies:
+ '@openstatus/db':
+ specifier: workspace:*
+ version: link:../../db
+ '@t3-oss/env-core':
+ specifier: 0.7.1
+ version: 0.7.1(typescript@5.4.5)(zod@3.22.4)
+ '@types/validator':
+ specifier: 13.11.6
+ version: 13.11.6
+ validator:
+ specifier: 13.11.0
+ version: 13.11.0
+ zod:
+ specifier: 3.22.4
+ version: 3.22.4
+ devDependencies:
+ '@openstatus/tsconfig':
+ specifier: workspace:*
+ version: link:../../tsconfig
+ '@types/node':
+ specifier: 20.8.0
+ version: 20.8.0
+ '@types/react':
+ specifier: 18.2.64
+ version: 18.2.64
+ '@types/react-dom':
+ specifier: 18.2.21
+ version: 18.2.21
+ typescript:
+ specifier: 5.4.5
+ version: 5.4.5
+
packages/notifications/slack:
dependencies:
'@openstatus/db':
@@ -4105,9 +4145,15 @@ packages:
'@types/prop-types@15.7.12':
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
+ '@types/react-dom@18.2.21':
+ resolution: {integrity: sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==}
+
'@types/react-dom@18.3.0':
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
+ '@types/react@18.2.64':
+ resolution: {integrity: sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==}
+
'@types/react@18.3.3':
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
@@ -4120,6 +4166,9 @@ packages:
'@types/rss@0.0.32':
resolution: {integrity: sha512-2oKNqKyUY4RSdvl5eZR1n2Q9yvw3XTe3mQHsFPn9alaNBxfPnbXBtGP8R0SV8pK1PrVnLul0zx7izbm5/gF5Qw==}
+ '@types/scheduler@0.23.0':
+ resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==}
+
'@types/through@0.0.32':
resolution: {integrity: sha512-7XsfXIsjdfJM2wFDRAtEWp3zb2aVPk5QeyZxGlVK57q4u26DczMHhJmlhr0Jqv0THwxam/L8REXkj8M2I/lcvw==}
@@ -7806,6 +7855,11 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ typescript@5.4.5:
+ resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
typescript@5.5.2:
resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==}
engines: {node: '>=14.17'}
@@ -11371,6 +11425,12 @@ snapshots:
optionalDependencies:
typescript: 5.4.4
+ '@t3-oss/env-core@0.7.1(typescript@5.4.5)(zod@3.22.4)':
+ dependencies:
+ zod: 3.22.4
+ optionalDependencies:
+ typescript: 5.4.5
+
'@t3-oss/env-core@0.7.1(typescript@5.5.2)(zod@3.23.8)':
dependencies:
zod: 3.23.8
@@ -11613,10 +11673,20 @@ snapshots:
'@types/prop-types@15.7.12': {}
+ '@types/react-dom@18.2.21':
+ dependencies:
+ '@types/react': 18.3.3
+
'@types/react-dom@18.3.0':
dependencies:
'@types/react': 18.3.3
+ '@types/react@18.2.64':
+ dependencies:
+ '@types/prop-types': 15.7.12
+ '@types/scheduler': 0.23.0
+ csstype: 3.1.3
+
'@types/react@18.3.3':
dependencies:
'@types/prop-types': 15.7.12
@@ -11633,6 +11703,8 @@ snapshots:
'@types/rss@0.0.32': {}
+ '@types/scheduler@0.23.0': {}
+
'@types/through@0.0.32':
dependencies:
'@types/node': 20.14.8
@@ -16023,6 +16095,8 @@ snapshots:
typescript@5.4.4: {}
+ typescript@5.4.5: {}
+
typescript@5.5.2: {}
uglify-js@3.17.4: