-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
analytics pair programming review #444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 & { | ||
sources: PageViewSource[]; | ||
}; | ||
|
||
type DailyStatsValues = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
}; |
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'; | ||
} | ||
} |
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; | ||
} |
There was a problem hiding this comment.
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.