Wochenreport

This commit is contained in:
itsrubberduck
2026-05-06 17:38:36 +02:00
parent 2bcd27c635
commit f38b47acbd
14 changed files with 954 additions and 1 deletions

View File

@@ -0,0 +1,42 @@
import { createError, readBody } from 'h3'
import { LandingAnalyticsEvent, type LandingAnalyticsEventType } from '../../../models/LandingAnalyticsEvent'
interface LandingAnalyticsBody {
type?: LandingAnalyticsEventType
product?: string
sessionId?: string
path?: string
source?: string
scrollDepth?: number
}
const allowedTypes = new Set<LandingAnalyticsEventType>(['view', 'scrolled', 'waitlist_submit'])
function cleanText(value: unknown, maxLength = 180) {
return typeof value === 'string' ? value.trim().slice(0, maxLength) : undefined
}
export default defineEventHandler(async (event) => {
const body = await readBody<LandingAnalyticsBody>(event).catch(() => ({} as LandingAnalyticsBody))
const type = body.type && allowedTypes.has(body.type) ? body.type : null
if (!type) {
throw createError({ statusCode: 400, statusMessage: 'Valid analytics event type is required.' })
}
const scrollDepth =
typeof body.scrollDepth === 'number' && Number.isFinite(body.scrollDepth)
? Math.max(0, Math.min(100, Math.round(body.scrollDepth)))
: undefined
await LandingAnalyticsEvent.create({
type,
product: body.product === 'liveatc' ? 'liveatc' : 'classroom',
sessionId: cleanText(body.sessionId, 80),
path: cleanText(body.path, 180),
source: cleanText(body.source, 180),
scrollDepth,
})
return { success: true }
})

View File

@@ -0,0 +1,57 @@
import { createError, readBody } from 'h3'
import { getUserFromEvent } from '../../../utils/auth'
import { ProductUsageSession } from '../../../models/ProductUsageSession'
interface ProductSessionBody {
product?: string
path?: string
durationSeconds?: number
startedAt?: string
endedAt?: string
}
function cleanPath(value: unknown) {
return typeof value === 'string' ? value.trim().slice(0, 180) : undefined
}
function parseDate(value: unknown, fallback: Date) {
if (typeof value !== 'string') {
return fallback
}
const parsed = new Date(value)
return Number.isNaN(parsed.getTime()) ? fallback : parsed
}
export default defineEventHandler(async (event) => {
const user = await getUserFromEvent(event)
const body = await readBody<ProductSessionBody>(event).catch(() => ({} as ProductSessionBody))
const product = body.product === 'liveatc' ? 'liveatc' : body.product === 'classroom' ? 'classroom' : null
if (!product) {
throw createError({ statusCode: 400, statusMessage: 'Valid product is required.' })
}
const durationSeconds =
typeof body.durationSeconds === 'number' && Number.isFinite(body.durationSeconds)
? Math.max(1, Math.min(60 * 60 * 6, Math.round(body.durationSeconds)))
: 0
if (durationSeconds < 5) {
return { success: true, ignored: true }
}
const endedAt = parseDate(body.endedAt, new Date())
const startedAt = parseDate(body.startedAt, new Date(endedAt.getTime() - durationSeconds * 1000))
await ProductUsageSession.create({
user: user?._id,
product,
path: cleanPath(body.path),
durationSeconds,
startedAt,
endedAt,
})
return { success: true }
})