Merge pull request #1803 from umami-software/feat/um-171-cloud-mode-env-variable

Cities, Subdivisions, Page Title
This commit is contained in:
Mike Cao 2023-03-01 16:14:29 -08:00 committed by GitHub
commit 94165ca5ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 3794 additions and 99 deletions

View File

@ -15,9 +15,13 @@ CREATE TABLE event
screen LowCardinality(String), screen LowCardinality(String),
language LowCardinality(String), language LowCardinality(String),
country LowCardinality(String), country LowCardinality(String),
subdivision1 LowCardinality(String),
subdivision2 LowCardinality(String),
city String,
--pageview --pageview
url String, url String,
referrer String, referrer String,
page_title String,
--event --event
event_type UInt32, event_type UInt32,
event_name String, event_name String,
@ -33,8 +37,7 @@ CREATE TABLE event_queue (
session_id UUID, session_id UUID,
event_id Nullable(UUID), event_id Nullable(UUID),
rev_id UInt32, rev_id UInt32,
url String, --session
referrer String,
hostname LowCardinality(String), hostname LowCardinality(String),
browser LowCardinality(String), browser LowCardinality(String),
os LowCardinality(String), os LowCardinality(String),
@ -42,6 +45,14 @@ CREATE TABLE event_queue (
screen LowCardinality(String), screen LowCardinality(String),
language LowCardinality(String), language LowCardinality(String),
country LowCardinality(String), country LowCardinality(String),
subdivision1 LowCardinality(String),
subdivision2 LowCardinality(String),
city String,
--pageview
url String,
referrer String,
page_title String,
--event
event_type UInt32, event_type UInt32,
event_name String, event_name String,
event_data String, event_data String,
@ -60,8 +71,6 @@ SELECT website_id,
session_id, session_id,
event_id, event_id,
rev_id, rev_id,
url,
referrer,
hostname, hostname,
browser, browser,
os, os,
@ -69,8 +78,14 @@ SELECT website_id,
screen, screen,
language, language,
country, country,
subdivision1,
subdivision2,
city,
url,
referrer,
page_title,
event_type, event_type,
event_name, event_name,
if((empty(event_data) = 0) AND startsWith(event_data, '"'), concat('{', event_data, ': true}'), event_data) AS event_data, event_data,
created_at created_at
FROM event_queue; FROM event_queue;

View File

@ -24,6 +24,9 @@ CREATE TABLE `session` (
`screen` VARCHAR(11) NULL, `screen` VARCHAR(11) NULL,
`language` VARCHAR(35) NULL, `language` VARCHAR(35) NULL,
`country` CHAR(2) NULL, `country` CHAR(2) NULL,
`subdivision1` CHAR(3) NULL,
`subdivision2` VARCHAR(50) NULL,
`city` VARCHAR(50) NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
UNIQUE INDEX `session_session_id_key`(`session_id`), UNIQUE INDEX `session_session_id_key`(`session_id`),
@ -60,6 +63,7 @@ CREATE TABLE `website_event` (
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`url` VARCHAR(500) NOT NULL, `url` VARCHAR(500) NOT NULL,
`referrer` VARCHAR(500) NULL, `referrer` VARCHAR(500) NULL,
`page_title` VARCHAR(500) NULL,
`event_type` INTEGER UNSIGNED NOT NULL DEFAULT 1, `event_type` INTEGER UNSIGNED NOT NULL DEFAULT 1,
`event_name` VARCHAR(50) NULL, `event_name` VARCHAR(50) NULL,
`event_data` JSON NULL, `event_data` JSON NULL,
@ -117,3 +121,6 @@ CREATE TABLE `team_website` (
INDEX `team_website_website_id_idx`(`website_id`), INDEX `team_website_website_id_idx`(`website_id`),
PRIMARY KEY (`team_website_id`) PRIMARY KEY (`team_website_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddSystemUser
INSERT INTO "user" (user_id, username, role, password) VALUES ('41e2b680-648e-4b09-bcd7-3e2b10c06264' , 'admin', 'admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa');

View File

@ -34,6 +34,9 @@ model Session {
screen String? @db.VarChar(11) screen String? @db.VarChar(11)
language String? @db.VarChar(35) language String? @db.VarChar(35)
country String? @db.Char(2) country String? @db.Char(2)
subdivision1 String? @db.Char(3)
subdivision2 String? @db.VarChar(50)
city String? @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
@@index([createdAt]) @@index([createdAt])
@ -68,6 +71,7 @@ model WebsiteEvent {
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
url String @db.VarChar(500) url String @db.VarChar(500)
referrer String? @db.VarChar(500) referrer String? @db.VarChar(500)
pageTitle String? @map("page_title") @db.VarChar(500)
eventType Int @default(1) @map("event_type") @db.UnsignedInt eventType Int @default(1) @map("event_type") @db.UnsignedInt
eventName String? @map("event_name") @db.VarChar(50) eventName String? @map("event_name") @db.VarChar(50)
eventData Json? @map("event_data") eventData Json? @map("event_data")

View File

@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "session" ADD COLUMN "city" VARCHAR(50),
ADD COLUMN "subdivision1" CHAR(3),
ADD COLUMN "subdivision2" VARCHAR(50);
-- AlterTable
ALTER TABLE "website_event" ADD COLUMN "page_title" VARCHAR(500);

View File

@ -34,6 +34,9 @@ model Session {
screen String? @db.VarChar(11) screen String? @db.VarChar(11)
language String? @db.VarChar(35) language String? @db.VarChar(35)
country String? @db.Char(2) country String? @db.Char(2)
subdivision1 String? @db.Char(3)
subdivision2 String? @db.VarChar(50)
city String? @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
@@index([createdAt]) @@index([createdAt])
@ -68,6 +71,7 @@ model WebsiteEvent {
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
url String @db.VarChar(500) url String @db.VarChar(500)
referrer String? @db.VarChar(500) referrer String? @db.VarChar(500)
pageTitle String? @map("page_title") @db.VarChar(500)
eventType Int @default(1) @map("event_type") @db.Integer eventType Int @default(1) @map("event_type") @db.Integer
eventName String? @map("event_name") @db.VarChar(50) eventName String? @map("event_name") @db.VarChar(50)
eventData Json? @map("event_data") eventData Json? @map("event_data")

View File

@ -112,9 +112,13 @@ function getFilterQuery(filters = {}, params = {}) {
switch (key) { switch (key) {
case 'url': case 'url':
case 'pageTitle':
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 +151,25 @@ 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,
pageTitle,
os,
browser,
device,
country,
subdivision1,
subdivision2,
city,
eventName,
query,
} = filters;
const pageviewFilters = { domain, url, referrer, query }; const pageviewFilters = { domain, url, referrer, query, pageTitle };
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

@ -27,7 +27,7 @@ export function getIpAddress(req) {
return requestIp.getClientIp(req); return requestIp.getClientIp(req);
} }
export function getDevice(screen, browser, os) { export function getDevice(screen, os) {
if (!screen) return; if (!screen) return;
const [width] = screen.split('x'); const [width] = screen.split('x');
@ -55,12 +55,7 @@ export function getDevice(screen, browser, os) {
} }
} }
export async function getCountry(req, ip) { export async function getLocation(ip) {
// Cloudflare
if (req.headers['cf-ipcountry']) {
return req.headers['cf-ipcountry'];
}
// Ignore local ips // Ignore local ips
if (await isLocalhost(ip)) { if (await isLocalhost(ip)) {
return; return;
@ -68,23 +63,31 @@ export async function getCountry(req, ip) {
// Database lookup // Database lookup
if (!lookup) { if (!lookup) {
lookup = await maxmind.open(path.resolve('node_modules/.geo/GeoLite2-Country.mmdb')); lookup = await maxmind.open(path.resolve('node_modules/.geo/GeoLite2-City.mmdb'));
} }
const result = lookup.get(ip); const result = lookup.get(ip);
const country = result?.country?.iso_code ?? result?.registered_country?.iso_code;
const subdivision1 = result?.subdivisions[0]?.iso_code;
const subdivision2 = result?.subdivisions[1]?.names?.en;
const city = result?.city?.names?.en;
return result?.country?.iso_code; return { country, subdivision1, subdivision2, city };
} }
export async function getClientInfo(req, { screen }) { export async function getClientInfo(req, { screen }) {
const userAgent = req.headers['user-agent']; const userAgent = req.headers['user-agent'];
const ip = getIpAddress(req); const ip = getIpAddress(req);
const country = await getCountry(req, ip); const location = await getLocation(ip);
const country = location.country;
const subdivision1 = location.subdivision1;
const subdivision2 = location.subdivision2;
const city = location.city;
const browser = browserName(userAgent); const browser = browserName(userAgent);
const os = detectOS(userAgent); const os = detectOS(userAgent);
const device = getDevice(screen, browser, os); const device = getDevice(screen, browser, os);
return { userAgent, browser, os, ip, country, device }; return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };
} }
export function getJsonBody(req) { export function getJsonBody(req) {

View File

@ -133,8 +133,12 @@ function getFilterQuery(filters = {}, params = []): string {
switch (key) { switch (key) {
case 'url': case 'url':
case 'os': case 'os':
case 'pageTitle':
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 +175,25 @@ 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,
pageTitle,
os,
browser,
device,
country,
subdivision1,
subdivision2,
city,
eventName,
query,
} = filters;
const pageviewFilters = { domain, url, referrer, query }; const pageviewFilters = { domain, url, referrer, query, pageTitle };
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 +202,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;
}; };
} }
@ -31,7 +34,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
const { type, payload } = getJsonBody(req); const { type, payload } = getJsonBody(req);
const { referrer, eventName, eventData } = payload; const { referrer, eventName, eventData, pageTitle } = payload;
let { url } = payload; let { url } = payload;
// Validate eventData is JSON // Validate eventData is JSON
@ -95,12 +98,13 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
} }
if (type === 'pageview') { if (type === 'pageview') {
await savePageView({ ...session, url, referrer }); await savePageView({ ...session, url, referrer, pageTitle });
} else if (type === 'event') { } else if (type === 'event') {
await saveEvent({ await saveEvent({
...session, ...session,
url, url,
referrer, referrer,
pageTitle,
eventName, eventName,
eventData, eventData,
}); });

View File

@ -18,7 +18,7 @@ export default async (req: NextApiRequest, res: NextApiResponse<ConfigResponse>)
updatesDisabled: !!process.env.DISABLE_UPDATES, updatesDisabled: !!process.env.DISABLE_UPDATES,
telemetryDisabled: !!process.env.DISABLE_TELEMETRY, telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
adminDisabled: !!process.env.DISABLE_ADMIN, adminDisabled: !!process.env.DISABLE_ADMIN,
cloudMode: true, //!!process.env.CLOUD_MODE, cloudMode: process.env.CLOUD_MODE,
}); });
} }

View File

@ -2,10 +2,10 @@ import { canCreateUser, canViewUsers } from 'lib/auth';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto'; import { uuid } from 'lib/crypto';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiRequestQueryBody, User } from 'lib/types';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createUser, getUser, getUsers, User } from 'queries'; import { createUser, getUser, getUsers } from 'queries';
export interface UsersRequestBody { export interface UsersRequestBody {
username: string; username: string;

View File

@ -7,7 +7,7 @@ import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getPageviewMetrics, getSessionMetrics, getWebsite } from 'queries'; import { getPageviewMetrics, getSessionMetrics, getWebsite } from 'queries';
const sessionColumns = ['browser', 'os', 'device', 'screen', 'country', 'language']; const sessionColumns = ['browser', 'os', 'device', 'screen', 'country', 'language'];
const pageviewColumns = ['url', 'referrer', 'query']; const pageviewColumns = ['url', 'referrer', 'query', 'pageTitle'];
function getTable(type) { function getTable(type) {
if (type === 'event') { if (type === 'event') {
@ -42,10 +42,14 @@ export interface WebsiteMetricsRequestQuery {
endAt: number; endAt: number;
url: string; url: string;
referrer: string; referrer: string;
pageTitle: string;
os: string; os: string;
browser: string; browser: string;
device: string; device: string;
country: string; country: string;
subdivision1: string;
subdivision2: string;
city: string;
} }
export default async ( export default async (
@ -62,10 +66,14 @@ export default async (
endAt, endAt,
url, url,
referrer, referrer,
pageTitle,
os, os,
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
} = req.query; } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
@ -86,6 +94,9 @@ export default async (
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}); });
@ -127,10 +138,14 @@ export default async (
domain, domain,
url: type !== 'url' && table !== 'event' ? url : undefined, url: type !== 'url' && table !== 'event' ? url : undefined,
referrer: type !== 'referrer' && table !== 'event' ? referrer : FILTER_IGNORED, referrer: type !== 'referrer' && table !== 'event' ? referrer : FILTER_IGNORED,
pageTitle: type !== 'pageTitle' && table !== 'event' ? pageTitle : undefined,
os: type !== 'os' ? os : undefined, os: type !== 'os' ? os : undefined,
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

@ -17,10 +17,14 @@ export interface WebsitePageviewRequestQuery {
timezone: string; timezone: string;
url?: string; url?: string;
referrer?: string; referrer?: string;
pageTitle?: string;
os?: string; os?: string;
browser?: string; browser?: string;
device?: string; device?: string;
country?: string; country?: string;
subdivision1?: string;
subdivision2?: string;
city?: string;
} }
export default async ( export default async (
@ -38,10 +42,14 @@ export default async (
timezone, timezone,
url, url,
referrer, referrer,
pageTitle,
os, os,
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
} = req.query; } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
@ -66,10 +74,14 @@ export default async (
filters: { filters: {
url, url,
referrer, referrer,
pageTitle,
os, os,
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}), }),
getPageviewStats(websiteId, { getPageviewStats(websiteId, {
@ -80,10 +92,14 @@ export default async (
count: 'distinct website_event.', count: 'distinct website_event.',
filters: { filters: {
url, url,
pageTitle,
os, os,
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}), }),
]); ]);

View File

@ -13,10 +13,14 @@ export interface WebsiteStatsRequestQuery {
endAt: number; endAt: number;
url: string; url: string;
referrer: string; referrer: string;
pageTitle: string;
os: string; os: string;
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 +30,21 @@ 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,
pageTitle,
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))) {
@ -46,10 +64,14 @@ export default async (
filters: { filters: {
url, url,
referrer, referrer,
pageTitle,
os, os,
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}); });
const prevPeriod = await getWebsiteStats(websiteId, { const prevPeriod = await getWebsiteStats(websiteId, {
@ -58,10 +80,14 @@ export default async (
filters: { filters: {
url, url,
referrer, referrer,
pageTitle,
os, os,
browser, browser,
device, device,
country, country,
subdivision1,
subdivision2,
city,
}, },
}); });

3470
public/iso-3166-2.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ export async function saveEvent(args: {
websiteId: string; websiteId: string;
url: string; url: string;
referrer?: string; referrer?: string;
pageTitle?: string;
eventName?: string; eventName?: string;
eventData?: any; eventData?: any;
hostname?: string; hostname?: string;
@ -19,6 +20,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),
@ -31,43 +35,41 @@ async function relationalQuery(data: {
websiteId: string; websiteId: string;
url: string; url: string;
referrer?: string; referrer?: string;
pageTitle?: string;
eventName?: string; eventName?: string;
eventData?: any; eventData?: any;
}) { }) {
const { websiteId, id: sessionId, url, eventName, eventData, referrer } = data; const { websiteId, id: sessionId, url, eventName, eventData, referrer, pageTitle } = data;
const params = { return prisma.client.websiteEvent.create({
data: {
id: uuid(), id: uuid(),
websiteId, websiteId,
sessionId, sessionId,
url: url?.substring(0, URL_LENGTH), url: url?.substring(0, URL_LENGTH),
referrer: referrer?.substring(0, URL_LENGTH), referrer: referrer?.substring(0, URL_LENGTH),
pageTitle: pageTitle,
eventType: EVENT_TYPE.customEvent, eventType: EVENT_TYPE.customEvent,
eventName: eventName?.substring(0, EVENT_NAME_LENGTH), eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
eventData, eventData,
}; },
return prisma.client.websiteEvent.create({
data: params,
}); });
} }
async function clickhouseQuery(data: { async function clickhouseQuery(data) {
id: string; const {
websiteId: string; websiteId,
url: string; id: sessionId,
referrer?: string; url,
eventName?: string; pageTitle,
eventData?: any; eventName,
hostname?: string; eventData,
browser?: string; country,
os?: string; subdivision1,
device?: string; subdivision2,
screen?: string; city,
language?: string; ...args
country?: string; } = data;
}) {
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 +77,17 @@ 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),
page_title: pageTitle,
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

@ -10,6 +10,7 @@ export async function savePageView(args: {
websiteId: string; websiteId: string;
url: string; url: string;
referrer?: string; referrer?: string;
pageTitle?: string;
hostname?: string; hostname?: string;
browser?: string; browser?: string;
os?: string; os?: string;
@ -17,6 +18,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),
@ -29,8 +33,9 @@ async function relationalQuery(data: {
websiteId: string; websiteId: string;
url: string; url: string;
referrer?: string; referrer?: string;
pageTitle?: string;
}) { }) {
const { websiteId, id: sessionId, url, referrer } = data; const { websiteId, id: sessionId, url, referrer, pageTitle } = data;
return prisma.client.websiteEvent.create({ return prisma.client.websiteEvent.create({
data: { data: {
@ -39,25 +44,41 @@ async function relationalQuery(data: {
sessionId, sessionId,
url: url?.substring(0, URL_LENGTH), url: url?.substring(0, URL_LENGTH),
referrer: referrer?.substring(0, URL_LENGTH), referrer: referrer?.substring(0, URL_LENGTH),
pageTitle: pageTitle,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}, },
}); });
} }
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,
pageTitle,
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, page_title: pageTitle,
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

@ -35,7 +35,10 @@ async function clickhouseQuery(websiteId: string, startAt: Date) {
device, device,
screen, screen,
language, language,
country country,
subdivision1,
subdivision2,
city
from event from event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at >= {startAt:DateTime('UTC')}`, and created_at >= {startAt:DateTime('UTC')}`,

View File

@ -7,12 +7,12 @@ const zlib = require('zlib');
const tar = require('tar'); const tar = require('tar');
let url = let url =
'https://raw.githubusercontent.com/GitSquared/node-geolite2-redist/master/redist/GeoLite2-Country.tar.gz'; 'https://raw.githubusercontent.com/GitSquared/node-geolite2-redist/master/redist/GeoLite2-City.tar.gz';
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

@ -47,6 +47,7 @@
(dnt && doNotTrack()) || (dnt && doNotTrack()) ||
(domain && !domains.includes(hostname)); (domain && !domains.includes(hostname));
const tracker_delay_duration = 300;
const _data = 'data-'; const _data = 'data-';
const _false = 'false'; const _false = 'false';
const attr = currentScript.getAttribute.bind(currentScript); const attr = currentScript.getAttribute.bind(currentScript);
@ -68,6 +69,7 @@
let listeners = {}; let listeners = {};
let currentUrl = `${pathname}${search}`; let currentUrl = `${pathname}${search}`;
let currentRef = document.referrer; let currentRef = document.referrer;
let currentPageTitle = document.title;
let cache; let cache;
/* Collect metrics */ /* Collect metrics */
@ -92,22 +94,35 @@
.then(text => (cache = text)); .then(text => (cache = text));
}; };
const trackView = (url = currentUrl, referrer = currentRef, websiteId = website) => const trackView = (
url = currentUrl,
referrer = currentRef,
websiteId = website,
pageTitle = currentPageTitle,
) =>
collect( collect(
'pageview', 'pageview',
assign(getPayload(), { assign(getPayload(), {
website: websiteId, website: websiteId,
url, url,
referrer, referrer,
pageTitle,
}), }),
); );
const trackEvent = (eventName, eventData, url = currentUrl, websiteId = website) => const trackEvent = (
eventName,
eventData,
url = currentUrl,
websiteId = website,
pageTitle = currentPageTitle,
) =>
collect( collect(
'event', 'event',
assign(getPayload(), { assign(getPayload(), {
website: websiteId, website: websiteId,
url, url,
pageTitle,
eventName: eventName, eventName: eventName,
eventData: eventData, eventData: eventData,
}), }),
@ -162,6 +177,7 @@
const handlePush = (state, title, url) => { const handlePush = (state, title, url) => {
if (!url) return; if (!url) return;
observeTitle();
currentRef = currentUrl; currentRef = currentUrl;
const newUrl = url.toString(); const newUrl = url.toString();
@ -172,7 +188,7 @@
} }
if (currentUrl !== currentRef) { if (currentUrl !== currentRef) {
trackView(); setTimeout(() => trackView(), tracker_delay_duration);
} }
}; };
@ -189,6 +205,19 @@
observer.observe(document, { childList: true, subtree: true }); observer.observe(document, { childList: true, subtree: true });
}; };
const observeTitle = () => {
const monitorMutate = mutations => {
currentPageTitle = mutations[0].target.text;
};
const observer = new MutationObserver(monitorMutate);
observer.observe(document.querySelector('title'), {
subtree: true,
characterData: true,
childList: true,
});
};
/* Global */ /* Global */
if (!window.umami) { if (!window.umami) {