Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions template/app/src/analytics-new/operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { type DailyStats, type PageViewSource } from 'wasp/entities';
import { HttpError, prisma } from 'wasp/server';
import { type GetDailyStats } from 'wasp/server/operations';

type DailyStatsWithSources = DailyStats & {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it clear enough here that sources are PageViewSources? Mayee it is , but if not we can make it clearer.

sources: PageViewSource[];
};

type DailyStatsValues = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, it has daily and weekly stats, so maybe DailyStatsValues is not the best name?

dailyStats: DailyStatsWithSources;
weeklyStats: DailyStatsWithSources[];
};

export const getDailyStats: GetDailyStats<void, DailyStatsValues | undefined> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401, 'Only authenticated users are allowed to perform this operation');
}

if (!context.user.isAdmin) {
throw new HttpError(403, 'Only admins are allowed to perform this operation');
}

const statsQuery = {
orderBy: {
date: 'desc',
},
include: {
sources: true,
},
} as const;

const [dailyStats, weeklyStats] = await prisma.$transaction([
context.entities.DailyStats.findFirst(statsQuery),
context.entities.DailyStats.findMany({ ...statsQuery, take: 7 }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably just do findMany and get first one from it.

]);

if (!dailyStats) {
console.log('\x1b[34mNote: No daily stats have been generated by the dailyStatsJob yet. \x1b[0m');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that dailyStatsJob genreates the daily stats and that it is called that way. Might be better if the message here is less specific, or if it didn't happen here at all.

return undefined;
}

return { dailyStats, weeklyStats };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so this function says getDailyStats but returns both daily Stats and weeklyStats, look into that.

};
141 changes: 141 additions & 0 deletions template/app/src/analytics-new/providers/googleAnalyticsUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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';
}
}
106 changes: 106 additions & 0 deletions template/app/src/analytics-new/providers/plausibleAnalyticsUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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: {
'Content-Type': 'application/json',
Authorization: `Bearer ${PLAUSIBLE_API_KEY}`,
},
}
);
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;
}
Loading