add anomaly + fix billing + add emails templates

This commit is contained in:
Emily
2024-09-14 17:07:46 +02:00
parent c253846b86
commit 4c46a36c75
23 changed files with 336 additions and 48 deletions

View File

@@ -29,7 +29,7 @@ export default defineEventHandler(async event => {
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');
}

View File

@@ -1,6 +0,0 @@
export default defineEventHandler(async event => {
console.log('TEST');
return;
});

View File

@@ -9,9 +9,13 @@ export default defineEventHandler(async event => {
if (!project_id) 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 body = await readBody(event);
const { planId } = body;

View File

@@ -9,9 +9,13 @@ export default defineEventHandler(async event => {
if (!project_id) 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 body = await readBody(event);
const { planId } = body;

View File

@@ -143,6 +143,7 @@ async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
EmailService.sendPurchaseEmail(user.email, project.name);
}, 1);
return { ok: true };

View File

@@ -18,6 +18,8 @@ export default defineEventHandler(async event => {
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 });
if (projects == 1) return setResponseStatus(event, 400, 'Cannot delete last project');

View File

@@ -2,10 +2,17 @@ import mongoose from "mongoose";
import { Redis } from "~/server/services/CacheService";
import EmailService from '@services/EmailService';
import StripeService from '~/server/services/StripeService';
import { anomalyLoop } from "./services/AnomalyService";
const config = useRuntimeConfig();
let connection: mongoose.Mongoose;
let anomalyMinutesCount = 0;
function anomalyCheck() {
}
export default async () => {
console.log('[SERVER] Initializing');
@@ -37,4 +44,7 @@ export default async () => {
console.log('[SERVER] Completed');
console.log('[ANOMALY LOOP] Started');
anomalyLoop();
};

View File

@@ -0,0 +1,148 @@
import mongoose from "mongoose";
import { executeTimelineAggregation } from "./TimelineService";
import { VisitModel } from "@schema/metrics/VisitSchema";
import { AnomalyDomainModel } from '@schema/anomalies/AnomalyDomainSchema';
import { AnomalyVisitModel } from '@schema/anomalies/AnomalyVisitSchema';
import { AnomalyEventsModel } from '@schema/anomalies/AnomalyEventsSchema';
import { EventModel } from "@schema/metrics/EventSchema";
import EmailService from "@services/EmailService";
import * as url from 'url';
import { ProjectModel } from "@schema/ProjectSchema";
import { UserModel } from "@schema/UserSchema";
type TAvgInput = { _id: string, count: number }
const anomalyData = { minutes: 0 }
async function anomalyCheckAll() {
const start = performance.now();
console.log('START ANOMALY CHECK');
const projects = await ProjectModel.find({}, { _id: 1 });
for (const project of projects) {
await findAnomalies(project.id);
}
const end = start - performance.now();
console.log('END ANOMALY CHECK', end, 'ms');
}
export function anomalyLoop() {
if (anomalyData.minutes == 60 * 12) {
anomalyCheckAll();
anomalyData.minutes = 0;
}
setTimeout(() => anomalyLoop(), 1000 * 60);
}
function movingAverageAnomaly(visits: TAvgInput[], windowSize: number, threshold: number): TAvgInput[] {
const anomalies: TAvgInput[] = [];
for (let i = windowSize; i < visits.length; i++) {
const window = visits.slice(i - windowSize, i);
const mean = window.reduce((a, b) => a + b.count, 0) / window.length;
const stdDev = Math.sqrt(window.reduce((sum, visit) => sum + Math.pow(visit.count - mean, 2), 0) / window.length);
const currentVisit = visits[i];
if (Math.abs(currentVisit.count - mean) > threshold * stdDev) {
if (currentVisit.count <= mean) continue;
anomalies.push(currentVisit);
}
}
return anomalies;
}
function getUrlFromString(str: string) {
const res = str.startsWith('http') ? str : 'http://' + str;
return res;
}
export async function findAnomalies(project_id: string) {
const THRESHOLD = 6;
const WINDOW_SIZE = 14;
const pid = new mongoose.Types.ObjectId(project_id) as any;
const from = Date.now() - 1000 * 60 * 60 * 24 * 30;
const to = Date.now() - 1000 * 60 * 60 * 24;
const visitsTimelineData = await executeTimelineAggregation({
projectId: pid,
model: VisitModel,
from, to, slice: 'day'
});
const eventsTimelineData = await executeTimelineAggregation({
projectId: pid,
model: EventModel,
from, to, slice: 'day'
});
const websites: { _id: string, count: number }[] = await VisitModel.aggregate([
{ $match: { project_id: pid, created_at: { $gte: new Date(from), $lte: new Date(to) } }, },
{ $group: { _id: "$website", count: { $sum: 1, } } }
]);
const rootWebsite = websites.reduce((a, e) => {
return a.count > e.count ? a : e;
});
const rootDomain = new url.URL(getUrlFromString(rootWebsite._id)).hostname;
const detectedWebsites: string[] = [];
for (const website of websites) {
const websiteDomain = new url.URL(getUrlFromString(website._id)).hostname;
if (!websiteDomain.includes(rootDomain)) {
detectedWebsites.push(website._id);
}
}
const visitAnomalies = movingAverageAnomaly(visitsTimelineData, WINDOW_SIZE, THRESHOLD);
const eventAnomalies = movingAverageAnomaly(eventsTimelineData, WINDOW_SIZE, THRESHOLD);
const shouldSendMail = {
visitsEvents: false,
domains: false
}
for (const visit of visitAnomalies) {
const anomalyAlreadyExist = await AnomalyVisitModel.findOne({ visitDate: visit._id }, { _id: 1 });
if (anomalyAlreadyExist) continue;
await AnomalyVisitModel.create({ project_id: pid, visitDate: visit._id, created_at: Date.now() });
shouldSendMail.visitsEvents = true;
}
for (const event of eventAnomalies) {
const anomalyAlreadyExist = await AnomalyEventsModel.findOne({ eventDate: event._id }, { _id: 1 });
if (anomalyAlreadyExist) continue;
await AnomalyEventsModel.create({ project_id: pid, eventDate: event._id, created_at: Date.now() });
shouldSendMail.visitsEvents = true;
}
for (const website of detectedWebsites) {
const anomalyAlreadyExist = await AnomalyDomainModel.findOne({ domain: website }, { _id: 1 });
if (anomalyAlreadyExist) continue;
await AnomalyDomainModel.create({ project_id: pid, domain: website, created_at: Date.now() });
shouldSendMail.domains = true;
}
const project = await ProjectModel.findById(pid);
if (!project) return { ok: false, error: 'Cannot find project with id ' + pid.toString() }
const user = await UserModel.findById(project.owner);
if (!user) return { ok: false, error: 'Cannot find user with id ' + project.owner.toString() }
if (shouldSendMail.visitsEvents === true) {
await EmailService.sendAnomalyVisitsEventsEmail(user.email, project.name);
}
if (shouldSendMail.domains === true) {
await EmailService.sendAnomalyDomainEmail(user.email, project.name);
}
return { ok: true };
}