feat: basic telemetry api for badge and blank image

This commit is contained in:
moonrailgun 2023-09-17 19:59:34 +08:00
parent 4f4a1cfcef
commit bd15e47765
4 changed files with 152 additions and 8 deletions

View File

@ -28,7 +28,7 @@ model Workspace {
createdAt DateTime? @default(now()) @db.Timestamptz(6) createdAt DateTime? @default(now()) @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @db.Timestamptz(6) updatedAt DateTime? @updatedAt @db.Timestamptz(6)
users WorkspacesOnUsers[] users WorkspacesOnUsers[]
websites Website[] websites Website[]
// for user currentWorkspace // for user currentWorkspace
@ -170,3 +170,32 @@ model WebsiteSessionData {
@@index([websiteId]) @@index([websiteId])
@@index([sessionId]) @@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])
}

View File

@ -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<number> {
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<TelemetrySession | null> {
const session = await prisma.telemetrySession.findUnique({
where: {
id: sessionId,
},
});
if (!session) {
return null;
}
return session;
}

View File

@ -1,4 +1,5 @@
import { Router } from 'express'; import { Router } from 'express';
import { recordTelemetryEvent, sumTelemetryEvent } from '../model/telemetry';
import { numify } from '../utils/common'; import { numify } from '../utils/common';
const openBadge = require('openbadge'); const openBadge = require('openbadge');
@ -10,18 +11,22 @@ telemetryRouter.get('/blank.gif', async (req, res) => {
'base64' 'base64'
); );
recordTelemetryEvent(req);
res.header('Content-Type', 'image/gif').send(buffer); res.header('Content-Type', 'image/gif').send(buffer);
}); });
telemetryRouter.get('/badge.svg', async (req, res) => { telemetryRouter.get('/badge.svg', async (req, res) => {
const title = req.query.title || 'visitor'; 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) => { const svg = await new Promise((resolve, reject) => {
openBadge( openBadge(
{ {
text: [title, num], text: [title, numify(num + start)],
}, },
(err: any, badgeSvg: string) => { (err: any, badgeSvg: string) => {
if (err) { if (err) {

View File

@ -16,10 +16,7 @@ import maxmind, { Reader, CityResponse } from 'maxmind';
let lookup: Reader<CityResponse>; let lookup: Reader<CityResponse>;
export async function getClientInfo( export async function getRequestInfo(req: Request) {
req: Request,
payload: WebsiteEventPayload
) {
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, req); const location = await getLocation(ip, req);
@ -29,7 +26,6 @@ export async function getClientInfo(
const city = location?.city; const city = location?.city;
const browser = browserName(userAgent ?? ''); const browser = browserName(userAgent ?? '');
const os = detectOS(userAgent ?? ''); const os = detectOS(userAgent ?? '');
const device = getDevice(payload.screen, os);
return { return {
userAgent, userAgent,
@ -40,6 +36,18 @@ export async function getClientInfo(
subdivision1, subdivision1,
subdivision2, subdivision2,
city, city,
};
}
export async function getClientInfo(
req: Request,
payload: WebsiteEventPayload
) {
const requestInfo = await getRequestInfo(req);
const device = getDevice(payload.screen, requestInfo.os);
return {
...requestInfo,
device, device,
}; };
} }