mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-11-27 12:37:09 +01:00
reorganize admin dash (#227)
* reorganize admin dash * remove messages & ui stuff from admin * separate analytics from admin * update app_diff * Update .env.vault.diff
This commit is contained in:
@@ -1,19 +1,6 @@
|
||||
import { type DailyStats, type GptResponse, type User, type PageViewSource, type Task } from 'wasp/entities';
|
||||
import { type GptResponse, type Task } from 'wasp/entities';
|
||||
import { HttpError } from 'wasp/server';
|
||||
import {
|
||||
type GetGptResponses,
|
||||
type GetDailyStats,
|
||||
type GetAllTasksByUser,
|
||||
} from 'wasp/server/operations';
|
||||
|
||||
type DailyStatsWithSources = DailyStats & {
|
||||
sources: PageViewSource[];
|
||||
};
|
||||
|
||||
type DailyStatsValues = {
|
||||
dailyStats: DailyStatsWithSources;
|
||||
weeklyStats: DailyStatsWithSources[];
|
||||
};
|
||||
import { type GetGptResponses, type GetAllTasksByUser } from 'wasp/server/operations';
|
||||
|
||||
export const getGptResponses: GetGptResponses<void, GptResponse[]> = async (args, context) => {
|
||||
if (!context.user) {
|
||||
@@ -43,29 +30,3 @@ export const getAllTasksByUser: GetAllTasksByUser<void, Task[]> = async (_args,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getDailyStats: GetDailyStats<void, DailyStatsValues> = async (_args, context) => {
|
||||
if (!context.user?.isAdmin) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
const dailyStats = await context.entities.DailyStats.findFirstOrThrow({
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
include: {
|
||||
sources: true,
|
||||
},
|
||||
});
|
||||
|
||||
const weeklyStats = await context.entities.DailyStats.findMany({
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
take: 7,
|
||||
include: {
|
||||
sources: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { dailyStats, weeklyStats };
|
||||
};
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import { type DailyStatsJob } from 'wasp/server/jobs';
|
||||
import Stripe from 'stripe';
|
||||
import { stripe } from '../../payment/stripe/stripeClient';
|
||||
import { getDailyPageViews, getSources } from './plausibleAnalyticsUtils.js';
|
||||
// import { getDailyPageViews, getSources } from './googleAnalyticsUtils.js';
|
||||
|
||||
export const calculateDailyStats: DailyStatsJob<never, void> = async (_args, context) => {
|
||||
const nowUTC = new Date(Date.now());
|
||||
nowUTC.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
const yesterdayUTC = new Date(nowUTC);
|
||||
yesterdayUTC.setUTCDate(yesterdayUTC.getUTCDate() - 1);
|
||||
|
||||
try {
|
||||
const yesterdaysStats = await context.entities.DailyStats.findFirst({
|
||||
where: {
|
||||
date: {
|
||||
equals: yesterdayUTC,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userCount = await context.entities.User.count({});
|
||||
// users can have paid but canceled subscriptions which terminate at the end of the period
|
||||
// we don't want to count those users as current paying users
|
||||
const paidUserCount = await context.entities.User.count({
|
||||
where: {
|
||||
subscriptionStatus: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
let userDelta = userCount;
|
||||
let paidUserDelta = paidUserCount;
|
||||
if (yesterdaysStats) {
|
||||
userDelta -= yesterdaysStats.userCount;
|
||||
paidUserDelta -= yesterdaysStats.paidUserCount;
|
||||
}
|
||||
|
||||
const totalRevenue = await fetchTotalStripeRevenue();
|
||||
const { totalViews, prevDayViewsChangePercent } = await getDailyPageViews();
|
||||
|
||||
let dailyStats = await context.entities.DailyStats.findUnique({
|
||||
where: {
|
||||
date: nowUTC,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dailyStats) {
|
||||
console.log('No daily stat found for today, creating one...');
|
||||
dailyStats = await context.entities.DailyStats.create({
|
||||
data: {
|
||||
date: nowUTC,
|
||||
totalViews,
|
||||
prevDayViewsChangePercent,
|
||||
userCount,
|
||||
paidUserCount,
|
||||
userDelta,
|
||||
paidUserDelta,
|
||||
totalRevenue,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log('Daily stat found for today, updating it...');
|
||||
dailyStats = await context.entities.DailyStats.update({
|
||||
where: {
|
||||
id: dailyStats.id,
|
||||
},
|
||||
data: {
|
||||
totalViews,
|
||||
prevDayViewsChangePercent,
|
||||
userCount,
|
||||
paidUserCount,
|
||||
userDelta,
|
||||
paidUserDelta,
|
||||
totalRevenue,
|
||||
},
|
||||
});
|
||||
}
|
||||
const sources = await getSources();
|
||||
|
||||
for (const source of sources) {
|
||||
let visitors = source.visitors;
|
||||
if (typeof source.visitors !== 'number') {
|
||||
visitors = parseInt(source.visitors);
|
||||
}
|
||||
await context.entities.PageViewSource.upsert({
|
||||
where: {
|
||||
date_name: {
|
||||
date: nowUTC,
|
||||
name: source.source,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
date: nowUTC,
|
||||
name: source.source,
|
||||
visitors,
|
||||
dailyStatsId: dailyStats.id,
|
||||
},
|
||||
update: {
|
||||
visitors,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.table({ dailyStats });
|
||||
} catch (error: any) {
|
||||
console.error('Error calculating daily stats: ', error);
|
||||
await context.entities.Logs.create({
|
||||
data: {
|
||||
message: `Error calculating daily stats: ${error?.message}`,
|
||||
level: 'job-error',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchTotalStripeRevenue() {
|
||||
let totalRevenue = 0;
|
||||
let params: Stripe.BalanceTransactionListParams = {
|
||||
limit: 100,
|
||||
// created: {
|
||||
// gte: startTimestamp,
|
||||
// lt: endTimestamp
|
||||
// },
|
||||
type: 'charge',
|
||||
};
|
||||
|
||||
let hasMore = true;
|
||||
while (hasMore) {
|
||||
const balanceTransactions = await stripe.balanceTransactions.list(params);
|
||||
|
||||
for (const transaction of balanceTransactions.data) {
|
||||
if (transaction.type === 'charge') {
|
||||
totalRevenue += transaction.amount;
|
||||
}
|
||||
}
|
||||
|
||||
if (balanceTransactions.has_more) {
|
||||
// Set the starting point for the next iteration to the last object fetched
|
||||
params.starting_after = balanceTransactions.data[balanceTransactions.data.length - 1].id;
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Revenue is in cents so we convert to dollars (or your main currency unit)
|
||||
const formattedRevenue = totalRevenue / 100;
|
||||
return formattedRevenue;
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import { BetaAnalyticsDataClient } from '@google-analytics/data';
|
||||
|
||||
const CLIENT_EMAIL = process.env.GOOGLE_ANALYTICS_CLIENT_EMAIL;
|
||||
const PRIVATE_KEY = Buffer.from(process.env.GOOGLE_ANALYTICS_PRIVATE_KEY!, 'base64').toString('utf-8');
|
||||
const PROPERTY_ID = process.env.GOOGLE_ANALYTICS_PROPERTY_ID;
|
||||
|
||||
const analyticsDataClient = new BetaAnalyticsDataClient({
|
||||
credentials: {
|
||||
client_email: CLIENT_EMAIL,
|
||||
private_key: PRIVATE_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
export async function getSources() {
|
||||
const [response] = await analyticsDataClient.runReport({
|
||||
property: `properties/${PROPERTY_ID}`,
|
||||
dateRanges: [
|
||||
{
|
||||
startDate: '2020-01-01',
|
||||
endDate: 'today',
|
||||
},
|
||||
],
|
||||
// for a list of dimensions and metrics see https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema
|
||||
dimensions: [
|
||||
{
|
||||
name: 'source',
|
||||
},
|
||||
],
|
||||
metrics: [
|
||||
{
|
||||
name: 'activeUsers',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let activeUsersPerReferrer: any[] = [];
|
||||
if (response?.rows) {
|
||||
activeUsersPerReferrer = response.rows.map((row) => {
|
||||
if (row.dimensionValues && row.metricValues) {
|
||||
return {
|
||||
source: row.dimensionValues[0].value,
|
||||
visitors: row.metricValues[0].value,
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error('No response from Google Analytics');
|
||||
}
|
||||
|
||||
return activeUsersPerReferrer;
|
||||
}
|
||||
|
||||
export async function getDailyPageViews() {
|
||||
const totalViews = await getTotalPageViews();
|
||||
const prevDayViewsChangePercent = await getPrevDayViewsChangePercent();
|
||||
|
||||
return {
|
||||
totalViews,
|
||||
prevDayViewsChangePercent,
|
||||
};
|
||||
}
|
||||
|
||||
async function getTotalPageViews() {
|
||||
const [response] = await analyticsDataClient.runReport({
|
||||
property: `properties/${PROPERTY_ID}`,
|
||||
dateRanges: [
|
||||
{
|
||||
startDate: '2020-01-01', // go back to earliest date of your app
|
||||
endDate: 'today',
|
||||
},
|
||||
],
|
||||
metrics: [
|
||||
{
|
||||
name: 'screenPageViews',
|
||||
},
|
||||
],
|
||||
});
|
||||
let totalViews = 0;
|
||||
if (response?.rows) {
|
||||
// @ts-ignore
|
||||
totalViews = parseInt(response.rows[0].metricValues[0].value);
|
||||
} else {
|
||||
throw new Error('No response from Google Analytics');
|
||||
}
|
||||
return totalViews;
|
||||
}
|
||||
|
||||
async function getPrevDayViewsChangePercent() {
|
||||
const [response] = await analyticsDataClient.runReport({
|
||||
property: `properties/${PROPERTY_ID}`,
|
||||
|
||||
dateRanges: [
|
||||
{
|
||||
startDate: '2daysAgo',
|
||||
endDate: 'yesterday',
|
||||
},
|
||||
],
|
||||
orderBys: [
|
||||
{
|
||||
dimension: {
|
||||
dimensionName: 'date',
|
||||
},
|
||||
desc: true,
|
||||
},
|
||||
],
|
||||
dimensions: [
|
||||
{
|
||||
name: 'date',
|
||||
},
|
||||
],
|
||||
metrics: [
|
||||
{
|
||||
name: 'screenPageViews',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let viewsFromYesterday;
|
||||
let viewsFromDayBeforeYesterday;
|
||||
|
||||
if (response?.rows && response.rows.length === 2) {
|
||||
// @ts-ignore
|
||||
viewsFromYesterday = response.rows[0].metricValues[0].value;
|
||||
// @ts-ignore
|
||||
viewsFromDayBeforeYesterday = response.rows[1].metricValues[0].value;
|
||||
|
||||
if (viewsFromYesterday && viewsFromDayBeforeYesterday) {
|
||||
viewsFromYesterday = parseInt(viewsFromYesterday);
|
||||
viewsFromDayBeforeYesterday = parseInt(viewsFromDayBeforeYesterday);
|
||||
if (viewsFromYesterday === 0 || viewsFromDayBeforeYesterday === 0) {
|
||||
return '0';
|
||||
}
|
||||
console.table({ viewsFromYesterday, viewsFromDayBeforeYesterday });
|
||||
|
||||
const change = ((viewsFromYesterday - viewsFromDayBeforeYesterday) / viewsFromDayBeforeYesterday) * 100;
|
||||
return change.toFixed(0);
|
||||
}
|
||||
} else {
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
const PLAUSIBLE_API_KEY = process.env.PLAUSIBLE_API_KEY!;
|
||||
const PLAUSIBLE_SITE_ID = process.env.PLAUSIBLE_SITE_ID!;
|
||||
const PLAUSIBLE_BASE_URL = process.env.PLAUSIBLE_BASE_URL;
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${PLAUSIBLE_API_KEY}`,
|
||||
};
|
||||
|
||||
type PageViewsResult = {
|
||||
results: {
|
||||
[key: string]: {
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type PageViewSourcesResult = {
|
||||
results: [
|
||||
{
|
||||
source: string;
|
||||
visitors: number;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export async function getDailyPageViews() {
|
||||
const totalViews = await getTotalPageViews();
|
||||
const prevDayViewsChangePercent = await getPrevDayViewsChangePercent();
|
||||
|
||||
return {
|
||||
totalViews,
|
||||
prevDayViewsChangePercent,
|
||||
};
|
||||
}
|
||||
|
||||
async function getTotalPageViews() {
|
||||
const response = await fetch(
|
||||
`${PLAUSIBLE_BASE_URL}/v1/stats/aggregate?site_id=${PLAUSIBLE_SITE_ID}&metrics=pageviews`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
const json = (await response.json()) as PageViewsResult;
|
||||
|
||||
return json.results.pageviews.value;
|
||||
}
|
||||
|
||||
async function getPrevDayViewsChangePercent() {
|
||||
// Calculate today, yesterday, and the day before yesterday's dates
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today.setDate(today.getDate() - 1)).toISOString().split('T')[0];
|
||||
const dayBeforeYesterday = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString().split('T')[0];
|
||||
|
||||
// Fetch page views for yesterday and the day before yesterday
|
||||
const pageViewsYesterday = await getPageviewsForDate(yesterday);
|
||||
const pageViewsDayBeforeYesterday = await getPageviewsForDate(dayBeforeYesterday);
|
||||
|
||||
console.table({
|
||||
pageViewsYesterday,
|
||||
pageViewsDayBeforeYesterday,
|
||||
typeY: typeof pageViewsYesterday,
|
||||
typeDBY: typeof pageViewsDayBeforeYesterday,
|
||||
});
|
||||
|
||||
let change = 0;
|
||||
if (pageViewsYesterday === 0 || pageViewsDayBeforeYesterday === 0) {
|
||||
return '0';
|
||||
} else {
|
||||
change = ((pageViewsYesterday - pageViewsDayBeforeYesterday) / pageViewsDayBeforeYesterday) * 100;
|
||||
}
|
||||
return change.toFixed(0);
|
||||
}
|
||||
|
||||
async function getPageviewsForDate(date: string) {
|
||||
const url = `${PLAUSIBLE_BASE_URL}/v1/stats/aggregate?site_id=${PLAUSIBLE_SITE_ID}&period=day&date=${date}&metrics=pageviews`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
const data = (await response.json()) as PageViewsResult;
|
||||
return data.results.pageviews.value;
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
const url = `${PLAUSIBLE_BASE_URL}/v1/stats/breakdown?site_id=${PLAUSIBLE_SITE_ID}&property=visit:source&metrics=visitors`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
const data = (await response.json()) as PageViewSourcesResult;
|
||||
return data.results;
|
||||
}
|
||||
Reference in New Issue
Block a user