fix: funnel notifications
This commit is contained in:
@@ -4,10 +4,10 @@ import { cacheable } from '@openpanel/redis';
|
||||
import type { IChartEvent, IChartEventFilter } from '@openpanel/validation';
|
||||
import { pathOr } from 'ramda';
|
||||
import {
|
||||
db,
|
||||
type Integration,
|
||||
type Notification,
|
||||
Prisma,
|
||||
db,
|
||||
type Prisma,
|
||||
} from '../prisma-client';
|
||||
import type {
|
||||
IServiceCreateEventPayload,
|
||||
@@ -90,7 +90,7 @@ export const getNotificationRulesByProjectId = cacheable(
|
||||
},
|
||||
});
|
||||
},
|
||||
60 * 24,
|
||||
60 * 24
|
||||
);
|
||||
|
||||
function getIntegration(integrationId: string | null) {
|
||||
@@ -117,12 +117,30 @@ function getIntegration(integrationId: string | null) {
|
||||
};
|
||||
}
|
||||
|
||||
function stripNullChars<T>(value: T): T {
|
||||
if (typeof value === 'string') {
|
||||
return value.split('\u0000').join('') as T;
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(stripNullChars) as T;
|
||||
}
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([k, v]) => [k, stripNullChars(v)])
|
||||
) as T;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function createNotification(notification: ICreateNotification) {
|
||||
const data: Prisma.NotificationUncheckedCreateInput = {
|
||||
title: notification.title,
|
||||
message: notification.message,
|
||||
projectId: notification.projectId,
|
||||
payload: notification.payload || Prisma.DbNull,
|
||||
payload: stripNullChars(notification.payload) || undefined,
|
||||
...getIntegration(notification.integrationId),
|
||||
notificationRuleId: notification.notificationRuleId,
|
||||
};
|
||||
@@ -138,7 +156,7 @@ export async function createNotification(notification: ICreateNotification) {
|
||||
}
|
||||
|
||||
export function triggerNotification(
|
||||
notification: Prisma.NotificationUncheckedCreateInput,
|
||||
notification: Prisma.NotificationUncheckedCreateInput
|
||||
) {
|
||||
return notificationQueue.add('sendNotification', {
|
||||
type: 'sendNotification',
|
||||
@@ -150,12 +168,14 @@ export function triggerNotification(
|
||||
|
||||
function matchEventFilters(
|
||||
payload: IServiceCreateEventPayload,
|
||||
filters: IChartEventFilter[],
|
||||
filters: IChartEventFilter[]
|
||||
) {
|
||||
return filters.every((filter) => {
|
||||
const { name, value, operator } = filter;
|
||||
|
||||
if (value.length === 0) return true;
|
||||
if (value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name === 'has_profile') {
|
||||
if (value.includes('true')) {
|
||||
@@ -214,7 +234,7 @@ function matchEventFilters(
|
||||
|
||||
export function matchEvent(
|
||||
payload: IServiceCreateEventPayload,
|
||||
chartEvent: IChartEvent,
|
||||
chartEvent: IChartEvent
|
||||
) {
|
||||
if (payload.name !== chartEvent.name && chartEvent.name !== '*') {
|
||||
return false;
|
||||
@@ -234,7 +254,9 @@ function notificationTemplateEvent({
|
||||
payload: IServiceCreateEventPayload;
|
||||
rule: INotificationRuleCached;
|
||||
}) {
|
||||
if (!rule.template) return `You received a new "${payload.name}" event`;
|
||||
if (!rule.template) {
|
||||
return `You received a new "${payload.name}" event`;
|
||||
}
|
||||
let template = rule.template
|
||||
.replaceAll('$EVENT_NAME', payload.name)
|
||||
.replaceAll('$RULE_NAME', rule.name)
|
||||
@@ -249,7 +271,7 @@ function notificationTemplateEvent({
|
||||
if (value) {
|
||||
template = template.replaceAll(
|
||||
match,
|
||||
typeof value === 'object' ? JSON.stringify(value) : value,
|
||||
typeof value === 'object' ? JSON.stringify(value) : value
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -264,14 +286,17 @@ function notificationTemplateFunnel({
|
||||
events: IServiceEvent[];
|
||||
rule: INotificationRuleCached;
|
||||
}) {
|
||||
if (!rule.template) return `Funnel "${rule.name}" completed`;
|
||||
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);
|
||||
}
|
||||
|
||||
const PROFILE_TEMPLATE_REGEX = /{{profile\.[^}]*}}/;
|
||||
export async function checkNotificationRulesForEvent(
|
||||
payload: IServiceCreateEventPayload,
|
||||
payload: IServiceCreateEventPayload
|
||||
) {
|
||||
const project = await getProjectByIdCached(payload.projectId);
|
||||
const rules = await getNotificationRulesByProjectId(payload.projectId);
|
||||
@@ -280,7 +305,7 @@ export async function checkNotificationRulesForEvent(
|
||||
// so we can use it in the template
|
||||
if (
|
||||
payload.profileId &&
|
||||
rules.some((rule) => rule.template?.match(/{{profile\.[^}]*}}/))
|
||||
rules.some((rule) => rule.template?.match(PROFILE_TEMPLATE_REGEX))
|
||||
) {
|
||||
const profile = await getProfileById(payload.profileId, payload.projectId);
|
||||
if (profile) {
|
||||
@@ -317,7 +342,7 @@ export async function checkNotificationRulesForEvent(
|
||||
...notification,
|
||||
integrationId: integration.id,
|
||||
notificationRuleId: rule.id,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
if (rule.sendToApp) {
|
||||
@@ -326,7 +351,7 @@ export async function checkNotificationRulesForEvent(
|
||||
...notification,
|
||||
integrationId: APP_NOTIFICATION_INTEGRATION_ID,
|
||||
notificationRuleId: rule.id,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,13 +361,15 @@ export async function checkNotificationRulesForEvent(
|
||||
...notification,
|
||||
integrationId: EMAIL_NOTIFICATION_INTEGRATION_ID,
|
||||
notificationRuleId: rule.id,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return promises;
|
||||
}
|
||||
}),
|
||||
|
||||
return [];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -358,13 +385,15 @@ export function getFunnelRules(rules: INotificationRuleCached[]) {
|
||||
}
|
||||
|
||||
export async function checkNotificationRulesForSessionEnd(
|
||||
events: IServiceEvent[],
|
||||
events: IServiceEvent[]
|
||||
) {
|
||||
const sortedEvents = events.sort(
|
||||
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
|
||||
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
||||
);
|
||||
const projectId = sortedEvents[0]?.projectId;
|
||||
if (!projectId) return null;
|
||||
if (!projectId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [project, rules] = await Promise.all([
|
||||
getProjectByIdCached(projectId),
|
||||
@@ -380,12 +409,16 @@ export async function checkNotificationRulesForSessionEnd(
|
||||
if (matchEvent(event, rule.config.events[funnelIndex]!)) {
|
||||
matchedEvents.push(event);
|
||||
funnelIndex++;
|
||||
if (funnelIndex === rule.config.events.length) break;
|
||||
if (funnelIndex === rule.config.events.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If funnel not completed, skip this rule
|
||||
if (funnelIndex < rule.config.events.length) return [];
|
||||
if (funnelIndex < rule.config.events.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Create notification object
|
||||
const notification = {
|
||||
@@ -405,7 +438,7 @@ export async function checkNotificationRulesForSessionEnd(
|
||||
...notification,
|
||||
integrationId: integration.id,
|
||||
notificationRuleId: rule.id,
|
||||
}),
|
||||
})
|
||||
),
|
||||
...(rule.sendToApp
|
||||
? [
|
||||
|
||||
Reference in New Issue
Block a user