This commit is contained in:
Emily
2024-10-07 15:26:57 +02:00
parent c2846ca595
commit b27cacf4e6
50 changed files with 512 additions and 583 deletions

View File

@@ -0,0 +1,92 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { EventModel } from "@schema/metrics/EventSchema";
import { VisitModel } from "@schema/metrics/VisitSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id, from, to } = data;
const { name: eventName } = getQuery(event);
if (!eventName) return setResponseStatus(event, 400, 'name is required');
const allEvents = await EventModel.find({
project_id: project_id,
name: eventName,
created_at: {
$gte: new Date(from),
$lte: new Date(to),
}
}, { flowHash: 1 });
const allFlowHashes = new Map<string, number>();
allEvents.forEach(e => {
if (!e.flowHash) return;
if (e.flowHash.length == 0) return;
if (allFlowHashes.has(e.flowHash)) {
const count = allFlowHashes.get(e.flowHash) as number;
allFlowHashes.set(e.flowHash, count + 1);
} else {
allFlowHashes.set(e.flowHash, 1);
}
});
const flowHashIds = Array.from(allFlowHashes.keys());
const allReferrers: { referrer: string, flowHash: string }[] = [];
const promises: any[] = [];
while (flowHashIds.length > 0) {
promises.push(new Promise<void>(async resolve => {
const flowHashIdsChunk = flowHashIds.splice(0, 10);
const visits = await VisitModel.find({ project_id, flowHash: { $in: flowHashIdsChunk } }, { referrer: 1, flowHash: 1 });
allReferrers.push(...visits.map(e => { return { referrer: e.referrer, flowHash: e.flowHash } }));
resolve();
}));
}
await Promise.all(promises);
const groupedFlows: Record<string, { referrers: string[] }> = {};
flowHashIds.forEach(flowHash => {
if (!groupedFlows[flowHash]) groupedFlows[flowHash] = { referrers: [] };
const target = groupedFlows[flowHash];
if (!target) return;
const referrers = allReferrers.filter(e => e.flowHash === flowHash).map(e => e.referrer);
for (const referrer of referrers) {
if (target.referrers.includes(referrer)) continue;
target.referrers.push(referrer);
}
});
const grouped: Record<string, number> = {};
for (const referrerPlusHash of allReferrers) {
const referrer = referrerPlusHash.referrer;
if (!grouped[referrer]) grouped[referrer] = 0
grouped[referrer]++;
}
const eventsCount = allEvents.length;
const allGroupedValue = Object.keys(grouped)
.map(key => grouped[key])
.reduce((a, e) => a + e, 0);
for (const key in grouped) {
grouped[key] = 100 / allGroupedValue * grouped[key];
}
return grouped;
});

View File

@@ -0,0 +1,43 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { EventModel } from "@schema/metrics/EventSchema";
import { EVENT_METADATA_FIELDS_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
import { PipelineStage } from "mongoose";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const { name: eventName, field, from, to } = getQuery(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!eventName) return setResponseStatus(event, 400, 'name is required');
if (!field) return setResponseStatus(event, 400, 'field is required');
const aggregation: PipelineStage[] = [
{
$match: {
project_id, name: eventName,
created_at: {
$gte: new Date(from.toString()),
$lte: new Date(to.toString()),
}
}
},
{ $group: { _id: `$metadata.${field}`, count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]
const metadataGrouped = await EventModel.aggregate(aggregation);
return metadataGrouped;
});

View File

@@ -0,0 +1,32 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { Redis } from "~/server/services/CacheService";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const { name: eventName } = getQuery(event);
if (!eventName) return [];
const fields: string[] = await Redis.useCache({
key: `metadata_fields:${project_id}:${eventName}`,
exp: 60
}, async () => {
const eventsWithName = await EventModel.find({ project_id, name: eventName }, { metadata: 1 }, { limit: 10, sort: { created_at: -1 } });
const allMetadata = eventsWithName.map(e => e.metadata);
const allFields = new Set<string>();
for (const metadata of allMetadata) {
const keys = Object.keys(metadata || {});
keys.forEach(key => allFields.add(key));
}
return Array.from(allFields.values());
});
return fields;
});

View File

@@ -0,0 +1,26 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { Redis } from "~/server/services/CacheService";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const names: string[] = await Redis.useCache({
key: `event_names:${project_id}`,
exp: 60
}, async () => {
const namesAggregation = await EventModel.aggregate([
{ $match: { project_id } },
{ $group: { _id: "$name" } }
]);
return namesAggregation.map(e => e._id);
});
return names;
});

View File

@@ -0,0 +1,25 @@
import { SessionModel } from "@schema/metrics/SessionSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const online_users = await SessionModel.aggregate([
{
$match: {
project_id,
updated_at: { $gt: new Date(Date.now() - 1000 * 60 * 5) }
}
},
{ $count: 'count' }
]);
if (!online_users[0]) return 0;
return online_users[0].count;
});

View File

@@ -1,21 +0,0 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event);
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user, false);
if (!project) return;
if (!project.customer_id) return;
const customer = await StripeService.getCustomer(project.customer_id);
if (customer?.deleted) return;
return customer?.address;
});

View File

@@ -5,16 +5,10 @@ import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event);
if (!project_id) return;
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!data) return;
const user = getRequestUser(event);
if (!user?.logged) return setResponseStatus(event, 400, 'User need to be logged');
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
if (project.owner.toString() != user.id) return setResponseStatus(event, 400, 'You cannot upgrade a project as guest');
const { project, pid } = data;
const body = await readBody(event);
@@ -30,7 +24,7 @@ export default defineEventHandler(async event => {
const intent = await StripeService.createOnetimePayment(
StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
'https://dashboard.litlyx.com/payment_ok',
project_id,
pid,
project.customer_id
)

View File

@@ -1,20 +1,13 @@
import { getPlanFromId } from "@data/PREMIUM";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event);
if (!project_id) return;
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!data) return;
const user = getRequestUser(event);
if (!user?.logged) return setResponseStatus(event, 400, 'User need to be logged');
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
if (project.owner.toString() != user.id) return setResponseStatus(event, 400, 'You cannot upgrade a project as guest');
const { project, pid } = data;
const body = await readBody(event);
@@ -30,7 +23,7 @@ export default defineEventHandler(async event => {
const checkout = await StripeService.createPayment(
StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
'https://dashboard.litlyx.com/payment_ok',
project_id,
pid,
project.customer_id
);

View File

@@ -0,0 +1,16 @@
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!data) return;
const { project } = data;
const customer = await StripeService.getCustomer(project.customer_id);
if (customer?.deleted) return;
return customer?.address;
});

View File

@@ -12,17 +12,14 @@ export type InvoiceData = {
}
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!data) return;
const project_id = getRequestProjectId(event);
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user, false);
if (!project) return;
const { project, pid } = data;
if (!project.customer_id) return [];
return await Redis.useCache({ key: `invoices:${project_id}`, exp: 10 }, async () => {
return await Redis.useCache({ key: `invoices:${pid}`, exp: 10 }, async () => {
const invoices = await StripeService.getInvoices(project.customer_id);
if (!invoices) return [];

View File

@@ -4,13 +4,11 @@ import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event);
if (!project_id) return setResponseStatus(event, 400, 'Cannot get project_id');
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user, false);
if (!project) return setResponseStatus(event, 400, 'Cannot get user from project_id');
const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!data) return;
const { project } = data;
if (!project.customer_id) return setResponseStatus(event, 400, 'Project has no customer_id');
const body = await readBody(event);

View File

@@ -1,27 +1,13 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema";
import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
if (project.owner.toString() != userData.id) {
return setResponseStatus(event, 400, 'You are not the owner');
}
const { project } = data;
const { name } = await readBody(event);
if (name.length == 0) return setResponseStatus(event, 400, 'name is required');
project.name = name;

View File

@@ -8,19 +8,12 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
export default defineEventHandler(async event => {
const body = await readBody(event);
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!data) return;
const project_id = body.project_id;
const { project, user, project_id } = data;
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not exist');
if (userData.id != project.owner.toString()) return setResponseStatus(event, 400, 'You cannot delete a project as guest');
const projects = await ProjectModel.countDocuments({ owner: userData.id });
const projects = await ProjectModel.countDocuments({ owner: user.id });
if (projects == 1) return setResponseStatus(event, 400, 'Cannot delete last project');
if (project.premium === true) return setResponseStatus(event, 400, 'Cannot delete premium project');
@@ -29,26 +22,22 @@ export default defineEventHandler(async event => {
await StripeService.deleteCustomer(project.customer_id);
}
const projectDeletation = await ProjectModel.deleteOne({ _id: project_id });
const countDeletation = await ProjectCountModel.deleteMany({ project_id });
const limitdeletation = await ProjectLimitModel.deleteMany({ project_id });
const sessionsDeletation = await SessionModel.deleteMany({ project_id });
const notifiesDeletation = await LimitNotifyModel.deleteMany({ project_id });
const aiChatsDeletation = await AiChatModel.deleteMany({ project_id });
const projectDeletation = ProjectModel.deleteOne({ _id: project_id });
const countDeletation = ProjectCountModel.deleteMany({ project_id });
const limitdeletation = ProjectLimitModel.deleteMany({ project_id });
const sessionsDeletation = SessionModel.deleteMany({ project_id });
const notifiesDeletation = LimitNotifyModel.deleteMany({ project_id });
const aiChatsDeletation = AiChatModel.deleteMany({ project_id });
const ok = countDeletation.acknowledged && limitdeletation.acknowledged &&
projectDeletation.acknowledged && sessionsDeletation.acknowledged && notifiesDeletation.acknowledged && aiChatsDeletation.acknowledged
const results = await Promise.all([
projectDeletation,
countDeletation,
limitdeletation,
sessionsDeletation,
notifiesDeletation,
aiChatsDeletation
])
return {
ok,
data: [
countDeletation.acknowledged,
limitdeletation.acknowledged,
projectDeletation.acknowledged,
sessionsDeletation.acknowledged,
notifiesDeletation.acknowledged,
aiChatsDeletation.acknowledged
]
};
return { data: results };
});

View File

@@ -1,24 +1,14 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema";
import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!data) return;
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
if (project.owner.toString() != userData.id) {
return setResponseStatus(event, 400, 'You are not the owner');
}
const { project_id } = data;
const { email } = await readBody(event);

View File

@@ -1,24 +1,14 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema";
import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
if (project.owner.toString() != userData.id) {
return setResponseStatus(event, 400, 'You are not the owner');
}
const { project_id } = data;
const { email } = await readBody(event);

View File

@@ -1,22 +1,15 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema";
import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const { project_id, user } = data;
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
await TeamMemberModel.deleteOne({ project_id, user_id: userData.id });
await TeamMemberModel.deleteOne({ project_id, user_id: user.id });
return { ok: true }

View File

@@ -6,16 +6,10 @@ import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
const { project_id, project, user } = data;
const owner = await UserModel.findById(project.owner);
if (!owner) return setResponseStatus(event, 400, 'No owner');
@@ -29,7 +23,7 @@ export default defineEventHandler(async event => {
name: owner.name,
role: 'OWNER',
pending: false,
me: userData.id === owner.id
me: user.id === owner.id
})
for (const member of members) {
@@ -40,7 +34,7 @@ export default defineEventHandler(async event => {
name: userMember.name,
role: member.role,
pending: member.pending,
me: userData.id === userMember.id
me: user.id === userMember.id
})
}

View File

@@ -1,21 +1,12 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { ProjectLimitModel } from "@schema/ProjectsLimits";
import { UserSettingsModel } from "@schema/UserSettings";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!data) return;
const { project, project_id } = data;
if (project.subscription_id === 'onetime') {

View File

@@ -1,16 +1,12 @@
import { ProjectSnapshotModel, TProjectSnapshot } from "@schema/ProjectSnapshot";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const { project_id } = data;
const snapshots = await ProjectSnapshotModel.find({ project_id });

View File

@@ -14,7 +14,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:bouncing_rate:${pid}:${from}:${to}`;
const cacheKey = `timeline:bouncing_rate:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60 * 60; //1 hour
return await Redis.useCacheV2(cacheKey, cacheExp, async (noStore, updateExp) => {

View File

@@ -9,7 +9,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:events:${pid}:${from}:${to}`;
const cacheKey = `timeline:events:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -0,0 +1,28 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { executeAdvancedTimelineAggregation} from "~/server/services/TimelineService";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
if (!data) return;
const { from, to, slice, project_id } = data;
return await Redis.useCache({ key: `timeline:events_stacked:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
const timelineStackedEvents = await executeAdvancedTimelineAggregation<{ name: String }>({
model: EventModel,
projectId: project_id,
from, to, slice,
customProjection: { name: "$_id.name" },
customIdGroup: { name: '$name' },
})
// const filledDates = DateService.createBetweenDates(from, to, slice);
// const merged = DateService.mergeFilledDates(filledDates.dates, timelineStackedEvents, '_id', slice, { count: 0, name: '' });
return timelineStackedEvents;
});
});

View File

@@ -9,7 +9,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:sessions:${pid}:${from}:${to}`;
const cacheKey = `timeline:sessions:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -9,7 +9,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:sessions_duration:${pid}:${from}:${to}`;
const cacheKey = `timeline:sessions_duration:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -9,7 +9,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:visits:${pid}:${from}:${to}`;
const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -96,6 +96,10 @@ export async function getRequestData(event: H3Event<EventHandlerRequest>, option
if (!allowLitlyx) return setResponseStatus(event, 400, 'no access to project');
}
return { from, to, pid, project_id, project, user, limit, slice, schemaName, model }
return {
from: from as string,
to: to as string,
pid, project_id, project, user, limit, slice, schemaName, model
}
}