[admin] feat: allow country filters
This commit is contained in:
parent
40ff702ed2
commit
0abc13e917
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue