Initial commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import { Pool } from 'pg'
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
})
|
||||
|
||||
// Initialize tables
|
||||
pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS ping_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
ms INTEGER,
|
||||
code INTEGER,
|
||||
error TEXT,
|
||||
ts BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::BIGINT)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ping_site ON ping_logs(site_id, ts DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pageviews (
|
||||
id SERIAL PRIMARY KEY,
|
||||
domain TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
referrer TEXT,
|
||||
country TEXT,
|
||||
city TEXT,
|
||||
ua TEXT,
|
||||
device TEXT,
|
||||
browser TEXT,
|
||||
os TEXT,
|
||||
visitor_id TEXT NOT NULL,
|
||||
ts BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::BIGINT)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pv_domain ON pageviews(domain, ts DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_pv_ts ON pageviews(ts DESC);
|
||||
`).catch(console.error)
|
||||
|
||||
export type PingLog = {
|
||||
id: number
|
||||
site_id: string
|
||||
status: 'up' | 'down' | 'degraded'
|
||||
ms: number | null
|
||||
code: number | null
|
||||
error: string | null
|
||||
ts: number
|
||||
}
|
||||
|
||||
export async function insertPingLog(log: Omit<PingLog, 'id' | 'ts'>) {
|
||||
await pool.query(
|
||||
`INSERT INTO ping_logs (site_id, status, ms, code, error) VALUES ($1, $2, $3, $4, $5)`,
|
||||
[log.site_id, log.status, log.ms, log.code, log.error]
|
||||
)
|
||||
}
|
||||
|
||||
export async function getRecentLogs(siteId: string, limit = 100): Promise<PingLog[]> {
|
||||
const res = await pool.query(
|
||||
`SELECT * FROM ping_logs WHERE site_id = $1 ORDER BY ts DESC LIMIT $2`,
|
||||
[siteId, limit]
|
||||
)
|
||||
return res.rows.map(r => ({ ...r, ms: Number(r.ms), code: Number(r.code), ts: Number(r.ts) }))
|
||||
}
|
||||
|
||||
export async function getUptimePercent(siteId: string, hours = 24): Promise<number> {
|
||||
const since = Math.floor(Date.now() / 1000) - hours * 3600
|
||||
const res = await pool.query(
|
||||
`SELECT status FROM ping_logs WHERE site_id = $1 AND ts > $2`,
|
||||
[siteId, since]
|
||||
)
|
||||
const rows = res.rows
|
||||
if (rows.length === 0) return 100
|
||||
const up = rows.filter(r => r.status === 'up').length
|
||||
return Math.round((up / rows.length) * 1000) / 10
|
||||
}
|
||||
|
||||
export async function getLastLog(siteId: string): Promise<PingLog | null> {
|
||||
const res = await pool.query(
|
||||
`SELECT * FROM ping_logs WHERE site_id = $1 ORDER BY ts DESC LIMIT 1`,
|
||||
[siteId]
|
||||
)
|
||||
if (res.rows.length === 0) return null
|
||||
const r = res.rows[0]
|
||||
return { ...r, ms: Number(r.ms), code: Number(r.code), ts: Number(r.ts) }
|
||||
}
|
||||
|
||||
export type Pageview = {
|
||||
id: number
|
||||
domain: string
|
||||
path: string
|
||||
referrer: string | null
|
||||
country: string | null
|
||||
city: string | null
|
||||
ua: string | null
|
||||
device: string | null
|
||||
browser: string | null
|
||||
os: string | null
|
||||
visitor_id: string
|
||||
ts: number
|
||||
}
|
||||
|
||||
export async function insertPageview(pv: Omit<Pageview, 'id' | 'ts'>) {
|
||||
await pool.query(
|
||||
`INSERT INTO pageviews (domain, path, referrer, country, city, ua, device, browser, os, visitor_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
[pv.domain, pv.path, pv.referrer, pv.country, pv.city, pv.ua, pv.device, pv.browser, pv.os, pv.visitor_id]
|
||||
)
|
||||
}
|
||||
|
||||
export async function getAnalyticsStats(domain: string, days = 30) {
|
||||
const since = Math.floor(Date.now() / 1000) - days * 86400
|
||||
|
||||
const totalRes = await pool.query(`SELECT COUNT(*) as n FROM pageviews WHERE domain = $1 AND ts > $2`, [domain, since])
|
||||
const total = Number(totalRes.rows[0].n)
|
||||
|
||||
const uniqueRes = await pool.query(`SELECT COUNT(DISTINCT visitor_id) as n FROM pageviews WHERE domain = $1 AND ts > $2`, [domain, since])
|
||||
const unique = Number(uniqueRes.rows[0].n)
|
||||
|
||||
const topPagesRes = await pool.query(`
|
||||
SELECT path, COUNT(*) as views
|
||||
FROM pageviews WHERE domain = $1 AND ts > $2
|
||||
GROUP BY path ORDER BY views DESC LIMIT 10
|
||||
`, [domain, since])
|
||||
const topPages = topPagesRes.rows.map(r => ({ path: r.path, views: Number(r.views) }))
|
||||
|
||||
const topReferrersRes = await pool.query(`
|
||||
SELECT referrer, COUNT(*) as visits
|
||||
FROM pageviews WHERE domain = $1 AND ts > $2 AND referrer IS NOT NULL AND referrer != ''
|
||||
GROUP BY referrer ORDER BY visits DESC LIMIT 10
|
||||
`, [domain, since])
|
||||
const topReferrers = topReferrersRes.rows.map(r => ({ referrer: r.referrer, visits: Number(r.visits) }))
|
||||
|
||||
const byDeviceRes = await pool.query(`
|
||||
SELECT device, COUNT(*) as n
|
||||
FROM pageviews WHERE domain = $1 AND ts > $2 AND device IS NOT NULL
|
||||
GROUP BY device ORDER BY n DESC
|
||||
`, [domain, since])
|
||||
const byDevice = byDeviceRes.rows.map(r => ({ device: r.device, n: Number(r.n) }))
|
||||
|
||||
const byBrowserRes = await pool.query(`
|
||||
SELECT browser, COUNT(*) as n
|
||||
FROM pageviews WHERE domain = $1 AND ts > $2 AND browser IS NOT NULL
|
||||
GROUP BY browser ORDER BY n DESC LIMIT 6
|
||||
`, [domain, since])
|
||||
const byBrowser = byBrowserRes.rows.map(r => ({ browser: r.browser, n: Number(r.n) }))
|
||||
|
||||
const byOsRes = await pool.query(`
|
||||
SELECT os, COUNT(*) as n
|
||||
FROM pageviews WHERE domain = $1 AND ts > $2 AND os IS NOT NULL
|
||||
GROUP BY os ORDER BY n DESC LIMIT 6
|
||||
`, [domain, since])
|
||||
const byOs = byOsRes.rows.map(r => ({ os: r.os, n: Number(r.n) }))
|
||||
|
||||
const byCountryRes = await pool.query(`
|
||||
SELECT country, COUNT(*) as n
|
||||
FROM pageviews WHERE domain = $1 AND ts > $2 AND country IS NOT NULL
|
||||
GROUP BY country ORDER BY n DESC LIMIT 10
|
||||
`, [domain, since])
|
||||
const byCountry = byCountryRes.rows.map(r => ({ country: r.country, n: Number(r.n) }))
|
||||
|
||||
const dailyRes = await pool.query(`
|
||||
SELECT to_char(to_timestamp(ts), 'YYYY-MM-DD') as day, COUNT(*) as views, COUNT(DISTINCT visitor_id) as visitors
|
||||
FROM pageviews WHERE domain = $1 AND ts > $2
|
||||
GROUP BY day ORDER BY day ASC
|
||||
`, [domain, since])
|
||||
const daily = dailyRes.rows.map(r => ({ day: r.day, views: Number(r.views), visitors: Number(r.visitors) }))
|
||||
|
||||
return { total, unique, topPages, topReferrers, byDevice, byBrowser, byOs, byCountry, daily }
|
||||
}
|
||||
|
||||
export async function getAllDomains(): Promise<string[]> {
|
||||
const res = await pool.query(`SELECT DISTINCT domain FROM pageviews ORDER BY domain`)
|
||||
return res.rows.map(r => r.domain)
|
||||
}
|
||||
|
||||
export default pool
|
||||
Reference in New Issue
Block a user