From 0abc13e917b267d4af722e5fd0ba8aede2b8f2e1 Mon Sep 17 00:00:00 2001 From: Yangshun Date: Mon, 28 Jul 2025 17:53:10 +0800 Subject: [PATCH] [admin] feat: allow country filters --- apps/admin/src/app/api/conversions/route.ts | 29 +++++-- apps/admin/src/components/ConversionsPage.tsx | 87 ++++++++++++++++++- 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/apps/admin/src/app/api/conversions/route.ts b/apps/admin/src/app/api/conversions/route.ts index d522d4b87..f37105635 100644 --- a/apps/admin/src/app/api/conversions/route.ts +++ b/apps/admin/src/app/api/conversions/route.ts @@ -1,5 +1,6 @@ import { Axiom } from '@axiomhq/js'; import { startOfDay, subDays } from 'date-fns'; +import type { NextRequest } from 'next/server'; import { Pool } from 'pg'; const daysBefore = 30; @@ -9,9 +10,18 @@ const pgPool = new Pool({ ssl: { rejectUnauthorized: false }, }); -const aplQuery = ` +function aplQuery({ + countryExclude, + countryInclude, +}: { + countryExclude?: string | null; + countryInclude?: string | null; +}) { + return ` events | extend shifted_time = _time + 8h +${countryInclude ? `| where ['request.country'] == '${countryInclude}'` : ''} +${countryExclude ? `| where ['request.country'] != '${countryExclude}'` : ''} | summarize CheckoutSuccess = countif(['event.name'] == 'checkout.success' and ['event.payload.namespace'] == 'interviews'), CheckoutInitiate = dcountif(['user.fingerprint'], ['event.name'] == 'checkout.attempt' and ['event.payload.namespace'] == 'interviews'), @@ -41,6 +51,7 @@ events | extend CheckoutInitiateToCheckoutSuccessSameDayRate = round(100.0 * CheckoutSuccessSameDay / CheckoutInitiateSameDay, 2) | order by _time desc `; +} const pgQuerySignUps = `SELECT date_trunc('day', created_at AT TIME ZONE 'Asia/Singapore') AS date, @@ -67,11 +78,15 @@ GROUP BY ORDER BY date DESC;`; -export async function GET(_request: Request) { - const axiom = new Axiom({ - orgId: process.env.AXIOM_ORG_ID!, - token: process.env.AXIOM_TOKEN!, - }); +const axiom = new Axiom({ + orgId: process.env.AXIOM_ORG_ID!, + token: process.env.AXIOM_TOKEN!, +}); + +export async function GET(request: NextRequest) { + const { searchParams } = request.nextUrl; + const countryInclude = searchParams.get('country_include'); + const countryExclude = searchParams.get('country_exclude'); try { await pgPool.connect(); @@ -80,7 +95,7 @@ export async function GET(_request: Request) { } const [axiomRes, pgResSignUps, pgResEmailSignUps] = await Promise.all([ - axiom.query(aplQuery, { + axiom.query(aplQuery({ countryExclude, countryInclude }), { startTime: startOfDay(subDays(new Date(), daysBefore)).toISOString(), }), pgPool.query(pgQuerySignUps), diff --git a/apps/admin/src/components/ConversionsPage.tsx b/apps/admin/src/components/ConversionsPage.tsx index 35e5e2ce9..8d70724ae 100644 --- a/apps/admin/src/components/ConversionsPage.tsx +++ b/apps/admin/src/components/ConversionsPage.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import useSWR from 'swr'; import type { DayData, EmailSignupData, SignupData } from './ConversionsTable'; @@ -11,19 +12,99 @@ type ConversionsData = Readonly<{ signUps: ReadonlyArray; }>; +type CountryFilter = 'exclude' | 'include' | 'none'; + export default function ConversionsPage() { + const [country, setCountry] = useState(null); + const [countryFilter, setCountryFilter] = useState('none'); + + const queryParams = (() => { + if (countryFilter === 'include') { + return { country_include: country }; + } + + if (countryFilter === 'exclude') { + return { country_exclude: country }; + } + + return {}; + })(); + const { data, error, isLoading } = useSWR( - '/api/conversions', + { query: queryParams, url: '/api/conversions' }, { - fetcher: (url: string) => fetch(url).then((res) => res.json()), + fetcher: ({ + query, + url, + }: Readonly<{ query: Record; url: string }>) => + fetch(`${url}?${new URLSearchParams(query)}`, { + headers: { 'Content-Type': 'application/json' }, + method: 'GET', + }).then((res) => res.json()), }, ); return (

Critical data

+
{ + e.preventDefault(); + + const formData = new FormData(e.currentTarget); + + setCountry(formData.get('country') as string); + setCountryFilter(formData.get('countryFilter') as CountryFilter); + }}> + +
+ + + +
+ +
{isLoading &&
Loading...
} - {error &&
Error: {error.toString()}
} + {error && ( +
Error: {error.toString()}
+ )} {data && (