[admin] feat: allow country filters

This commit is contained in:
Yangshun 2025-07-28 17:53:10 +08:00
parent 40ff702ed2
commit 0abc13e917
2 changed files with 106 additions and 10 deletions

View File

@ -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),

View File

@ -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<SignupData>;
}>;
type CountryFilter = 'exclude' | 'include' | 'none';
export default function ConversionsPage() {
const [country, setCountry] = useState<string | null>(null);
const [countryFilter, setCountryFilter] = useState<CountryFilter>('none');
const queryParams = (() => {
if (countryFilter === 'include') {
return { country_include: country };
}
if (countryFilter === 'exclude') {
return { country_exclude: country };
}
return {};
})();
const { data, error, isLoading } = useSWR<ConversionsData>(
'/api/conversions',
{ query: queryParams, url: '/api/conversions' },
{
fetcher: (url: string) => fetch(url).then((res) => res.json()),
fetcher: ({
query,
url,
}: Readonly<{ query: Record<string, string>; url: string }>) =>
fetch(`${url}?${new URLSearchParams(query)}`, {
headers: { 'Content-Type': 'application/json' },
method: 'GET',
}).then((res) => res.json()),
},
);
return (
<div className="flex flex-col gap-2 p-4">
<h1 className="text-2xl font-bold">Critical data</h1>
<form
className="flex items-center gap-4"
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
setCountry(formData.get('country') as string);
setCountryFilter(formData.get('countryFilter') as CountryFilter);
}}>
<label className="flex items-center gap-2">
<input
className="rounded border border-neutral-300 p-1 text-sm"
defaultValue={country ?? ''}
name="country"
placeholder="Country (e.g. US)"
type="text"
/>
</label>
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 text-sm">
<input
defaultChecked={countryFilter === 'none'}
name="countryFilter"
type="radio"
value="none"
/>
None
</label>
<label className="flex items-center gap-2 text-sm">
<input
defaultChecked={countryFilter === 'include'}
name="countryFilter"
type="radio"
value="include"
/>
Only include
</label>
<label className="flex items-center gap-2 text-sm">
<input
defaultChecked={countryFilter === 'exclude'}
name="countryFilter"
type="radio"
value="exclude"
/>
Exclude
</label>
</div>
<button
className="mt-2 rounded-full bg-blue-500 px-4 py-2 text-sm text-white"
type="submit">
Search
</button>
</form>
{isLoading && <div className="text-sm">Loading...</div>}
{error && <div>Error: {error.toString()}</div>}
{error && (
<div className="text-sm text-red-600">Error: {error.toString()}</div>
)}
{data && (
<ConversionsTable
conversions={data.conversions}