From bd15e4776576f158adbd082311d593f9bff34913 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sun, 17 Sep 2023 19:59:34 +0800 Subject: [PATCH] feat: basic telemetry api for badge and blank image --- prisma/schema.prisma | 31 +++++++++- src/server/model/telemetry.ts | 102 +++++++++++++++++++++++++++++++++ src/server/router/telemetry.ts | 9 ++- src/server/utils/detect.ts | 18 ++++-- 4 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 src/server/model/telemetry.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cc18b13..7dcc8c1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,7 +28,7 @@ model Workspace { createdAt DateTime? @default(now()) @db.Timestamptz(6) updatedAt DateTime? @updatedAt @db.Timestamptz(6) - users WorkspacesOnUsers[] + users WorkspacesOnUsers[] websites Website[] // for user currentWorkspace @@ -170,3 +170,32 @@ model WebsiteSessionData { @@index([websiteId]) @@index([sessionId]) } + +model TelemetrySession { + id String @id @unique @db.Uuid + hostname String? @db.VarChar(100) + browser String? @db.VarChar(20) + os String? @db.VarChar(20) + country String? @db.Char(2) + subdivision1 String? @db.VarChar(20) + subdivision2 String? @db.VarChar(50) + city String? @db.VarChar(50) + createdAt DateTime? @default(now()) @db.Timestamptz(6) + + telemetryEvent TelemetryEvent[] + + @@index([createdAt]) +} + +model TelemetryEvent { + id String @id() @default(uuid()) @db.Uuid + sessionId String @db.Uuid + createdAt DateTime? @default(now()) @db.Timestamptz(6) + urlOrigin String @db.VarChar(500) + urlPath String @db.VarChar(500) + + session TelemetrySession @relation(fields: [sessionId], references: [id]) + + @@index([createdAt]) + @@index([sessionId]) +} diff --git a/src/server/model/telemetry.ts b/src/server/model/telemetry.ts new file mode 100644 index 0000000..a5b9926 --- /dev/null +++ b/src/server/model/telemetry.ts @@ -0,0 +1,102 @@ +import { TelemetrySession } from '@prisma/client'; +import { Request } from 'express'; +import { hashUuid } from '../utils/common'; +import { getRequestInfo } from '../utils/detect'; +import { prisma } from './_client'; + +export async function recordTelemetryEvent(req: Request) { + const url = req.query.url ?? req.headers.referer; + if (!(url && typeof url === 'string')) { + return; + } + + const session = await findSession(req, url); + if (!session) { + return; + } + + const { origin, pathname } = new URL(url); + + await prisma.telemetryEvent.create({ + data: { + sessionId: session.id, + urlOrigin: origin, + urlPath: pathname, + }, + }); +} + +export async function sumTelemetryEvent(req: Request): Promise { + const url = req.query.url ?? req.headers.referer; + if (!(url && typeof url === 'string')) { + return 0; + } + + const { origin, pathname } = new URL(url); + + const number = await prisma.telemetryEvent.count({ + where: { + urlOrigin: origin, + urlPath: pathname, + }, + }); + + return number; +} + +async function findSession(req: Request, url: string) { + const { hostname } = new URL(url); + + const { + userAgent, + browser, + os, + ip, + country, + subdivision1, + subdivision2, + city, + } = await getRequestInfo(req); + + const sessionId = hashUuid(hostname, ip, userAgent!); + + let session = await loadSession(sessionId); + if (!session) { + try { + session = await prisma.telemetrySession.create({ + data: { + id: sessionId, + hostname, + browser, + os, + country, + subdivision1, + subdivision2, + city, + }, + }); + } catch (e: any) { + if (!e.message.toLowerCase().includes('unique constraint')) { + throw e; + } + } + } + + return session; +} + +async function loadSession( + sessionId: string +): Promise { + const session = await prisma.telemetrySession.findUnique({ + where: { + id: sessionId, + }, + }); + + if (!session) { + return null; + } + + return session; +} diff --git a/src/server/router/telemetry.ts b/src/server/router/telemetry.ts index 14261e1..85c3c01 100644 --- a/src/server/router/telemetry.ts +++ b/src/server/router/telemetry.ts @@ -1,4 +1,5 @@ import { Router } from 'express'; +import { recordTelemetryEvent, sumTelemetryEvent } from '../model/telemetry'; import { numify } from '../utils/common'; const openBadge = require('openbadge'); @@ -10,18 +11,22 @@ telemetryRouter.get('/blank.gif', async (req, res) => { 'base64' ); + recordTelemetryEvent(req); + res.header('Content-Type', 'image/gif').send(buffer); }); telemetryRouter.get('/badge.svg', async (req, res) => { const title = req.query.title || 'visitor'; + const start = req.query.start ? Number(req.query.start) : 0; - const num = numify(11123243); + recordTelemetryEvent(req); + const num = await sumTelemetryEvent(req); const svg = await new Promise((resolve, reject) => { openBadge( { - text: [title, num], + text: [title, numify(num + start)], }, (err: any, badgeSvg: string) => { if (err) { diff --git a/src/server/utils/detect.ts b/src/server/utils/detect.ts index 4ef23b3..4ba7ac3 100644 --- a/src/server/utils/detect.ts +++ b/src/server/utils/detect.ts @@ -16,10 +16,7 @@ import maxmind, { Reader, CityResponse } from 'maxmind'; let lookup: Reader; -export async function getClientInfo( - req: Request, - payload: WebsiteEventPayload -) { +export async function getRequestInfo(req: Request) { const userAgent = req.headers['user-agent']; const ip = getIpAddress(req); const location = await getLocation(ip, req); @@ -29,7 +26,6 @@ export async function getClientInfo( const city = location?.city; const browser = browserName(userAgent ?? ''); const os = detectOS(userAgent ?? ''); - const device = getDevice(payload.screen, os); return { userAgent, @@ -40,6 +36,18 @@ export async function getClientInfo( subdivision1, subdivision2, city, + }; +} + +export async function getClientInfo( + req: Request, + payload: WebsiteEventPayload +) { + const requestInfo = await getRequestInfo(req); + const device = getDevice(payload.screen, requestInfo.os); + + return { + ...requestInfo, device, }; }