add subdivision1/2, cities to query logic

This commit is contained in:
Francis Cao 2023-02-20 09:04:20 -08:00
parent 6bacfa5892
commit 55a586fe27
16 changed files with 184 additions and 57 deletions

View File

@ -15,7 +15,7 @@ CREATE TABLE event
screen LowCardinality(String), screen LowCardinality(String),
language LowCardinality(String), language LowCardinality(String),
country LowCardinality(String), country LowCardinality(String),
subdivision LowCardinality(String), subdivision1 LowCardinality(String),
subdivision2 LowCardinality(String), subdivision2 LowCardinality(String),
city String, city String,
--pageview --pageview

View File

@ -115,6 +115,9 @@ function getFilterQuery(filters = {}, params = {}) {
case 'os': case 'os':
case 'browser': case 'browser':
case 'device': case 'device':
case 'subdivision1':
case 'subdivision2':
case 'city':
case 'country': case 'country':
arr.push(`and ${key} = {${key}:String}`); arr.push(`and ${key} = {${key}:String}`);
params[key] = filter; params[key] = filter;
@ -147,11 +150,24 @@ function getFilterQuery(filters = {}, params = {}) {
} }
function parseFilters(filters: any = {}, params: any = {}) { function parseFilters(filters: any = {}, params: any = {}) {
const { domain, url, eventUrl, referrer, os, browser, device, country, eventName, query } = const {
filters; domain,
url,
eventUrl,
referrer,
os,
browser,
device,
country,
subdivision1,
subdivision2,
city,
eventName,
query,
} = filters;
const pageviewFilters = { domain, url, referrer, query }; const pageviewFilters = { domain, url, referrer, query };
const sessionFilters = { os, browser, device, country }; const sessionFilters = { os, browser, device, country, subdivision1, subdivision2, city };
const eventFilters = { url: eventUrl, eventName }; const eventFilters = { url: eventUrl, eventName };
return { return {

View File

@ -68,17 +68,17 @@ export async function getLocation(ip) {
const result = lookup.get(ip); const result = lookup.get(ip);
const country = result?.country?.iso_code ?? result?.registered_country?.iso_code; const country = result?.country?.iso_code ?? result?.registered_country?.iso_code;
const subdivision1 = result?.subdivisions[0].iso_code; const subdivision1 = result?.subdivisions[0]?.iso_code;
const subdivision2 = result?.subdivisions[1].iso_code; const subdivision2 = result?.subdivisions[1]?.iso_code;
const city = result?.city?.names?.en; const city = result?.city?.names?.en;
return { country, subdivision1, subdivision2, city }; return { country, subdivision1, subdivision2, city };
} }
export async function getClientInfo(req, { screen }) { export async function getClientInfo(req, { screen }) {
const location = await getLocation(ip);
const userAgent = req.headers['user-agent']; const userAgent = req.headers['user-agent'];
const ip = getIpAddress(req); const ip = getIpAddress(req);
const location = await getLocation(ip);
const country = location.country; const country = location.country;
const subdivision1 = location.subdivision1; const subdivision1 = location.subdivision1;
const subdivision2 = location.subdivision2; const subdivision2 = location.subdivision2;

View File

@ -135,6 +135,9 @@ function getFilterQuery(filters = {}, params = []): string {
case 'os': case 'os':
case 'browser': case 'browser':
case 'device': case 'device':
case 'subdivision1':
case 'subdivision2':
case 'city':
case 'country': case 'country':
arr.push(`and ${key}=$${params.length + 1}`); arr.push(`and ${key}=$${params.length + 1}`);
params.push(decodeURIComponent(filter)); params.push(decodeURIComponent(filter));
@ -171,11 +174,24 @@ function parseFilters(
params = [], params = [],
sessionKey = 'session_id', sessionKey = 'session_id',
) { ) {
const { domain, url, eventUrl, referrer, os, browser, device, country, eventName, query } = const {
filters; domain,
url,
eventUrl,
referrer,
os,
browser,
device,
country,
subdivision1,
subdivision2,
city,
eventName,
query,
} = filters;
const pageviewFilters = { domain, url, referrer, query }; const pageviewFilters = { domain, url, referrer, query };
const sessionFilters = { os, browser, device, country }; const sessionFilters = { os, browser, device, country, subdivision1, subdivision2, city };
const eventFilters = { url: eventUrl, eventName }; const eventFilters = { url: eventUrl, eventName };
return { return {
@ -184,7 +200,7 @@ function parseFilters(
eventFilters, eventFilters,
event: { eventName }, event: { eventName },
joinSession: joinSession:
os || browser || device || country os || browser || device || country || subdivision1 || subdivision2 || city
? `inner join session on website_event.${sessionKey} = session.${sessionKey}` ? `inner join session on website_event.${sessionKey} = session.${sessionKey}`
: '', : '',
filterQuery: getFilterQuery(filters, params), filterQuery: getFilterQuery(filters, params),

View File

@ -44,7 +44,8 @@ export async function findSession(req) {
throw new Error(`Website not found: ${websiteId}`); throw new Error(`Website not found: ${websiteId}`);
} }
const { userAgent, browser, os, ip, country, device } = await getClientInfo(req, payload); const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } =
await getClientInfo(req, payload);
const sessionId = uuid(websiteId, hostname, ip, userAgent); const sessionId = uuid(websiteId, hostname, ip, userAgent);
// Clickhouse does not require session lookup // Clickhouse does not require session lookup
@ -59,6 +60,9 @@ export async function findSession(req) {
screen, screen,
language, language,
country, country,
subdivision1,
subdivision2,
city,
}; };
} }
@ -84,6 +88,9 @@ export async function findSession(req) {
screen, screen,
language, language,
country, country,
subdivision1,
subdivision2,
city,
}); });
} catch (e) { } catch (e) {
if (!e.message.toLowerCase().includes('unique constraint')) { if (!e.message.toLowerCase().includes('unique constraint')) {

View File

@ -19,6 +19,9 @@ export interface NextApiRequestCollect extends NextApiRequest {
screen: string; screen: string;
language: string; language: string;
country: string; country: string;
subdivision1: string;
subdivision2: string;
city: string;
}; };
} }

View File

@ -46,6 +46,9 @@ export interface WebsiteMetricsRequestQuery {
browser: string; browser: string;
device: string; device: string;
country: string; country: string;
subdivision1: string;
subdivision2: string;
city: string;
} }
export default async ( export default async (
@ -66,6 +69,9 @@ export default async (
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
} = req.query; } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
@ -86,6 +92,9 @@ export default async (
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}); });
@ -131,6 +140,9 @@ export default async (
browser: type !== 'browser' ? browser : undefined, browser: type !== 'browser' ? browser : undefined,
device: type !== 'device' ? device : undefined, device: type !== 'device' ? device : undefined,
country: type !== 'country' ? country : undefined, country: type !== 'country' ? country : undefined,
subdivision1: type !== 'subdivision1' ? subdivision1 : undefined,
subdivision2: type !== 'subdivision2' ? subdivision2 : undefined,
city: type !== 'city' ? city : undefined,
eventUrl: type !== 'url' && table === 'event' ? url : undefined, eventUrl: type !== 'url' && table === 'event' ? url : undefined,
query: type === 'query' && table !== 'event' ? true : undefined, query: type === 'query' && table !== 'event' ? true : undefined,
}; };

View File

@ -21,6 +21,9 @@ export interface WebsitePageviewRequestQuery {
browser?: string; browser?: string;
device?: string; device?: string;
country?: string; country?: string;
subdivision1?: string;
subdivision2?: string;
city?: string;
} }
export default async ( export default async (
@ -42,6 +45,9 @@ export default async (
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
} = req.query; } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
@ -70,6 +76,9 @@ export default async (
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}), }),
getPageviewStats(websiteId, { getPageviewStats(websiteId, {
@ -84,6 +93,9 @@ export default async (
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}), }),
]); ]);

View File

@ -17,6 +17,9 @@ export interface WebsiteStatsRequestQuery {
browser: string; browser: string;
device: string; device: string;
country: string; country: string;
subdivision1: string;
subdivision2: string;
city: string;
} }
export default async ( export default async (
@ -26,7 +29,20 @@ export default async (
await useCors(req, res); await useCors(req, res);
await useAuth(req, res); await useAuth(req, res);
const { id: websiteId, startAt, endAt, url, referrer, os, browser, device, country } = req.query; const {
id: websiteId,
startAt,
endAt,
url,
referrer,
os,
browser,
device,
country,
subdivision1,
subdivision2,
city,
} = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) { if (!(await canViewWebsite(req.auth, websiteId))) {
@ -50,6 +66,9 @@ export default async (
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}); });
const prevPeriod = await getWebsiteStats(websiteId, { const prevPeriod = await getWebsiteStats(websiteId, {
@ -62,6 +81,9 @@ export default async (
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}); });

View File

@ -19,6 +19,9 @@ export async function saveEvent(args: {
screen?: string; screen?: string;
language?: string; language?: string;
country?: string; country?: string;
subdivision1?: string;
subdivision2?: string;
city?: string;
}) { }) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(args), [PRISMA]: () => relationalQuery(args),
@ -36,38 +39,33 @@ async function relationalQuery(data: {
}) { }) {
const { websiteId, id: sessionId, url, eventName, eventData, referrer } = data; const { websiteId, id: sessionId, url, eventName, eventData, referrer } = data;
const params = {
id: uuid(),
websiteId,
sessionId,
url: url?.substring(0, URL_LENGTH),
referrer: referrer?.substring(0, URL_LENGTH),
eventType: EVENT_TYPE.customEvent,
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
eventData,
};
return prisma.client.websiteEvent.create({ return prisma.client.websiteEvent.create({
data: params, data: {
id: uuid(),
websiteId,
sessionId,
url: url?.substring(0, URL_LENGTH),
referrer: referrer?.substring(0, URL_LENGTH),
eventType: EVENT_TYPE.customEvent,
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
eventData,
},
}); });
} }
async function clickhouseQuery(data: { async function clickhouseQuery(data) {
id: string; const {
websiteId: string; websiteId,
url: string; id: sessionId,
referrer?: string; url,
eventName?: string; eventName,
eventData?: any; eventData,
hostname?: string; country,
browser?: string; subdivision1,
os?: string; subdivision2,
device?: string; city,
screen?: string; ...args
language?: string; } = data;
country?: string;
}) {
const { websiteId, id: sessionId, url, eventName, eventData, country, ...args } = data;
const { getDateFormat, sendMessage } = kafka; const { getDateFormat, sendMessage } = kafka;
const website = await cache.fetchWebsite(websiteId); const website = await cache.fetchWebsite(websiteId);
@ -75,13 +73,16 @@ async function clickhouseQuery(data: {
website_id: websiteId, website_id: websiteId,
session_id: sessionId, session_id: sessionId,
event_id: uuid(), event_id: uuid(),
rev_id: website?.revId || 0,
country: country ? country : null,
subdivision1: subdivision1 ? subdivision1 : null,
subdivision2: subdivision2 ? subdivision2 : null,
city: city ? city : null,
url: url?.substring(0, URL_LENGTH), url: url?.substring(0, URL_LENGTH),
event_type: EVENT_TYPE.customEvent, event_type: EVENT_TYPE.customEvent,
event_name: eventName?.substring(0, EVENT_NAME_LENGTH), event_name: eventName?.substring(0, EVENT_NAME_LENGTH),
event_data: eventData ? JSON.stringify(eventData) : null, event_data: eventData ? JSON.stringify(eventData) : null,
rev_id: website?.revId || 0,
created_at: getDateFormat(new Date()), created_at: getDateFormat(new Date()),
country: country ? country : null,
...args, ...args,
}; };

View File

@ -17,6 +17,9 @@ export async function savePageView(args: {
screen?: string; screen?: string;
language?: string; language?: string;
country?: string; country?: string;
subdivision1?: string;
subdivision2?: string;
city?: string;
}) { }) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(args), [PRISMA]: () => relationalQuery(args),
@ -45,19 +48,32 @@ async function relationalQuery(data: {
} }
async function clickhouseQuery(data) { async function clickhouseQuery(data) {
const { websiteId, id: sessionId, url, referrer, country, ...args } = data; const {
const website = await cache.fetchWebsite(websiteId); websiteId,
id: sessionId,
url,
referrer,
country,
subdivision1,
subdivision2,
city,
...args
} = data;
const { getDateFormat, sendMessage } = kafka; const { getDateFormat, sendMessage } = kafka;
const website = await cache.fetchWebsite(websiteId);
const message = { const message = {
session_id: sessionId,
website_id: websiteId, website_id: websiteId,
session_id: sessionId,
rev_id: website?.revId || 0,
country: country ? country : null,
subdivision1: subdivision1 ? subdivision1 : null,
subdivision2: subdivision2 ? subdivision2 : null,
city: city ? city : null,
url: url?.substring(0, URL_LENGTH), url: url?.substring(0, URL_LENGTH),
referrer: referrer?.substring(0, URL_LENGTH), referrer: referrer?.substring(0, URL_LENGTH),
rev_id: website?.revId || 0,
created_at: getDateFormat(new Date()),
country: country ? country : null,
event_type: EVENT_TYPE.pageView, event_type: EVENT_TYPE.pageView,
created_at: getDateFormat(new Date()),
...args, ...args,
}; };

View File

@ -31,8 +31,24 @@ async function clickhouseQuery(data: {
screen?: string; screen?: string;
language?: string; language?: string;
country?: string; country?: string;
subdivision1?: string;
subdivision2?: string;
city?: string;
}) { }) {
const { id, websiteId, hostname, browser, os, device, screen, language, country } = data; const {
id,
websiteId,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
subdivision2,
city,
} = data;
const { getDateFormat, sendMessage } = kafka; const { getDateFormat, sendMessage } = kafka;
const website = await cache.fetchWebsite(websiteId); const website = await cache.fetchWebsite(websiteId);
@ -46,6 +62,9 @@ async function clickhouseQuery(data: {
screen, screen,
language, language,
country, country,
subdivision1,
subdivision2,
city,
rev_id: website?.revId || 0, rev_id: website?.revId || 0,
created_at: getDateFormat(new Date()), created_at: getDateFormat(new Date()),
}; };

View File

@ -31,7 +31,10 @@ async function clickhouseQuery({ id: sessionId }: { id: string }) {
device, device,
screen, screen,
language, language,
country country,
subdivision1,
subdivision2,
city
from event from event
where session_id = {sessionId:UUID} where session_id = {sessionId:UUID}
limit 1`, limit 1`,

View File

@ -40,7 +40,10 @@ async function clickhouseQuery(websites: string[], startAt: Date) {
device, device,
screen, screen,
language, language,
country country,
subdivision1,
subdivision2,
city
from event from event
where ${websites && websites.length > 0 ? `website_id in {websites:Array(UUID)}` : '0 = 0'} where ${websites && websites.length > 0 ? `website_id in {websites:Array(UUID)}` : '0 = 0'}
and created_at >= {startAt:DateTime('UTC')}`, and created_at >= {startAt:DateTime('UTC')}`,

View File

@ -12,7 +12,7 @@ let url =
if (process.env.MAXMIND_LICENSE_KEY) { if (process.env.MAXMIND_LICENSE_KEY) {
url = url =
`https://download.maxmind.com/app/geoip_download` + `https://download.maxmind.com/app/geoip_download` +
`?edition_id=GeoLite2-Country&license_key=${process.env.MAXMIND_LICENSE_KEY}&suffix=tar.gz`; `?edition_id=GeoLite2-City&license_key=${process.env.MAXMIND_LICENSE_KEY}&suffix=tar.gz`;
} }
const dest = path.resolve(__dirname, '../node_modules/.geo'); const dest = path.resolve(__dirname, '../node_modules/.geo');

View File

@ -1,10 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
require('dotenv').config(); require('dotenv').config();
const fs = require('fs');
const path = require('path'); const path = require('path');
const https = require('https');
const zlib = require('zlib');
const tar = require('tar');
const maxmind = require('maxmind'); const maxmind = require('maxmind');
async function getLocation() { async function getLocation() {