[admin] feat: allow country filters
This commit is contained in:
parent
40ff702ed2
commit
0abc13e917
|
|
@ -1,5 +1,6 @@
|
||||||
import { Axiom } from '@axiomhq/js';
|
import { Axiom } from '@axiomhq/js';
|
||||||
import { startOfDay, subDays } from 'date-fns';
|
import { startOfDay, subDays } from 'date-fns';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
import { Pool } from 'pg';
|
import { Pool } from 'pg';
|
||||||
|
|
||||||
const daysBefore = 30;
|
const daysBefore = 30;
|
||||||
|
|
@ -9,9 +10,18 @@ const pgPool = new Pool({
|
||||||
ssl: { rejectUnauthorized: false },
|
ssl: { rejectUnauthorized: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const aplQuery = `
|
function aplQuery({
|
||||||
|
countryExclude,
|
||||||
|
countryInclude,
|
||||||
|
}: {
|
||||||
|
countryExclude?: string | null;
|
||||||
|
countryInclude?: string | null;
|
||||||
|
}) {
|
||||||
|
return `
|
||||||
events
|
events
|
||||||
| extend shifted_time = _time + 8h
|
| extend shifted_time = _time + 8h
|
||||||
|
${countryInclude ? `| where ['request.country'] == '${countryInclude}'` : ''}
|
||||||
|
${countryExclude ? `| where ['request.country'] != '${countryExclude}'` : ''}
|
||||||
| summarize
|
| summarize
|
||||||
CheckoutSuccess = countif(['event.name'] == 'checkout.success' and ['event.payload.namespace'] == 'interviews'),
|
CheckoutSuccess = countif(['event.name'] == 'checkout.success' and ['event.payload.namespace'] == 'interviews'),
|
||||||
CheckoutInitiate = dcountif(['user.fingerprint'], ['event.name'] == 'checkout.attempt' 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)
|
| extend CheckoutInitiateToCheckoutSuccessSameDayRate = round(100.0 * CheckoutSuccessSameDay / CheckoutInitiateSameDay, 2)
|
||||||
| order by _time desc
|
| order by _time desc
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const pgQuerySignUps = `SELECT
|
const pgQuerySignUps = `SELECT
|
||||||
date_trunc('day', created_at AT TIME ZONE 'Asia/Singapore') AS date,
|
date_trunc('day', created_at AT TIME ZONE 'Asia/Singapore') AS date,
|
||||||
|
|
@ -67,11 +78,15 @@ GROUP BY
|
||||||
ORDER BY
|
ORDER BY
|
||||||
date DESC;`;
|
date DESC;`;
|
||||||
|
|
||||||
export async function GET(_request: Request) {
|
const axiom = new Axiom({
|
||||||
const axiom = new Axiom({
|
orgId: process.env.AXIOM_ORG_ID!,
|
||||||
orgId: process.env.AXIOM_ORG_ID!,
|
token: process.env.AXIOM_TOKEN!,
|
||||||
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 {
|
try {
|
||||||
await pgPool.connect();
|
await pgPool.connect();
|
||||||
|
|
@ -80,7 +95,7 @@ export async function GET(_request: Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [axiomRes, pgResSignUps, pgResEmailSignUps] = await Promise.all([
|
const [axiomRes, pgResSignUps, pgResEmailSignUps] = await Promise.all([
|
||||||
axiom.query(aplQuery, {
|
axiom.query(aplQuery({ countryExclude, countryInclude }), {
|
||||||
startTime: startOfDay(subDays(new Date(), daysBefore)).toISOString(),
|
startTime: startOfDay(subDays(new Date(), daysBefore)).toISOString(),
|
||||||
}),
|
}),
|
||||||
pgPool.query(pgQuerySignUps),
|
pgPool.query(pgQuerySignUps),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import type { DayData, EmailSignupData, SignupData } from './ConversionsTable';
|
import type { DayData, EmailSignupData, SignupData } from './ConversionsTable';
|
||||||
|
|
@ -11,19 +12,99 @@ type ConversionsData = Readonly<{
|
||||||
signUps: ReadonlyArray<SignupData>;
|
signUps: ReadonlyArray<SignupData>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type CountryFilter = 'exclude' | 'include' | 'none';
|
||||||
|
|
||||||
export default function ConversionsPage() {
|
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>(
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-2 p-4">
|
<div className="flex flex-col gap-2 p-4">
|
||||||
<h1 className="text-2xl font-bold">Critical data</h1>
|
<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>}
|
{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 && (
|
{data && (
|
||||||
<ConversionsTable
|
<ConversionsTable
|
||||||
conversions={data.conversions}
|
conversions={data.conversions}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue