mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-06 17:53:19 +08:00
Wochenreport
This commit is contained in:
42
server/api/service/analytics/landing.post.ts
Normal file
42
server/api/service/analytics/landing.post.ts
Normal 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 }
|
||||
})
|
||||
57
server/api/service/analytics/product-session.post.ts
Normal file
57
server/api/service/analytics/product-session.post.ts
Normal 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 }
|
||||
})
|
||||
Reference in New Issue
Block a user