feature(dashboard): add template for notification rules
This commit is contained in:
@@ -15,6 +15,7 @@ import { PureFilterItem } from '@/components/report/sidebar/filters/FilterItem';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useEventNames } from '@/hooks/useEventNames';
|
import { useEventNames } from '@/hooks/useEventNames';
|
||||||
import { useEventProperties } from '@/hooks/useEventProperties';
|
import { useEventProperties } from '@/hooks/useEventProperties';
|
||||||
@@ -51,6 +52,7 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
integrations:
|
integrations:
|
||||||
rule?.integrations.map((integration) => integration.id) ?? [],
|
rule?.integrations.map((integration) => integration.id) ?? [],
|
||||||
projectId,
|
projectId,
|
||||||
|
template: rule?.template ?? '',
|
||||||
config: rule?.config ?? {
|
config: rule?.config ?? {
|
||||||
type: 'events',
|
type: 'events',
|
||||||
events: [
|
events: [
|
||||||
@@ -160,6 +162,16 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</WithLabel>
|
</WithLabel>
|
||||||
|
|
||||||
|
<WithLabel
|
||||||
|
label="Template"
|
||||||
|
info="Customize your notification. Exisiting variables: $EVENT_NAME, $RULE_NAME"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
{...form.register('template')}
|
||||||
|
placeholder="You received a new '$EVENT_NAME' event"
|
||||||
|
/>
|
||||||
|
</WithLabel>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="integrations"
|
name="integrations"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "notification_rules" ADD COLUMN "template" TEXT;
|
||||||
@@ -329,6 +329,7 @@ model NotificationRule {
|
|||||||
sendToEmail Boolean @default(false)
|
sendToEmail Boolean @default(false)
|
||||||
/// [IPrismaNotificationRuleConfig]
|
/// [IPrismaNotificationRuleConfig]
|
||||||
config Json
|
config Json
|
||||||
|
template String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ export const BASE_INTEGRATIONS: Integration[] = [
|
|||||||
export const isBaseIntegration = (id: string) =>
|
export const isBaseIntegration = (id: string) =>
|
||||||
BASE_INTEGRATIONS.find((i) => i.id === id);
|
BASE_INTEGRATIONS.find((i) => i.id === id);
|
||||||
|
|
||||||
|
export type INotificationRuleCached = Awaited<
|
||||||
|
ReturnType<typeof getNotificationRulesByProjectId>
|
||||||
|
>[number];
|
||||||
export const getNotificationRulesByProjectId = cacheable(
|
export const getNotificationRulesByProjectId = cacheable(
|
||||||
function getNotificationRulesByProjectId(projectId: string) {
|
function getNotificationRulesByProjectId(projectId: string) {
|
||||||
return db.notificationRule.findMany({
|
return db.notificationRule.findMany({
|
||||||
@@ -71,6 +74,7 @@ export const getNotificationRulesByProjectId = cacheable(
|
|||||||
sendToApp: true,
|
sendToApp: true,
|
||||||
sendToEmail: true,
|
sendToEmail: true,
|
||||||
config: true,
|
config: true,
|
||||||
|
template: true,
|
||||||
integrations: {
|
integrations: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -190,6 +194,32 @@ export function matchEvent(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notificationTemplateEvent({
|
||||||
|
payload,
|
||||||
|
rule,
|
||||||
|
}: {
|
||||||
|
payload: IServiceCreateEventPayload;
|
||||||
|
rule: INotificationRuleCached;
|
||||||
|
}) {
|
||||||
|
if (!rule.template) return `You received a new "${payload.name}" event`;
|
||||||
|
return rule.template
|
||||||
|
.replaceAll('$EVENT_NAME', payload.name)
|
||||||
|
.replaceAll('$RULE_NAME', rule.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notificationTemplateFunnel({
|
||||||
|
events,
|
||||||
|
rule,
|
||||||
|
}: {
|
||||||
|
events: IServiceEvent[];
|
||||||
|
rule: INotificationRuleCached;
|
||||||
|
}) {
|
||||||
|
if (!rule.template) return `Funnel "${rule.name}" completed`;
|
||||||
|
return rule.template
|
||||||
|
.replaceAll('$EVENT_NAME', events.map((e) => e.name).join(' -> '))
|
||||||
|
.replaceAll('$RULE_NAME', rule.name);
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkNotificationRulesForEvent(
|
export async function checkNotificationRulesForEvent(
|
||||||
payload: IServiceCreateEventPayload,
|
payload: IServiceCreateEventPayload,
|
||||||
) {
|
) {
|
||||||
@@ -207,7 +237,10 @@ export async function checkNotificationRulesForEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notification = {
|
const notification = {
|
||||||
title: `You received a new "${payload.name}" event`,
|
title: notificationTemplateEvent({
|
||||||
|
payload,
|
||||||
|
rule,
|
||||||
|
}),
|
||||||
message: project?.name ? `Project: ${project?.name}` : '',
|
message: project?.name ? `Project: ${project?.name}` : '',
|
||||||
projectId: payload.projectId,
|
projectId: payload.projectId,
|
||||||
payload: {
|
payload: {
|
||||||
@@ -266,7 +299,7 @@ export async function checkNotificationRulesForSessionEnd(
|
|||||||
const notificationPromises = funnelRules.flatMap((rule) => {
|
const notificationPromises = funnelRules.flatMap((rule) => {
|
||||||
// Match funnel events
|
// Match funnel events
|
||||||
let funnelIndex = 0;
|
let funnelIndex = 0;
|
||||||
const matchedEvents = [];
|
const matchedEvents: IServiceEvent[] = [];
|
||||||
for (const event of sortedEvents) {
|
for (const event of sortedEvents) {
|
||||||
if (matchEvent(event, rule.config.events[funnelIndex]!)) {
|
if (matchEvent(event, rule.config.events[funnelIndex]!)) {
|
||||||
matchedEvents.push(event);
|
matchedEvents.push(event);
|
||||||
@@ -280,7 +313,10 @@ export async function checkNotificationRulesForSessionEnd(
|
|||||||
|
|
||||||
// Create notification object
|
// Create notification object
|
||||||
const notification = {
|
const notification = {
|
||||||
title: `Funnel "${rule.name}" completed`,
|
title: notificationTemplateFunnel({
|
||||||
|
rule,
|
||||||
|
events: matchedEvents,
|
||||||
|
}),
|
||||||
message: project?.name ? `Project: ${project?.name}` : '',
|
message: project?.name ? `Project: ${project?.name}` : '',
|
||||||
projectId,
|
projectId,
|
||||||
payload: { type: 'funnel', funnel: matchedEvents } as const,
|
payload: { type: 'funnel', funnel: matchedEvents } as const,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
.map((id) => ({ id })),
|
.map((id) => ({ id })),
|
||||||
},
|
},
|
||||||
config: input.config,
|
config: input.config,
|
||||||
|
template: input.template || null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export type INotificationRuleConfig = z.infer<typeof zNotificationRuleConfig>;
|
|||||||
export const zCreateNotificationRule = z.object({
|
export const zCreateNotificationRule = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
|
template: z.string().optional(),
|
||||||
config: zNotificationRuleConfig,
|
config: zNotificationRuleConfig,
|
||||||
integrations: z.array(z.string()),
|
integrations: z.array(z.string()),
|
||||||
sendToApp: z.boolean(),
|
sendToApp: z.boolean(),
|
||||||
|
|||||||
Reference in New Issue
Block a user