-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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 = { | ||
|
||
| 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 }), | ||
|
||
| ]); | ||
|
|
||
| if (!dailyStats) { | ||
| console.log('\x1b[34mNote: No daily stats have been generated by the dailyStatsJob yet. \x1b[0m'); | ||
|
||
| return undefined; | ||
| } | ||
|
|
||
| return { dailyStats, weeklyStats }; | ||
|
||
| }; | ||
| 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.