통계 콘텐츠 세션 원장 추가
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
ANALYTICS_ENGAGED_MIN_SECONDS,
|
||||
ANALYTICS_RETENTION_PURGE_INTERVAL_MS,
|
||||
ANALYTICS_VISITOR_HASH_RETENTION_DAYS,
|
||||
createContentSessionHash,
|
||||
clampAnalyticsDurationSeconds,
|
||||
clampAnalyticsScrollRatio,
|
||||
createDailyVisitorHash,
|
||||
@@ -39,6 +40,15 @@ const getAnalyticsHashSecret = () => {
|
||||
).trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 비어 있지 않은 통계 세션 ID인지 확인한다.
|
||||
* @param {string} value - 세션 ID 후보
|
||||
* @returns {boolean} 사용 가능 여부
|
||||
*/
|
||||
const hasAnalyticsSessionId = (value) => {
|
||||
return Boolean(String(value || '').trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 집계 시작일을 반환한다.
|
||||
* @returns {string} YYYY-MM-DD
|
||||
@@ -368,7 +378,7 @@ const recordPostAnalyticsTraffic = async (sql, day, postId, visitorHash, traffic
|
||||
|
||||
/**
|
||||
* 페이지뷰·읽음 이벤트를 기록한다.
|
||||
* @param {{ visitorHash: string, postId?: string | null, pageId?: string | null, traffic?: { sourceGroup: string, sourceName: string, deviceType: string, osName: string, keyword?: string }, recordSite?: boolean, recordView?: boolean, recordRead?: boolean }} input - 기록 입력
|
||||
* @param {{ visitorHash: string, sessionHash?: string, contentSessionHash?: string, path?: string, postId?: string | null, pageId?: string | null, traffic?: { sourceGroup: string, sourceName: string, deviceType: string, osName: string, keyword?: string }, recordSite?: boolean, recordView?: boolean, recordRead?: boolean }} input - 기록 입력
|
||||
* @returns {Promise<{ ok: true }>}
|
||||
*/
|
||||
export const recordAnalyticsPageview = async (input) => {
|
||||
@@ -380,14 +390,41 @@ export const recordAnalyticsPageview = async (input) => {
|
||||
|
||||
const day = getAnalyticsDayKey()
|
||||
const visitorHash = input.visitorHash
|
||||
const sessionHash = input.sessionHash || ''
|
||||
const contentSessionHash = input.contentSessionHash || ''
|
||||
const path = input.path || ''
|
||||
const postId = input.postId || null
|
||||
const pageId = input.pageId || null
|
||||
const recordSite = input.recordSite !== false
|
||||
const recordView = Boolean(input.recordView)
|
||||
const recordRead = Boolean(input.recordRead)
|
||||
const traffic = input.traffic || null
|
||||
const shouldRecordPageview = recordView || recordSite
|
||||
? await markContentSessionPageview(sql, {
|
||||
day,
|
||||
visitorHash,
|
||||
sessionHash,
|
||||
contentSessionHash,
|
||||
path,
|
||||
postId,
|
||||
pageId,
|
||||
traffic
|
||||
})
|
||||
: true
|
||||
const shouldRecordRead = recordRead
|
||||
? await markContentSessionRead(sql, {
|
||||
day,
|
||||
visitorHash,
|
||||
sessionHash,
|
||||
contentSessionHash,
|
||||
path,
|
||||
postId,
|
||||
pageId,
|
||||
traffic
|
||||
})
|
||||
: false
|
||||
|
||||
if (recordSite) {
|
||||
if (recordSite && shouldRecordPageview) {
|
||||
await ensureSiteDailyRow(sql, day)
|
||||
|
||||
const isNewSiteVisitor = await registerSiteVisitor(sql, day, visitorHash)
|
||||
@@ -406,7 +443,7 @@ export const recordAnalyticsPageview = async (input) => {
|
||||
}
|
||||
|
||||
if (!postId) {
|
||||
if (pageId && recordView) {
|
||||
if (pageId && recordView && shouldRecordPageview) {
|
||||
await ensurePageDailyRow(sql, day, pageId)
|
||||
|
||||
const isNewPageVisitor = await registerPageVisitor(sql, day, pageId, visitorHash)
|
||||
@@ -427,7 +464,7 @@ export const recordAnalyticsPageview = async (input) => {
|
||||
|
||||
await ensurePostDailyRow(sql, day, postId)
|
||||
|
||||
if (recordView) {
|
||||
if (recordView && shouldRecordPageview) {
|
||||
const isNewPostVisitor = await registerPostVisitor(sql, day, postId, visitorHash)
|
||||
|
||||
await sql`
|
||||
@@ -444,7 +481,7 @@ export const recordAnalyticsPageview = async (input) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (recordRead) {
|
||||
if (recordRead && shouldRecordRead) {
|
||||
await sql`
|
||||
UPDATE post_analytics_daily
|
||||
SET reads = reads + 1
|
||||
@@ -490,6 +527,20 @@ export const createSessionHashFromEvent = (event, clientSessionId) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 콘텐츠 단위 세션 해시를 만든다.
|
||||
* @param {import('h3').H3Event} event - H3 이벤트
|
||||
* @param {string} contentSessionId - 콘텐츠 단위 세션 ID
|
||||
* @returns {string} content session hash
|
||||
*/
|
||||
export const createContentSessionHashFromEvent = (event, contentSessionId) => {
|
||||
return createContentSessionHash({
|
||||
contentSessionId: String(contentSessionId || '').trim(),
|
||||
visitorHash: createVisitorHashFromEvent(event),
|
||||
secret: getAnalyticsHashSecret()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 만료된 실시간 세션 행을 정리한다.
|
||||
* @param {import('postgres').Sql} sql - DB 클라이언트
|
||||
@@ -522,9 +573,136 @@ const purgeAnalyticsRetention = async (sql) => {
|
||||
WHERE day < ${visitorHashCutoffDay}::date
|
||||
`
|
||||
|
||||
await sql`
|
||||
DELETE FROM analytics_content_sessions
|
||||
WHERE day < ${visitorHashCutoffDay}::date
|
||||
`
|
||||
|
||||
lastAnalyticsRetentionPurgeAt = now
|
||||
}
|
||||
|
||||
/**
|
||||
* 콘텐츠 세션에 pageview 처리를 표시한다.
|
||||
* @param {import('postgres').Sql} sql - DB 클라이언트
|
||||
* @param {{ day: string, contentSessionHash: string, visitorHash: string, sessionHash: string, path: string, postId?: string | null, pageId?: string | null, traffic?: { sourceGroup: string, sourceName: string, deviceType: string, osName: string, keyword?: string } | null }} input - 콘텐츠 세션 입력
|
||||
* @returns {Promise<boolean>} 새 pageview 집계 필요 여부
|
||||
*/
|
||||
const markContentSessionPageview = async (sql, input) => {
|
||||
if (!hasAnalyticsSessionId(input.contentSessionHash)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const traffic = input.traffic || {}
|
||||
const rows = await sql`
|
||||
INSERT INTO analytics_content_sessions (
|
||||
content_session_hash,
|
||||
day,
|
||||
visitor_hash,
|
||||
session_hash,
|
||||
path,
|
||||
post_id,
|
||||
page_id,
|
||||
source_group,
|
||||
source_name,
|
||||
device_type,
|
||||
os_name,
|
||||
keyword,
|
||||
view_recorded
|
||||
)
|
||||
VALUES (
|
||||
${input.contentSessionHash},
|
||||
${input.day},
|
||||
${input.visitorHash},
|
||||
${input.sessionHash},
|
||||
${input.path},
|
||||
${input.postId || null},
|
||||
${input.pageId || null},
|
||||
${traffic.sourceGroup || ''},
|
||||
${traffic.sourceName || ''},
|
||||
${traffic.deviceType || ''},
|
||||
${traffic.osName || ''},
|
||||
${traffic.keyword || ''},
|
||||
true
|
||||
)
|
||||
ON CONFLICT (content_session_hash) DO UPDATE
|
||||
SET
|
||||
day = EXCLUDED.day,
|
||||
visitor_hash = EXCLUDED.visitor_hash,
|
||||
session_hash = EXCLUDED.session_hash,
|
||||
path = EXCLUDED.path,
|
||||
post_id = EXCLUDED.post_id,
|
||||
page_id = EXCLUDED.page_id,
|
||||
source_group = EXCLUDED.source_group,
|
||||
source_name = EXCLUDED.source_name,
|
||||
device_type = EXCLUDED.device_type,
|
||||
os_name = EXCLUDED.os_name,
|
||||
keyword = EXCLUDED.keyword,
|
||||
view_recorded = true,
|
||||
last_seen_at = now()
|
||||
WHERE analytics_content_sessions.view_recorded = false
|
||||
RETURNING content_session_hash
|
||||
`
|
||||
|
||||
return rows.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 콘텐츠 세션에 read 처리를 표시한다.
|
||||
* @param {import('postgres').Sql} sql - DB 클라이언트
|
||||
* @param {{ day: string, contentSessionHash: string, visitorHash: string, sessionHash: string, path: string, postId?: string | null, pageId?: string | null, traffic?: { sourceGroup: string, sourceName: string, deviceType: string, osName: string, keyword?: string } | null }} input - 콘텐츠 세션 입력
|
||||
* @returns {Promise<boolean>} 새 read 집계 필요 여부
|
||||
*/
|
||||
const markContentSessionRead = async (sql, input) => {
|
||||
if (!hasAnalyticsSessionId(input.contentSessionHash)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const traffic = input.traffic || {}
|
||||
const rows = await sql`
|
||||
INSERT INTO analytics_content_sessions (
|
||||
content_session_hash,
|
||||
day,
|
||||
visitor_hash,
|
||||
session_hash,
|
||||
path,
|
||||
post_id,
|
||||
page_id,
|
||||
source_group,
|
||||
source_name,
|
||||
device_type,
|
||||
os_name,
|
||||
keyword,
|
||||
read_recorded,
|
||||
read_at
|
||||
)
|
||||
VALUES (
|
||||
${input.contentSessionHash},
|
||||
${input.day},
|
||||
${input.visitorHash},
|
||||
${input.sessionHash},
|
||||
${input.path},
|
||||
${input.postId || null},
|
||||
${input.pageId || null},
|
||||
${traffic.sourceGroup || ''},
|
||||
${traffic.sourceName || ''},
|
||||
${traffic.deviceType || ''},
|
||||
${traffic.osName || ''},
|
||||
${traffic.keyword || ''},
|
||||
true,
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (content_session_hash) DO UPDATE
|
||||
SET
|
||||
read_recorded = true,
|
||||
read_at = COALESCE(analytics_content_sessions.read_at, now()),
|
||||
last_seen_at = now()
|
||||
WHERE analytics_content_sessions.read_recorded = false
|
||||
RETURNING content_session_hash
|
||||
`
|
||||
|
||||
return rows.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 문자열이 통계 조회 형식인지 확인한다.
|
||||
* @param {string} value - 날짜 문자열
|
||||
@@ -789,7 +967,7 @@ const incrementPageScrollBuckets = async (sql, day, pageId, columns) => {
|
||||
|
||||
/**
|
||||
* heartbeat·체류·스크롤·실시간 세션을 기록한다.
|
||||
* @param {{ event: import('h3').H3Event, sessionHash: string, path: string, postId?: string | null, postSlug?: string, pageId?: string | null, pageSlug?: string, durationSeconds: number, maxScrollRatio: number }} input - 기록 입력
|
||||
* @param {{ event: import('h3').H3Event, sessionHash: string, contentSessionHash?: string, path: string, postId?: string | null, postSlug?: string, pageId?: string | null, pageSlug?: string, durationSeconds: number, maxScrollRatio: number }} input - 기록 입력
|
||||
* @returns {Promise<{ ok: true }>}
|
||||
*/
|
||||
export const recordAnalyticsHeartbeat = async (input) => {
|
||||
@@ -801,6 +979,7 @@ export const recordAnalyticsHeartbeat = async (input) => {
|
||||
|
||||
const day = getAnalyticsDayKey()
|
||||
const sessionHash = input.sessionHash
|
||||
const contentSessionHash = input.contentSessionHash || ''
|
||||
const path = input.path
|
||||
const postId = input.postId || null
|
||||
const postSlug = input.postSlug || ''
|
||||
@@ -808,25 +987,88 @@ export const recordAnalyticsHeartbeat = async (input) => {
|
||||
const pageSlug = input.pageSlug || ''
|
||||
const durationSeconds = clampAnalyticsDurationSeconds(input.durationSeconds)
|
||||
const maxScrollRatio = clampAnalyticsScrollRatio(input.maxScrollRatio)
|
||||
const visitorHash = createVisitorHashFromEvent(input.event)
|
||||
const memberSession = getMemberSession(input.event)
|
||||
const userId = memberSession?.userId || null
|
||||
const ipAddress = getClientIp(input.event)
|
||||
|
||||
const previousRows = await sql`
|
||||
SELECT duration_seconds, max_scroll_ratio
|
||||
FROM analytics_active_sessions
|
||||
WHERE session_hash = ${sessionHash}
|
||||
LIMIT 1
|
||||
`
|
||||
const previousRows = contentSessionHash
|
||||
? await sql`
|
||||
SELECT duration_seconds, max_scroll_ratio, engaged_recorded
|
||||
FROM analytics_content_sessions
|
||||
WHERE content_session_hash = ${contentSessionHash}
|
||||
LIMIT 1
|
||||
`
|
||||
: await sql`
|
||||
SELECT duration_seconds, max_scroll_ratio, false AS engaged_recorded
|
||||
FROM analytics_active_sessions
|
||||
WHERE session_hash = ${sessionHash}
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
const previousDuration = Number(previousRows[0]?.duration_seconds || 0)
|
||||
const previousScrollRatio = Number(previousRows[0]?.max_scroll_ratio || 0)
|
||||
const wasEngaged = Boolean(previousRows[0]?.engaged_recorded)
|
||||
const durationDelta = Math.max(0, durationSeconds - previousDuration)
|
||||
const scrollBuckets = getNewScrollBucketColumns(previousScrollRatio, maxScrollRatio)
|
||||
|
||||
if (contentSessionHash) {
|
||||
await sql`
|
||||
INSERT INTO analytics_content_sessions (
|
||||
content_session_hash,
|
||||
day,
|
||||
visitor_hash,
|
||||
session_hash,
|
||||
path,
|
||||
post_id,
|
||||
page_id,
|
||||
duration_seconds,
|
||||
max_scroll_ratio,
|
||||
engaged_recorded,
|
||||
scroll_25_recorded,
|
||||
scroll_50_recorded,
|
||||
scroll_75_recorded,
|
||||
scroll_100_recorded
|
||||
)
|
||||
VALUES (
|
||||
${contentSessionHash},
|
||||
${day},
|
||||
${visitorHash},
|
||||
${sessionHash},
|
||||
${path},
|
||||
${postId},
|
||||
${pageId},
|
||||
${durationSeconds},
|
||||
${maxScrollRatio},
|
||||
${durationSeconds >= ANALYTICS_ENGAGED_MIN_SECONDS},
|
||||
${maxScrollRatio >= 0.25},
|
||||
${maxScrollRatio >= 0.5},
|
||||
${maxScrollRatio >= 0.75},
|
||||
${maxScrollRatio >= 1}
|
||||
)
|
||||
ON CONFLICT (content_session_hash) DO UPDATE
|
||||
SET
|
||||
day = EXCLUDED.day,
|
||||
visitor_hash = EXCLUDED.visitor_hash,
|
||||
session_hash = EXCLUDED.session_hash,
|
||||
path = EXCLUDED.path,
|
||||
post_id = EXCLUDED.post_id,
|
||||
page_id = EXCLUDED.page_id,
|
||||
duration_seconds = GREATEST(analytics_content_sessions.duration_seconds, EXCLUDED.duration_seconds),
|
||||
max_scroll_ratio = GREATEST(analytics_content_sessions.max_scroll_ratio, EXCLUDED.max_scroll_ratio),
|
||||
engaged_recorded = analytics_content_sessions.engaged_recorded OR EXCLUDED.engaged_recorded,
|
||||
scroll_25_recorded = analytics_content_sessions.scroll_25_recorded OR EXCLUDED.scroll_25_recorded,
|
||||
scroll_50_recorded = analytics_content_sessions.scroll_50_recorded OR EXCLUDED.scroll_50_recorded,
|
||||
scroll_75_recorded = analytics_content_sessions.scroll_75_recorded OR EXCLUDED.scroll_75_recorded,
|
||||
scroll_100_recorded = analytics_content_sessions.scroll_100_recorded OR EXCLUDED.scroll_100_recorded,
|
||||
last_seen_at = now()
|
||||
`
|
||||
}
|
||||
|
||||
await sql`
|
||||
INSERT INTO analytics_active_sessions (
|
||||
session_hash,
|
||||
content_session_hash,
|
||||
user_id,
|
||||
path,
|
||||
post_id,
|
||||
@@ -839,6 +1081,7 @@ export const recordAnalyticsHeartbeat = async (input) => {
|
||||
)
|
||||
VALUES (
|
||||
${sessionHash},
|
||||
${contentSessionHash},
|
||||
${userId},
|
||||
${path},
|
||||
${postId},
|
||||
@@ -852,14 +1095,23 @@ export const recordAnalyticsHeartbeat = async (input) => {
|
||||
ON CONFLICT (session_hash) DO UPDATE
|
||||
SET
|
||||
user_id = COALESCE(EXCLUDED.user_id, analytics_active_sessions.user_id),
|
||||
content_session_hash = EXCLUDED.content_session_hash,
|
||||
path = EXCLUDED.path,
|
||||
post_id = EXCLUDED.post_id,
|
||||
post_slug = EXCLUDED.post_slug,
|
||||
page_id = EXCLUDED.page_id,
|
||||
page_slug = EXCLUDED.page_slug,
|
||||
ip_address = EXCLUDED.ip_address,
|
||||
duration_seconds = GREATEST(analytics_active_sessions.duration_seconds, EXCLUDED.duration_seconds),
|
||||
max_scroll_ratio = GREATEST(analytics_active_sessions.max_scroll_ratio, EXCLUDED.max_scroll_ratio),
|
||||
duration_seconds = CASE
|
||||
WHEN analytics_active_sessions.content_session_hash = EXCLUDED.content_session_hash
|
||||
THEN GREATEST(analytics_active_sessions.duration_seconds, EXCLUDED.duration_seconds)
|
||||
ELSE EXCLUDED.duration_seconds
|
||||
END,
|
||||
max_scroll_ratio = CASE
|
||||
WHEN analytics_active_sessions.content_session_hash = EXCLUDED.content_session_hash
|
||||
THEN GREATEST(analytics_active_sessions.max_scroll_ratio, EXCLUDED.max_scroll_ratio)
|
||||
ELSE EXCLUDED.max_scroll_ratio
|
||||
END,
|
||||
last_seen_at = now()
|
||||
`
|
||||
|
||||
@@ -872,7 +1124,9 @@ export const recordAnalyticsHeartbeat = async (input) => {
|
||||
WHERE day = ${day}
|
||||
`
|
||||
|
||||
const wasSiteEngaged = previousDuration >= ANALYTICS_ENGAGED_MIN_SECONDS
|
||||
const wasSiteEngaged = contentSessionHash
|
||||
? wasEngaged
|
||||
: previousDuration >= ANALYTICS_ENGAGED_MIN_SECONDS
|
||||
const isSiteEngaged = durationSeconds >= ANALYTICS_ENGAGED_MIN_SECONDS
|
||||
|
||||
if (!wasSiteEngaged && isSiteEngaged) {
|
||||
@@ -895,7 +1149,9 @@ export const recordAnalyticsHeartbeat = async (input) => {
|
||||
AND post_id = ${postId}
|
||||
`
|
||||
|
||||
const wasPostEngaged = previousDuration >= ANALYTICS_ENGAGED_MIN_SECONDS
|
||||
const wasPostEngaged = contentSessionHash
|
||||
? wasEngaged
|
||||
: previousDuration >= ANALYTICS_ENGAGED_MIN_SECONDS
|
||||
const isPostEngaged = durationSeconds >= ANALYTICS_ENGAGED_MIN_SECONDS
|
||||
|
||||
if (!wasPostEngaged && isPostEngaged) {
|
||||
@@ -922,7 +1178,9 @@ export const recordAnalyticsHeartbeat = async (input) => {
|
||||
AND page_id = ${pageId}
|
||||
`
|
||||
|
||||
const wasPageEngaged = previousDuration >= ANALYTICS_ENGAGED_MIN_SECONDS
|
||||
const wasPageEngaged = contentSessionHash
|
||||
? wasEngaged
|
||||
: previousDuration >= ANALYTICS_ENGAGED_MIN_SECONDS
|
||||
const isPageEngaged = durationSeconds >= ANALYTICS_ENGAGED_MIN_SECONDS
|
||||
|
||||
if (!wasPageEngaged && isPageEngaged) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '../../lib/analytics.js'
|
||||
import { getPageBySlug, getPostBySlug } from '../repositories/content-repository.js'
|
||||
import {
|
||||
createContentSessionHashFromEvent,
|
||||
createSessionHashFromEvent,
|
||||
recordAnalyticsHeartbeat
|
||||
} from '../repositories/analytics-repository.js'
|
||||
@@ -18,6 +19,7 @@ const heartbeatInputSchema = z.object({
|
||||
postSlug: z.string().trim().max(200).optional().default(''),
|
||||
pageSlug: z.string().trim().max(200).optional().default(''),
|
||||
clientSessionId: z.string().trim().min(8).max(120),
|
||||
contentSessionId: z.string().trim().max(120).optional().default(''),
|
||||
durationSeconds: z.number().int().min(0).max(1800),
|
||||
maxScrollRatio: z.number().min(0).max(1)
|
||||
})
|
||||
@@ -78,10 +80,14 @@ export const handleAnalyticsHeartbeat = async (event) => {
|
||||
}
|
||||
|
||||
const sessionHash = createSessionHashFromEvent(event, body.clientSessionId)
|
||||
const contentSessionHash = body.contentSessionId
|
||||
? createContentSessionHashFromEvent(event, body.contentSessionId)
|
||||
: ''
|
||||
|
||||
await recordAnalyticsHeartbeat({
|
||||
event,
|
||||
sessionHash,
|
||||
contentSessionHash,
|
||||
path: body.path,
|
||||
postId,
|
||||
postSlug,
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
} from '../../lib/analytics-traffic.js'
|
||||
import { getPageBySlug, getPostBySlug } from '../repositories/content-repository.js'
|
||||
import {
|
||||
createContentSessionHashFromEvent,
|
||||
createSessionHashFromEvent,
|
||||
createVisitorHashFromEvent,
|
||||
recordAnalyticsPageview
|
||||
} from '../repositories/analytics-repository.js'
|
||||
@@ -21,6 +23,8 @@ const pageviewInputSchema = 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().max(120).optional().default(''),
|
||||
contentSessionId: z.string().trim().max(120).optional().default(''),
|
||||
referrer: z.string().trim().max(1000).optional().default(''),
|
||||
currentUrl: z.string().trim().max(1000).optional().default(''),
|
||||
read: z.boolean().optional().default(false)
|
||||
@@ -82,6 +86,12 @@ export const handleAnalyticsPageview = async (event) => {
|
||||
}
|
||||
|
||||
const visitorHash = createVisitorHashFromEvent(event)
|
||||
const sessionHash = body.clientSessionId
|
||||
? createSessionHashFromEvent(event, body.clientSessionId)
|
||||
: ''
|
||||
const contentSessionHash = body.contentSessionId
|
||||
? createContentSessionHashFromEvent(event, body.contentSessionId)
|
||||
: ''
|
||||
const isReadEvent = Boolean(body.read)
|
||||
const traffic = {
|
||||
...classifyAnalyticsTrafficSource({
|
||||
@@ -93,6 +103,9 @@ export const handleAnalyticsPageview = async (event) => {
|
||||
|
||||
await recordAnalyticsPageview({
|
||||
visitorHash,
|
||||
sessionHash,
|
||||
contentSessionHash,
|
||||
path: body.path,
|
||||
postId,
|
||||
pageId,
|
||||
traffic,
|
||||
|
||||
Reference in New Issue
Block a user