feat: basic telemetry api for badge and blank image
This commit is contained in:
parent
4f4a1cfcef
commit
bd15e47765
@ -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])
|
||||||
|
}
|
||||||
|
102
src/server/model/telemetry.ts
Normal file
102
src/server/model/telemetry.ts
Normal 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;
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user