Skip to content

Commit

Permalink
feat: metrics exporting
Browse files Browse the repository at this point in the history
  • Loading branch information
cyyynthia committed Jan 27, 2023
1 parent e70e28b commit 522d1f7
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 81 deletions.
1 change: 0 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
"no-lone-blocks": "error",
"no-loop-func": "error",
"no-multi-spaces": "error",
"no-new": "error",
"no-new-wrappers": "error",
"no-octal": "error",
"no-octal-escape": "error",
Expand Down
5 changes: 5 additions & 0 deletions packages/website/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

// Astro only uses process.env for variables visible during build time
// As I do not want anything from .env to be inlined in server bundle, I load everything with dotenv
import 'dotenv/config'

import { defineConfig } from 'astro/config'
import tailwind from '@astrojs/tailwind'
import node from '@astrojs/node'

export default defineConfig({
site: 'https://pronoundb.org/',
output: 'server',
integrations: [ tailwind() ],
adapter: node({ mode: 'standalone' }),
Expand Down
1 change: 1 addition & 0 deletions packages/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.49.0",
"astro": "^2.0.2",
"dotenv": "^16.0.3",
"eslint": "^8.32.0",
"feather-icons": "^4.29.0",
"simple-icons": "^8.3.0",
Expand Down
27 changes: 20 additions & 7 deletions packages/website/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ export interface Props {
const DEFAULT_DESCRIPTION = 'PronounDB is a browser extension that helps people know each other\'s pronouns easily and instantly.'
const { title, description, flash } = Astro.props
const { title: t, description: d, flash } = Astro.props
const description = d || DEFAULT_DESCRIPTION
const title = t ? `${t} • PronounDB` : 'PronounDB'
const canonicalUrl = new URL(Astro.url.pathname, Astro.site)
const [ flashIcon, flashColor ] = flash?.startsWith('S_')
? [ checkSvg, 'text-green-700 border-green-700 dark:text-green-400 dark:border-green-400' ]
: [ alertSvg, 'text-red-600 border-red-600 dark:text-red-orange dark:border-red-orange' ]
Expand All @@ -50,14 +54,23 @@ const [ flashIcon, flashColor ] = flash?.startsWith('S_')
<html lang='en' class='font-sans w-full h-full'>
<head>
<meta charset='utf8'/>
<meta name='viewport' content='width=device-width'/>
<meta name='theme-color' content='#f49898'/>
<meta name='viewport' content='width=device-width, initial-scale=1'/>

<title>{title}</title>
<meta name='description' content={description}/>
<link rel='canonical' href={canonicalUrl}/>

<meta property='og:site_name' content='PronounDB'/>
<meta property='og:type' property='og:type' content='website'>
<meta property='og:url' content={canonicalUrl}/>
<meta property='og:title' content={title}/>
<meta property='og:description' content={description}/>

<meta name='og:site_name' content='PronounDB'/>
<meta name='og:title' content={title || 'PronounDB'}/>
<title>{title ? `${title} • PronounDB` : 'PronounDB'}</title>
<meta name='og:description' content={description || DEFAULT_DESCRIPTION}/>
<meta name='description' content={description || DEFAULT_DESCRIPTION}/>
<meta name='twitter:card' content='summary'/>
<meta name='twitter:creator' content='@cyyynthia_'/>
<meta name='twitter:title' content={title}/>
<meta name='twitter:description' content={description}/>
</head>
<body class='flex flex-col w-full h-full text-black bg-white dark:bg-gray-800 dark:text-white'>
<Header/>
Expand Down
4 changes: 2 additions & 2 deletions packages/website/src/pages/api/v1/lookup-bulk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export async function get (ctx: APIContext) {
const res = Object.create(null)
let user: PronounsOfUser | null
while ((user = await cursor.next())) {
res[user.id] = user.pronouns
ids.delete(user.id)
res[user.account.id] = user.pronouns
ids.delete(user.account.id)
}

await cursor.close()
Expand Down
4 changes: 3 additions & 1 deletion packages/website/src/pages/me/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
*/

import type { APIContext } from 'astro'
import { DeletedAccountCount } from '@server/metrics.js'
import { authenticate, validateCsrf } from '@server/auth.js'
import { setFlash } from '@server/flash.js'
import { deleteAccount } from '@server/database/account.js'
import { setFlash } from '@server/flash.js'

export async function post (ctx: APIContext) {
const user = await authenticate(ctx)
Expand All @@ -43,6 +44,7 @@ export async function post (ctx: APIContext) {
return ctx.redirect('/me')
}

DeletedAccountCount.inc()
deleteAccount(user._id)
ctx.cookies.delete('token')
setFlash(ctx, 'S_ACC_DELETED')
Expand Down
4 changes: 3 additions & 1 deletion packages/website/src/pages/me/unlink-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
*/

import type { APIContext } from 'astro'
import { LinkedAccountsRemovalCount } from '@server/metrics.js'
import { authenticate, validateCsrf } from '@server/auth.js'
import { setFlash } from '@server/flash.js'
import { removeLinkedAccount } from '@server/database/account.js'
import { setFlash } from '@server/flash.js'

export async function post (ctx: APIContext) {
const user = await authenticate(ctx)
Expand All @@ -50,6 +51,7 @@ export async function post (ctx: APIContext) {
return new Response('400: Bad request', { status: 400 })
}

LinkedAccountsRemovalCount.inc({ platform: platform })
removeLinkedAccount(user._id, platform, id)
return ctx.redirect('/me')
}
Expand Down
7 changes: 7 additions & 0 deletions packages/website/src/pages/oauth/[platform]/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import type { APIContext } from 'astro'

import { CreatedAccountCount, LinkedAccountsAddCount } from '@server/metrics.js'
import { generateToken, authenticate } from '@server/auth.js'
import { type FlashMessage, setFlash } from '@server/flash.js'
import { type ExternalAccount, createAccount, findByExternalAccount, addLinkedAccount } from '@server/database/account.js'
Expand Down Expand Up @@ -81,6 +82,7 @@ export async function get (ctx: APIContext) {
}

if (!existingAccount) {
LinkedAccountsAddCount.inc({ platform: external.platform })
await addLinkedAccount(user!._id, external)
}

Expand All @@ -96,6 +98,11 @@ export async function get (ctx: APIContext) {
return ctx.redirect('/')
}

if (intent === 'register') {
CreatedAccountCount.inc({ platform: external.platform })
LinkedAccountsAddCount.inc({ platform: external.platform })
}

const authToken = generateToken({ id: account.toString() })
ctx.cookies.set('token', authToken, { path: '/', maxAge: 365 * 24 * 3600, httpOnly: true, secure: import.meta.env.PROD })
if (intent === 'register') setFlash(ctx, 'S_REGISTERED')
Expand Down
1 change: 0 additions & 1 deletion packages/website/src/pages/shields/[id].json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export const LegacyPronouns: Record<string, string | string[]> = {
avoid: 'Avoid pronouns, use my name',
}


function formatPronouns (pronounsId: string, capitalize: boolean) {
const pronouns = LegacyPronouns[pronounsId]
return Array.isArray(pronouns) ? pronouns[capitalize ? 1 : 0] : pronouns
Expand Down
11 changes: 6 additions & 5 deletions packages/website/src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { APIContext } from 'astro'
import { ObjectId } from 'mongodb'
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'crypto'
import { createSigner, createVerifier } from 'fast-jwt'
import { LegacyTokenizeMigrationCounter } from './metrics.js'
import { findById } from './database/account.js'

export type JwtPayload = { id: string }
Expand Down Expand Up @@ -81,10 +82,9 @@ export async function authenticate ({ cookies }: APIContext, lax?: boolean) {

try {
const { id } = verifier(token)
const user = await findById(new ObjectId(id))
if (!user) cookies.delete('token')
return user
return findById(new ObjectId(id))
} catch {
// Not deleting the token if invalid is intentional. This allows the extension to still show up pronouns.
return null
}
}
Expand Down Expand Up @@ -117,11 +117,11 @@ export function validateCsrf ({ cookies }: APIContext, csrf: string) {

// Legacy tokenize migration
export function migrateAuth ({ cookies }: APIContext) {
if (!import.meta.env.LEGACY_SECRET_KEY) return

let token = cookies.get('token').value
if (!token || token.startsWith('ey')) return

if (!import.meta.env.LEGACY_SECRET_KEY) return

const [ id, gen, sig ] = token.split('.')
if (!id || !gen || !sig) return

Expand All @@ -132,6 +132,7 @@ export function migrateAuth ({ cookies }: APIContext) {

if (!safeEqual(sig, expectedSig)) return

LegacyTokenizeMigrationCounter.inc()
token = generateToken({ id: Buffer.from(id, 'base64').toString() })
cookies.set('token', token, { path: '/', maxAge: 365 * 24 * 3600, httpOnly: true, secure: import.meta.env.PROD })
}
Expand Down
31 changes: 23 additions & 8 deletions packages/website/src/server/database/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
import type { ObjectId } from 'mongodb'
import database from './database.js'

const collection = database.collection<Account>('accounts')
export const collection = database.collection<Account>('accounts')
await collection.createIndex({ 'accounts.id': 1, 'accounts.platform': 1 })
await collection.createIndex({ 'accounts.platform': 1 })

export type Account = {
pronouns: string
Expand All @@ -44,8 +46,7 @@ export type ExternalAccount = {

export type PronounsOfUser = {
pronouns: string
platform: string
id: string
account: ExternalAccount
}

export async function createAccount (from: ExternalAccount) {
Expand Down Expand Up @@ -75,20 +76,34 @@ export async function findByExternalAccount (external: ExternalAccount) {
}

export function findPronounsOf (platform: string, externalIds: string[]) {
// perf: first filtering ($match) needs to be done before doing anything to ensure we hit the index.
// once initial filtering is done, we can do whatever as the dataset is small enough.
// it was behaving with 10k+ docs, it should be ridiculously fast for <=50 docs...
return collection.aggregate<PronounsOfUser>([
{ $unwind: '$accounts' },
{
'$match': {
$match: {
'accounts.platform': platform,
'accounts.id': { '$in': externalIds },
'accounts.id': { $in: externalIds },
},
},
{
$project: {
_id: 0,
pronouns: 1,
platform: '$accounts.platform',
id: '$accounts.id',
account: {
$first: {
$filter: {
input: '$accounts',
as: 'account',
cond: {
$and: [
{ $eq: [ '$$account.platform', platform ] },
{ $in: [ '$$account.id', externalIds ] },
],
},
},
},
},
},
},
])
Expand Down
2 changes: 1 addition & 1 deletion packages/website/src/server/flash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const FlashMessages = <const> {
S_ACC_DELETED: 'Your account has been successfully deleted. Sorry to see you go!',

// Error
E_CSRF: 'Verification of the authenticity of the submission failed. Please try again.',
E_CSRF: 'Verification of the authenticity of the submission failed (CSRF check). Please try again.',
E_OAUTH_GENERIC: 'An unknown error occurred while authenticating with the third party service.',
E_OAUTH_FETCH: 'Could not fetch information about your external account.',
E_OAUTH_10A_EXCHANGE: 'Could not initialize the authentication request with the third party.',
Expand Down
66 changes: 63 additions & 3 deletions packages/website/src/server/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,68 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import { register, collectDefaultMetrics, Counter, Histogram } from 'prom-client'
import { register, collectDefaultMetrics, Counter, Histogram, Gauge } from 'prom-client'
import { collection } from './database/account.js'

const providersGlob = import.meta.glob('../server/oauth/platforms/*.ts', { eager: true })
const providers = Object.keys(providersGlob).map((k) => k.slice(26, -3))

collectDefaultMetrics({
prefix: 'pronoundb_',
})

/// ACCOUNT METRICS
// todo
new Gauge({
name: 'pronoundb_accounts_total',
help: 'accounts count',
labelNames: [],
collect: async function () {
const count = await collection.countDocuments()
this.set({}, count)
},
})

new Gauge({
name: 'pronoundb_linked_accounts_total',
help: 'accounts linked per platform count',
labelNames: [ 'platform' ],
collect: async function () {
// - But Cynthia!!!! You're doing this every 30 seconds!!!!
// - lol yea whatever hehe
// - ................
// - *noms cookie*
await Promise.all(
providers.map(
(p) => collection.countDocuments({ 'accounts.platform': p })
.then((count) => this.set({ platform: p }, count))
)
)
},
})

export const CreatedAccountCount = new Counter({
name: 'pronoundb_account_create_count',
help: 'amount of accounts created',
labelNames: [ 'platform' ],
})

export const DeletedAccountCount = new Counter({
name: 'pronoundb_account_deletion_count',
help: 'amount of accounts deleted',
labelNames: [],
})

export const LinkedAccountsAddCount = new Counter({
name: 'pronoundb_linked_accounts_add_count',
help: 'amount of accounts linked per platform',
labelNames: [ 'platform' ],
})

export const LinkedAccountsRemovalCount = new Counter({
name: 'pronoundb_linked_accounts_remove_count',
help: 'amount of accounts unlinked per platform',
labelNames: [ 'platform' ],
})

/// LOOKUP METRICS
export const LookupRequestsCounter = new Counter({
Expand Down Expand Up @@ -61,7 +115,13 @@ export const LookupHitCounter = new Counter({
})

/// INTERNAL HEALTH METRICS
// todo
// some more metrics might be welcome
// for now I just log this one so I can know roughly when I can ditch Tokenize migration code
export const LegacyTokenizeMigrationCounter = new Counter({
name: 'pronoundb_tokenize_migration_total',
help: 'tokens migrated from legacy tokenize to jwt',
labelNames: [],
})

/// HELPERS
export function metrics () {
Expand Down
Loading

0 comments on commit 522d1f7

Please sign in to comment.