86 lines
2.2 KiB
JavaScript
86 lines
2.2 KiB
JavaScript
import { z } from 'zod'
|
|
import {
|
|
isBotUserAgent,
|
|
isTrackableAnalyticsPath,
|
|
normalizePageSlugForAnalytics,
|
|
normalizePostSlugForAnalytics
|
|
} from '../../lib/analytics.js'
|
|
import { getPageBySlug, getPostBySlug } from '../repositories/content-repository.js'
|
|
import {
|
|
createSessionHashFromEvent,
|
|
recordAnalyticsHeartbeat
|
|
} from '../repositories/analytics-repository.js'
|
|
|
|
const heartbeatInputSchema = z.object({
|
|
path: z.string().trim().min(1).max(500),
|
|
postSlug: z.string().trim().max(200).optional().default(''),
|
|
pageSlug: z.string().trim().max(200).optional().default(''),
|
|
clientSessionId: z.string().trim().min(8).max(120),
|
|
durationSeconds: z.number().int().min(0).max(1800),
|
|
maxScrollRatio: z.number().min(0).max(1)
|
|
})
|
|
|
|
/**
|
|
* heartbeat 추적 요청을 처리한다.
|
|
* @param {import('h3').H3Event} event - H3 이벤트
|
|
* @returns {Promise<{ ok: true }>}
|
|
*/
|
|
export const handleAnalyticsHeartbeat = async (event) => {
|
|
const parsedBody = heartbeatInputSchema.safeParse(await readBody(event))
|
|
|
|
if (!parsedBody.success) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: '통계 heartbeat 형식이 올바르지 않습니다.'
|
|
})
|
|
}
|
|
|
|
const body = parsedBody.data
|
|
const userAgent = String(getRequestHeader(event, 'user-agent') || '')
|
|
|
|
if (isBotUserAgent(userAgent)) {
|
|
return { ok: true }
|
|
}
|
|
|
|
if (!isTrackableAnalyticsPath(body.path)) {
|
|
return { ok: true }
|
|
}
|
|
|
|
const postSlug = normalizePostSlugForAnalytics(body.postSlug)
|
|
const pageSlug = normalizePageSlugForAnalytics(body.pageSlug)
|
|
let postId = null
|
|
let pageId = null
|
|
|
|
if (postSlug) {
|
|
const post = await getPostBySlug(postSlug)
|
|
if (!post) {
|
|
return { ok: true }
|
|
}
|
|
postId = post.id
|
|
}
|
|
|
|
if (!postId && pageSlug) {
|
|
const page = await getPageBySlug(pageSlug)
|
|
if (!page) {
|
|
return { ok: true }
|
|
}
|
|
pageId = page.id
|
|
}
|
|
|
|
const sessionHash = createSessionHashFromEvent(event, body.clientSessionId)
|
|
|
|
await recordAnalyticsHeartbeat({
|
|
event,
|
|
sessionHash,
|
|
path: body.path,
|
|
postId,
|
|
postSlug,
|
|
pageId,
|
|
pageSlug,
|
|
durationSeconds: body.durationSeconds,
|
|
maxScrollRatio: body.maxScrollRatio
|
|
})
|
|
|
|
return { ok: true }
|
|
}
|