add selfhosted env + start fix dates

This commit is contained in:
Emily
2024-12-05 17:30:28 +01:00
parent 91f69baacd
commit 06768b6cdc
17 changed files with 100 additions and 86 deletions

View File

@@ -24,9 +24,12 @@ const chartOptions = ref<ChartOptions<'line'>>({
color: '#CCCCCC22',
// borderDash: [5, 10]
},
beginAtZero: true,
},
x: {
ticks: { display: true },
stacked: false,
offset: false,
grid: {
display: true,
drawBorder: false,
@@ -136,7 +139,7 @@ const { snapshotDuration } = useSnapshot();
const selectLabels: { label: string, value: Slice }[] = [
{ label: 'Hour', value: 'hour' },
{ label: 'Day', value: 'day' },
{ label: 'Week', value: 'week' },
// { label: 'Week', value: 'week' },
{ label: 'Month', value: 'month' },
];
@@ -220,6 +223,9 @@ function onDataReady() {
const maxChartY = Math.max(...visitsData.data.value.data, ...sessionsData.data.value.data);
const maxEventSize = Math.max(...eventsData.data.value.data)
const currentDateTime = Date.now();
chartData.value.datasets[0].data = visitsData.data.value.data;
chartData.value.datasets[1].data = sessionsData.data.value.data;
chartData.value.datasets[2].data = eventsData.data.value.data.map(e => {
@@ -227,6 +233,7 @@ function onDataReady() {
return { x: 0, y: maxChartY + 70, r: isNaN(rValue) ? 0 : rValue, r2: e }
});
chartData.value.datasets[0].backgroundColor = [createGradient('#5655d7')];
chartData.value.datasets[1].backgroundColor = [createGradient('#4abde8')];
chartData.value.datasets[2].backgroundColor = [createGradient('#fbbf24')];

View File

@@ -7,6 +7,7 @@ const { onlineUsers, stopWatching, startWatching } = useOnlineUsers();
onMounted(() => startWatching());
onUnmounted(() => stopWatching());
const selfhosted = useSelfhosted();
const { createAlert } = useAlert();
@@ -62,7 +63,7 @@ function showAnomalyInfoAlert() {
</div>
</div> -->
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
<div v-if="!selfhosted" class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
<div class="poppins font-regular text-[.9rem]"> AI Anomaly Detector </div>
<div class="flex items-center">

View File

@@ -6,7 +6,7 @@ import * as fns from 'date-fns';
export type DefaultSnapshot = TProjectSnapshot & { default: true }
export type GenericSnapshot = TProjectSnapshot | DefaultSnapshot;
export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id']) {
export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'], project_created_at: Date | string) {
const today: DefaultSnapshot = {
project_id,
@@ -70,9 +70,19 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'])
}
const allTime: DefaultSnapshot = {
project_id,
_id: '___allTime' as any,
name: 'All Time',
from: new Date(project_created_at.toString()),
to: new Date(Date.now()),
color: '#CC11CC',
default: true
}
const snapshotList = [lastDay, today, lastMonth, currentMonth, lastWeek, currentWeek]
const snapshotList = [lastDay, today, lastMonth, currentMonth, lastWeek, currentWeek, allTime]
return snapshotList;

View File

@@ -0,0 +1,7 @@
const app = useRuntimeConfig();
export function useSelfhosted() {
return app.public.SELFHOSTED === 'TRUE';
}

View File

@@ -15,11 +15,11 @@ const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', {
watch(project, async () => {
await remoteSnapshots.refresh();
snapshot.value = isLiveDemo.value ? snapshots.value[0] : snapshots.value[1];
snapshot.value = isLiveDemo.value ? snapshots.value[2] : snapshots.value[2];
});
const snapshots = computed<GenericSnapshot[]>(() => {
const defaultSnapshots: GenericSnapshot[] = project.value?._id ? getDefaultSnapshots(project.value._id as any) : [];
const defaultSnapshots: GenericSnapshot[] = project.value?._id ? getDefaultSnapshots(project.value._id as any, project.value.created_at) : [];
return [...defaultSnapshots, ...(remoteSnapshots.data.value || [])];
})

View File

@@ -9,6 +9,8 @@ const { project } = useProject();
const pricingDrawer = usePricingDrawer();
const selfhosted = useSelfhosted();
const sections: Section[] = [
{
title: '',
@@ -16,10 +18,12 @@ const sections: Section[] = [
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
{ label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted },
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
{
grow: true,

View File

@@ -55,10 +55,12 @@ export default defineNuxtConfig({
STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST,
STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST,
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME,
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME || 'FALSE',
SELFHOSTED: process.env.SELFHOSTED,
public: {
AUTH_MODE: process.env.AUTH_MODE,
GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE'
GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE',
SELFHOSTED: process.env.SELFHOSTED || 'FALSE',
}
},

View File

@@ -60,12 +60,21 @@ async function sendMessage() {
} catch (ex: any) {
if (ex.message.includes('CHAT_LIMIT_REACHED')) {
currentChatMessages.value.push({
role: 'assistant',
content: 'You have reached your current tier chat limit.\n Upgrade to an higher tier. <a style="color: blue; text-decoration: underline;" href="/plans"> Upgrade now. </a>',
});
}
if (ex.message.includes('Unauthorized')) {
currentChatMessages.value.push({
role: 'assistant',
content: 'To use AI you need to provide AI_ORG, AI_PROJECT and AI_KEY in docker compose',
});
}
}
@@ -89,7 +98,7 @@ async function openChat(chat_id?: string) {
}
currentChatId.value = chat_id;
const messages = await $fetch(`/api/ai/${chat_id}/get_messages`, {
headers: useComputedHeaders({useSnapshotDates:false}).value
headers: useComputedHeaders({ useSnapshotDates: false }).value
});
if (!messages) return;
@@ -132,7 +141,7 @@ async function deleteChat(chat_id: string) {
currentChatMessages.value = [];
}
await $fetch(`/api/ai/${chat_id}/delete`, {
headers: useComputedHeaders({useSnapshotDates:false}).value
headers: useComputedHeaders({ useSnapshotDates: false }).value
});
await reloadChatsList();
}
@@ -148,6 +157,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
<div class="flex-[5] py-8 flex h-full flex-col items-center relative overflow-y-hidden">
<div class="flex flex-col items-center xl:mt-[20vh] px-8 xl:px-28"
v-if="currentChatMessages.length == 0">
<div class="w-[7rem] xl:w-[10rem]">
@@ -164,6 +174,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
</div>
</div>
<div ref="scroller" class="flex flex-col w-full gap-6 px-6 xl:px-28 overflow-y-auto pb-20">
<div class="flex w-full flex-col" v-for="message of currentChatMessages">

View File

@@ -7,6 +7,11 @@ definePageMeta({ layout: 'dashboard' });
const { project } = useProject();
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
const selfhosted = useSelfhosted();
const canDownload = computed(() => {
if (selfhosted) return true;
return isPremium.value;
});
const metricsInfo = ref<number>(0);
@@ -105,12 +110,12 @@ function goToUpgrade() {
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
</div>
<div v-if="isPremium" @click="downloadCSV()"
<div v-if="canDownload" @click="downloadCSV()"
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
Download CSV
</div>
<div v-if="!isPremium" @click="goToUpgrade()"
<div v-if="!canDownload" @click="goToUpgrade()"
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
<i class="far fa-lock"></i>
Upgrade plan for CSV

View File

@@ -7,6 +7,11 @@ definePageMeta({ layout: 'dashboard' });
const { project } = useProject();
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
const selfhosted = useSelfhosted();
const canDownload = computed(() => {
if (selfhosted) return true;
return isPremium.value;
});
const metricsInfo = ref<number>(0);
@@ -110,12 +115,12 @@ function goToUpgrade() {
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
</div>
<div v-if="isPremium" @click="downloadCSV()"
<div v-if="canDownload" @click="downloadCSV()"
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
Download CSV
</div>
<div v-if="!isPremium" @click="goToUpgrade()"
<div v-if="!canDownload" @click="goToUpgrade()"
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
<i class="far fa-lock"></i>
Upgrade plan for CSV

View File

@@ -31,6 +31,7 @@ const firstInteraction = useFetch<boolean>('/api/project/first_interaction', {
const showDashboard = computed(() => project.value && firstInteraction.data.value);
const selfhosted = useSelfhosted();
</script>
<template>
@@ -40,8 +41,8 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
<div v-if="showDashboard">
<div class="w-full px-4 py-2 gap-2 flex flex-col">
<BannerLimitsInfo :key="refreshKey"></BannerLimitsInfo>
<BannerOffer :key="refreshKey"></BannerOffer>
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
<BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer>
</div>
<div>

View File

@@ -56,6 +56,8 @@ async function exportToGoogle(data: string, user_id: string) {
}
}
const { SELFHOSTED } = useRuntimeConfig();
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
@@ -63,9 +65,12 @@ export default defineEventHandler(async event => {
const { project, project_id, user } = data;
const PREMIUM_TYPE = project.premium_type;
if (PREMIUM_TYPE === 0) return setResponseStatus(event, 400, 'Project not premium');
if (SELFHOSTED !== 'TRUE') {
const PREMIUM_TYPE = project.premium_type;
if (PREMIUM_TYPE === 0) return setResponseStatus(event, 400, 'Project not premium');
}
const { mode, slice } = getQuery(event);

View File

@@ -4,7 +4,7 @@ import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import DateService from "@services/DateService";
import { checkSliceValidity, generateDateSlices } from "~/server/services/TimelineService";
import { checkSliceValidity } from "~/server/services/TimelineService";
export default defineEventHandler(async event => {
@@ -23,7 +23,7 @@ export default defineEventHandler(async event => {
const [sliceValid, errorOrDays] = checkSliceValidity(from, to, slice);
if (!sliceValid) throw Error(errorOrDays);
const allDates = generateDateSlices(slice, new Date(from), new Date(to));
const allDates = DateService.generateDateSlices(slice, new Date(from), new Date(to));
const result: { _id: string, count: number }[] = [];

View File

@@ -13,12 +13,15 @@ export default defineEventHandler(async event => {
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const timelineData = await executeTimelineAggregation({
projectId: project_id,
model: EventModel,
from, to, slice,
});
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
return timelineFilledMerged;
});

View File

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

View File

@@ -89,57 +89,11 @@ export function fillAndMergeTimelineAggregation(timeline: { _id: string, count:
}
export function fillAndMergeTimelineAggregationV2(timeline: { _id: string, count: number }[], slice: Slice, from: string, to: string) {
const allDates = generateDateSlices(slice, new Date(from), new Date(to));
const merged = mergeDates(timeline, allDates, slice);
const allDates = DateService.generateDateSlices(slice, new Date(from), new Date(to));
const merged = DateService.mergeDates(timeline, allDates, slice);
return merged;
}
export function checkSliceValidity(from: string | number | Date, to: string | number | Date, slice: Slice): [false, string] | [true, number] {
return DateService.canUseSlice(from, to, slice);
}
export function generateDateSlices(slice: Slice, fromDate: Date, toDate: Date) {
const slices: Date[] = [];
let currentDate = fromDate;
const addFunctions: { [key in Slice]: any } = { hour: fns.addHours, day: fns.addDays, week: fns.addWeeks, month: fns.addMonths, year: fns.addYears };
const addFunction = addFunctions[slice];
if (!addFunction) { throw new Error(`Invalid slice: ${slice}`); }
while (fns.isBefore(currentDate, toDate) || currentDate.getTime() === toDate.getTime()) {
slices.push(currentDate);
currentDate = addFunction(currentDate, 1);
}
return slices;
}
function mergeDates(timeline: { _id: string, count: number }[], dates: Date[], slice: Slice) {
const result: { _id: string, count: number }[] = [];
const isSames: { [key in Slice]: any } = { hour: fns.isSameHour, day: fns.isSameDay, week: fns.isSameWeek, month: fns.isSameMonth, year: fns.isSameYear, }
const isSame = isSames[slice];
if (!isSame) {
throw new Error(`Invalid slice: ${slice}`);
}
for (const element of timeline) {
const elementDate = new Date(element._id);
for (const date of dates) {
if (isSame(elementDate, date)) {
const existingEntry = result.find(item => isSame(new Date(item._id), date));
if (existingEntry) {
existingEntry.count += element.count;
} else {
result.push({
_id: date.toISOString(),
count: element.count,
});
}
}
}
}
return result;
}