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:
vincanger
2024-07-15 11:10:17 +02:00
committed by GitHub
parent a812b2d7ac
commit f9c920f0f1
107 changed files with 855 additions and 968 deletions

View File

@@ -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 };
};

View File

@@ -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;
}

View File

@@ -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';
}
}

View File

@@ -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;
}