diff --git a/.eslintignore b/.eslintignore index 1521c8b..241d967 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ dist +reports diff --git a/.github/workflows/metrics-update.yml b/.github/workflows/metrics-update.yml new file mode 100644 index 0000000..e2c6253 --- /dev/null +++ b/.github/workflows/metrics-update.yml @@ -0,0 +1,25 @@ +name: Update metrics data in google spreadsheet + +on: + workflow_dispatch: + schedule: + # Every 2 hours: "At minute 0 past every 2nd hour." + - cron: '0 */2 * * *' + +jobs: + update-metrics: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./reports + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master # npm install at the root + - run: npm install && npm run update-dashboards # npm install & run in reports directory + env: + GOOGLE_CREDENTIALS: '${{ secrets.GOOGLE_CREDENTIALS }}' + COUNTLY_USERNAME: '${{ secrets.COUNTLY_USERNAME }}' + COUNTLY_PASSWORD: '${{ secrets.COUNTLY_PASSWORD }}' diff --git a/.gitignore b/.gitignore index 82838fb..624da8f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,8 @@ typings/ storybook-static .envrc .tool-versions +reports/data +reports/node_modules +ignite-metrics-dashboard-d99dde383c4c.json +.env +output diff --git a/reports/README.md b/reports/README.md new file mode 100644 index 0000000..6d739d6 --- /dev/null +++ b/reports/README.md @@ -0,0 +1,55 @@ +# Explanation + +The code in this folder (`reports`) is used to generate CSV files for getting daily/weekly/monthly active users for all Ignite team (ipfs-gui) projects. + +The CSV content is then loaded into it's respective sheet at https://docs.google.com/spreadsheets/d/1xq36kjThObEaRKzb3VRtXEs9RgM-bfgfjGbi1vbPUiE/edit#gid=755468744 + +The charts in the "Charts" sheet are loaded in our Notion page at https://www.notion.so/pl-strflt/Ignite-IPFS-GUI-Tools-3bc1c1bf54d74f928bf11ef59c876b74#b6970aa92e914114848fbddd84eab2ba + +NOTE: The below instructions do not need to be followed once our GitHub Action is merged. The data will update according to the scheduled GitHub Action at [`../.github/workflows/metrics-update.yml`](../.github/workflows/metrics-update.yml) + +## With google sheets authentication + +You need the following ENV vars set properly: + +* `GOOGLE_CREDENTIALS` - Your 'JSON service account key' file stringified into a single line + +Just run `npm run update-dashboards`. This will download all data from countly and then automatically update the google sheets. + +## How to get the data from countly + +You need the following ENV vars set properly: + +* `COUNTLY_USERNAME` - The username you use to login to the countly server +* `COUNTLY_PASSWORD` - The password you use to login to the countly server + +Inside the `./reports` folder, run + +```bash +npm install +npm run get-csv +``` + +## How to copy the data to google spreadsheets (manually) + +If you have a valid keyfile for google sheets authentication + +1. Open up the relevant `./reports/output/*.csv` daily/weekly/monthly file and copy its contents. +1. Paste that content into the relevant google sheet, cell A1, at https://docs.google.com/spreadsheets/d/1xq36kjThObEaRKzb3VRtXEs9RgM-bfgfjGbi1vbPUiE/edit#gid=755468744 +1. Select "Data->Split Text to columns" + +The charts and everything should automatically update. + +## How to embed into Notion + +This is already done and should automatically update, but if it needs redone, it's somewhat like follows: + +***NOTE:*** DO NOT CHANGE THE Published Content & Settings unless you know what you're doing. You will break existing embeds if you change this. + +1. click the three dots in the top right of the chart. +1. select "publish chart" +1. Select the chart you wish to get the link for (in the first dropdown). Leave "Interactive" selected (in the second dropdown). +1. Copy the link +1. Go to Notion where you want to embed. Type `/embed` and select the generic embed "for PDFs, google maps, and more" +1. Paste the link you copied from google sheets. + diff --git a/reports/constants.ts b/reports/constants.ts new file mode 100644 index 0000000..b034e4b --- /dev/null +++ b/reports/constants.ts @@ -0,0 +1,76 @@ +export const hostname = 'countly.ipfs.tech' +const { env: { COUNTLY_USERNAME, COUNTLY_PASSWORD } } = process +const authorizationHeader = `Basic ${ + Buffer + .from(`${COUNTLY_USERNAME}:${COUNTLY_PASSWORD}`) + .toString('base64') +}` +export const baseOptions = { + method: 'GET', + headers: { + accept: 'application/json', + authorization: authorizationHeader + } +} + +async function getApiKey (): Promise { + const response = await fetch(`https://${hostname}/api-key`, { + ...baseOptions, + headers: { + ...baseOptions.headers, + accept: 'text/plain' + } + }) + + try { + return await response.text() + } catch (e) { + console.error('Could not get API key from Countly', e) + throw e + } +} + +export const apiKey = await getApiKey() + +/** + * 90 days of data + */ +export const daysOfDataInMs = 1000 * 60 * 60 * 24 * 90 + +export const appIds = { + // Webui.ipfs.io + 'ipfs-webui': '5c6e72803fd4432348b8119c', + + // webui-kubo + 'ipfs-webui-kubo': '63c596762a7760344a6b2cfd', + + // ipfs-desktop + 'ipfs-desktop': '5c6ec2b13fd4432348b811a0', + + // ipfs-companion + 'ipfs-companion': '639cbbcf8e6f3439c3796738', + + // public gateway checker + 'public-gateway-checker': '6345a52a31fdc11369a2f2db', + + // starmap.site + 'starmap.site': '639915ff21fd4330c469a191', + + // cid-utils-website + 'cid-utils-website': '63cf2d6ed09125d219d3d86c', + + // explore.ipld.io + 'explore.ipld.io': '63cef029d09125d219d3d69a', + + // ipfs-check + 'ipfs-check': '63d039e622fb279599709b09', + + // ipfs-dag-builder-vis + 'ipfs-dag-builder-vis': '63cee76ad09125d219d3d640', + + // pinning-service-compliance + 'pinning-service-compliance': '63cf08ccd09125d219d3d776', + + // pl-diagnose + 'pl-diagnose': '63cef095d09125d219d3d6a6' +} diff --git a/reports/data/.gitkeep b/reports/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/reports/doCountlyFetch.ts b/reports/doCountlyFetch.ts new file mode 100644 index 0000000..c152437 --- /dev/null +++ b/reports/doCountlyFetch.ts @@ -0,0 +1,21 @@ +import { apiKey, baseOptions, hostname } from './constants.js'; + +export async function doCountlyFetch({ + appId, + extraParams, + fetchOptions = {}, + path = '/o' +}: { + appId: string, + extraParams: string, + fetchOptions?: RequestInit, + path: string +}) { + const response = await fetch(`https://${hostname}${path}?api_key=${apiKey}&app_id=${appId}&${extraParams}`, {...baseOptions, ...fetchOptions}); + try { + return await response.json(); + } catch (e) { + console.error(`Could not fetch data from https://${hostname}${path}`, e); + throw e; + } +} diff --git a/reports/downloadDashboardData.ts b/reports/downloadDashboardData.ts new file mode 100644 index 0000000..dfbf6aa --- /dev/null +++ b/reports/downloadDashboardData.ts @@ -0,0 +1,104 @@ +import { writeFile } from 'node:fs/promises' + +import { appIds, daysOfDataInMs } from './constants.js' +import { doCountlyFetch } from './doCountlyFetch.js' +import type { googleSheetData } from './types.js' + +interface ActiveUsersResponse { + calculating: boolean + data: Record +} + +interface DailyData { + _id: string + date: Date + d: number + w: number + m: number +} + +export interface DashboardData { + daily: googleSheetData + weekly: googleSheetData + monthly: googleSheetData +} + +/** + * Print out a CSV of the number of unique users per day for each app + */ +export async function downloadDashboardData ({ writeFiles = false }: { writeFiles?: boolean } = {}): Promise { + const todayEpoch = Date.now() + const dailyArray: googleSheetData = [] + const weeklyArray: googleSheetData = [] + const monthlyArray: googleSheetData = [] + let headers: googleSheetData[0] + const results = await Promise.all(Object.entries(appIds).map(async ([appName, appId]) => { + let response: ActiveUsersResponse = { calculating: true, data: {} } + while (response.calculating) { + /** + * @see https://api.count.ly/reference/oanalyticssessions + */ + response = await doCountlyFetch({ path: '/o/active_users', appId, extraParams: `period=[${todayEpoch - daysOfDataInMs}, ${todayEpoch}]` }) + // eslint-disable-next-line no-console + console.log(`${appName} calculating? `, response.calculating) + if (response.calculating) { + await new Promise((resolve) => setTimeout(resolve, 4000)) + } + } + const activeUserData: DailyData[] = [] + for (const [key, value] of Object.entries(response.data)) { + activeUserData.push({ + _id: key, + date: new Date(key), + ...value + }) + } + + activeUserData.sort((a, b) => a.date.getTime() - a.date.getTime()) + + // output the name of the app as row headers and the date labels as column headers + if (headers == null) { + headers = ['App Name', ...activeUserData.map((day) => day.date.toISOString().split('T')[0])] + dailyArray.push(headers) + weeklyArray.push(headers) + monthlyArray.push(headers) + } + return { + appName, + daily: [appName, ...activeUserData.map((day) => day.d)], + weekly: [appName, ...activeUserData.map((day) => day.w)], + monthly: [appName, ...activeUserData.map((day) => day.m)] + } + + })) + + // now ensure that the arrays are in the same order as appIds + for (const [appName,] of Object.entries(appIds)) { + const result = results.find((result) => result.appName === appName) + if (result) { + dailyArray.push(result.daily) + weeklyArray.push(result.weekly) + monthlyArray.push(result.monthly) + } + } + + if (writeFiles) { + // Write the outputs to their appropriate Csv files + await writeFile('./output/activeUsers-daily.json', JSON.stringify(dailyArray, null, 2)) + await writeFile('./output/activeUsers-daily.csv', dailyArray.join('\n').toString()) + await writeFile('./output/activeUsers-weekly.json', JSON.stringify(weeklyArray, null, 2)) + await writeFile('./output/activeUsers-weekly.csv', weeklyArray.join('\n').toString()) + await writeFile('./output/activeUsers-monthly.json', JSON.stringify(monthlyArray, null, 2)) + await writeFile('./output/activeUsers-monthly.csv', monthlyArray.join('\n').toString()) + } + + return { + daily: dailyArray, + weekly: weeklyArray, + monthly: monthlyArray + } +} diff --git a/reports/getApiKey.ts b/reports/getApiKey.ts new file mode 100644 index 0000000..e1dc07f --- /dev/null +++ b/reports/getApiKey.ts @@ -0,0 +1,13 @@ +import { baseOptions, hostname } from './constants.js' + +export async function getApiKey (): Promise { + const response = await fetch(`https://${hostname}/api-key`, { + ...baseOptions, + headers: { + ...baseOptions.headers, + accept: 'text/plain' + } + }) + + return await response.text() +} diff --git a/reports/index.ts b/reports/index.ts new file mode 100644 index 0000000..b34470f --- /dev/null +++ b/reports/index.ts @@ -0,0 +1,10 @@ +import { downloadDashboardData } from './downloadDashboardData.js' +import { updateSheet } from './updateGoogleSheets.js' + +const {daily, weekly, monthly} = await downloadDashboardData() + +await Promise.all([ + updateSheet('Daily Active Users', daily), + updateSheet('Weekly Active Users', weekly), + updateSheet('Monthly Active Users', monthly) +]) diff --git a/reports/output/.gitkeep b/reports/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/reports/package-lock.json b/reports/package-lock.json new file mode 100644 index 0000000..47de908 --- /dev/null +++ b/reports/package-lock.json @@ -0,0 +1,640 @@ +{ + "name": "reports", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reports", + "version": "1.0.0", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@googleapis/sheets": "^4.0.1", + "googleapis": "^110.0.0" + }, + "devDependencies": { + "ts-node": "^10.9.1" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@googleapis/sheets": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@googleapis/sheets/-/sheets-4.0.1.tgz", + "integrity": "sha512-1BTBNiUAKqcq10vjt5rZIZBSWP9bHurgEy3CU2dd70rB2IrBlk2xQbYGHRezuFp6pKv2WUDLiuuQYq7l6mpGfw==", + "dependencies": { + "googleapis-common": "^6.0.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", + "dev": true, + "peer": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis": { + "version": "110.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-110.0.0.tgz", + "integrity": "sha512-k6de3PGsdFEBULMiFwPYCKOBljDTDvHD3YGe/OFqe8Ot0lYQPL8QV1qjxjrPWiE/Ftf0Ar2v4DNES66jLfSO7w==", + "dependencies": { + "google-auth-library": "^8.0.2", + "googleapis-common": "^6.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz", + "integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-fetch": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", + "integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/reports/package.json b/reports/package.json new file mode 100644 index 0000000..4eb5fb3 --- /dev/null +++ b/reports/package.json @@ -0,0 +1,19 @@ +{ + "name": "reports", + "version": "1.0.0", + "description": "Code for generating reports", + "main": "index.ts", + "type": "module", + "license": "Apache-2.0 OR MIT", + "devDependencies": { + "ts-node": "^10.9.1" + }, + "scripts": { + "get-csv": "ts-node-esm scripts/get-csv.ts", + "update-dashboards": "ts-node-esm index.ts" + }, + "dependencies": { + "@googleapis/sheets": "^4.0.1", + "googleapis": "^110.0.0" + } +} diff --git a/reports/scripts/get-csv.ts b/reports/scripts/get-csv.ts new file mode 100644 index 0000000..bac291c --- /dev/null +++ b/reports/scripts/get-csv.ts @@ -0,0 +1,3 @@ +import { downloadDashboardData } from '../downloadDashboardData.js' + +await downloadDashboardData() diff --git a/reports/tsconfig.json b/reports/tsconfig.json new file mode 100644 index 0000000..aa87481 --- /dev/null +++ b/reports/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "allowSyntheticDefaultImports": true, + "moduleResolution": "NodeNext", + "module": "NodeNext", + "esModuleInterop": true + }, + "include": [ + "**/*.ts", + "data/*.json", + "output/*.json" + ], + "exclude": [ + "node_modules", + ], + "ts-node": { + "extends": "../tsconfig.json", + "transpileOnly": true, + "compilerOptions": {} + } +} diff --git a/reports/types.d.ts b/reports/types.d.ts new file mode 100644 index 0000000..dc5bd11 --- /dev/null +++ b/reports/types.d.ts @@ -0,0 +1 @@ +export type googleSheetData = (string|number)[][] diff --git a/reports/updateGoogleSheets.ts b/reports/updateGoogleSheets.ts new file mode 100644 index 0000000..215d2c5 --- /dev/null +++ b/reports/updateGoogleSheets.ts @@ -0,0 +1,43 @@ +import { google } from 'googleapis' + +// daily: https://docs.google.com/spreadsheets/d/1xq36kjThObEaRKzb3VRtXEs9RgM-bfgfjGbi1vbPUiE/edit#gid=0 +// weekly: https://docs.google.com/spreadsheets/d/1xq36kjThObEaRKzb3VRtXEs9RgM-bfgfjGbi1vbPUiE/edit#gid=1417442855 +// monthly: https://docs.google.com/spreadsheets/d/1xq36kjThObEaRKzb3VRtXEs9RgM-bfgfjGbi1vbPUiE/edit#gid=431140753 +const spreadsheetId = '1xq36kjThObEaRKzb3VRtXEs9RgM-bfgfjGbi1vbPUiE' +if (!process.env.GOOGLE_CREDENTIALS) { + throw new Error('GOOGLE_CREDENTIALS must be set') +} +const credentials = JSON.parse(process.env.GOOGLE_CREDENTIALS) +const googleAuth = new google.auth.GoogleAuth({ + credentials, + scopes: 'https://www.googleapis.com/auth/spreadsheets' +}) +const auth = await googleAuth.getClient() +const googleSheetsInstance = google.sheets({ version: 'v4', auth }) + +const jsonFiles = { + 'Daily Active Users': './output/activeUsers-daily.json', + 'Weekly Active Users': './output/activeUsers-weekly.json', + 'Monthly Active Users': './output/activeUsers-monthly.json' +} + +export async function updateSheet (sheetName: string, values: (string|number)[][]): Promise { + console.info(`Writing ${values.length} rows to ${sheetName}...`) + await googleSheetsInstance.spreadsheets.values.update({ + auth, + spreadsheetId, + range: `'${sheetName}'!A1:CN13`, + valueInputOption: 'USER_ENTERED', + requestBody: { + majorDimension: 'ROWS', + values + } + }) +} + +export async function updateGoogleSheets (): Promise { + for await (const [sheetName, jsonFilePath] of Object.entries(jsonFiles)) { + const { default: json } = await import(jsonFilePath, { assert: { type: 'json' } }) + await updateSheet(sheetName, json) + } +} diff --git a/tsconfig.json b/tsconfig.json index d5cae2a..76b55f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ }, "rootDir": "src", "include": ["src", "types"], - "exclude": ["node_modules"], + "exclude": ["node_modules", "reports"], "ts-node": { "extends": "aegir/src/config/tsconfig.aegir.json", "transpileOnly": true,