관리자 대시보드 통계 표시 정규화
This commit is contained in:
@@ -53,6 +53,27 @@ const getAnalyticsCollectedSinceDay = () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 화면에 표시할 콘텐츠 참여 지표를 조회수 기준으로 정규화한다.
|
||||
* @param {{ views?: number, reads?: number, scroll50?: number, scroll75?: number, scroll100?: number }} values - 원시 지표
|
||||
* @returns {{ views: number, reads: number, scroll50: number, scroll75: number, scroll100: number }} 정규화 지표
|
||||
*/
|
||||
const normalizeContentEngagementStats = (values = {}) => {
|
||||
const views = Math.max(Number(values.views || 0), 0)
|
||||
const reads = Math.min(Math.max(Number(values.reads || 0), 0), views)
|
||||
const scroll50 = Math.min(Math.max(Number(values.scroll50 || 0), 0), views)
|
||||
const scroll75 = Math.min(Math.max(Number(values.scroll75 || 0), 0), scroll50)
|
||||
const scroll100 = Math.min(Math.max(Number(values.scroll100 || 0), 0), scroll75)
|
||||
|
||||
return {
|
||||
views,
|
||||
reads,
|
||||
scroll50,
|
||||
scroll75,
|
||||
scroll100
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 일별 사이트 통계 행을 보장한다.
|
||||
* @param {import('postgres').Sql} sql - DB 클라이언트
|
||||
@@ -999,15 +1020,20 @@ export const getAnalyticsSummary = async (options = {}) => {
|
||||
`
|
||||
|
||||
const scrollRows = await sql`
|
||||
SELECT COALESCE(SUM(scroll_50), 0)::int AS scroll_50
|
||||
SELECT
|
||||
COALESCE(SUM(views), 0)::int AS views,
|
||||
COALESCE(SUM(scroll_50), 0)::int AS scroll_50
|
||||
FROM post_analytics_daily
|
||||
WHERE day >= ${rangeStartDay}::date
|
||||
`
|
||||
|
||||
const onlineRows = await sql`
|
||||
SELECT
|
||||
COUNT(*)::int AS online_now,
|
||||
COUNT(*) FILTER (WHERE user_id IS NOT NULL)::int AS logged_in_now
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN user_id IS NOT NULL THEN 'user:' || user_id::text
|
||||
ELSE 'ip:' || COALESCE(NULLIF(ip_address, ''), session_hash)
|
||||
END)::int AS online_now,
|
||||
COUNT(DISTINCT user_id) FILTER (WHERE user_id IS NOT NULL)::int AS logged_in_now
|
||||
FROM analytics_active_sessions
|
||||
WHERE last_seen_at >= now() - (${ANALYTICS_ACTIVE_SESSION_TTL_SECONDS} * interval '1 second')
|
||||
`
|
||||
@@ -1028,6 +1054,7 @@ export const getAnalyticsSummary = async (options = {}) => {
|
||||
post_daily AS (
|
||||
SELECT
|
||||
day,
|
||||
COALESCE(SUM(views), 0)::int AS views,
|
||||
COALESCE(SUM(scroll_50), 0)::int AS scroll_50
|
||||
FROM post_analytics_daily
|
||||
WHERE day BETWEEN ${trendStartDay}::date AND ${today}::date
|
||||
@@ -1042,6 +1069,7 @@ export const getAnalyticsSummary = async (options = {}) => {
|
||||
THEN ROUND(site_analytics_daily.total_engaged_seconds::numeric / site_analytics_daily.engaged_views)::int
|
||||
ELSE 0
|
||||
END AS avg_engaged_seconds,
|
||||
COALESCE(post_daily.views, 0)::int AS post_views,
|
||||
COALESCE(post_daily.scroll_50, 0)::int AS scroll_50
|
||||
FROM buckets
|
||||
LEFT JOIN site_analytics_daily ON site_analytics_daily.day = buckets.start_day
|
||||
@@ -1098,6 +1126,12 @@ export const getAnalyticsSummary = async (options = {}) => {
|
||||
))::int
|
||||
ELSE 0
|
||||
END AS avg_engaged_seconds,
|
||||
COALESCE((
|
||||
SELECT SUM(views)
|
||||
FROM post_analytics_daily
|
||||
WHERE day BETWEEN buckets.start_day AND buckets.end_day
|
||||
AND day >= ${collectedSince}::date
|
||||
), 0)::int AS post_views,
|
||||
COALESCE((
|
||||
SELECT SUM(scroll_50)
|
||||
FROM post_analytics_daily
|
||||
@@ -1144,6 +1178,12 @@ export const getAnalyticsSummary = async (options = {}) => {
|
||||
))::int
|
||||
ELSE 0
|
||||
END AS avg_engaged_seconds,
|
||||
COALESCE((
|
||||
SELECT SUM(views)
|
||||
FROM post_analytics_daily
|
||||
WHERE day BETWEEN buckets.start_day AND LEAST((buckets.start_day + interval '1 month' - interval '1 day')::date, ${today}::date)
|
||||
AND day >= ${collectedSince}::date
|
||||
), 0)::int AS post_views,
|
||||
COALESCE((
|
||||
SELECT SUM(scroll_50)
|
||||
FROM post_analytics_daily
|
||||
@@ -1193,6 +1233,12 @@ export const getAnalyticsSummary = async (options = {}) => {
|
||||
))::int
|
||||
ELSE 0
|
||||
END AS avg_engaged_seconds,
|
||||
COALESCE((
|
||||
SELECT SUM(views)
|
||||
FROM post_analytics_daily
|
||||
WHERE day BETWEEN buckets.start_day AND LEAST((buckets.start_day + interval '1 year' - interval '1 day')::date, ${today}::date)
|
||||
AND day >= ${collectedSince}::date
|
||||
), 0)::int AS post_views,
|
||||
COALESCE((
|
||||
SELECT SUM(scroll_50)
|
||||
FROM post_analytics_daily
|
||||
@@ -1268,7 +1314,10 @@ export const getAnalyticsSummary = async (options = {}) => {
|
||||
avgEngagedSeconds: engagedViews > 0
|
||||
? Math.round(totalEngagedSeconds / engagedViews)
|
||||
: 0,
|
||||
scroll50Reach: Number(scrollRows[0]?.scroll_50 || 0),
|
||||
scroll50Reach: Math.min(
|
||||
Number(scrollRows[0]?.scroll_50 || 0),
|
||||
Number(scrollRows[0]?.views || 0)
|
||||
),
|
||||
collectedSince,
|
||||
trends: trendRows.map((row) => {
|
||||
const startDay = row.start_day ? new Date(row.start_day).toISOString().slice(0, 10) : ''
|
||||
@@ -1277,25 +1326,30 @@ export const getAnalyticsSummary = async (options = {}) => {
|
||||
? aggregateTrafficBreakdownForRange(startDay, endDay)
|
||||
: { trafficSearch: 0, trafficSns: 0, trafficOther: 0 }
|
||||
|
||||
const scroll50Reach = Math.min(
|
||||
Number(row.scroll_50 || 0),
|
||||
Number(row.post_views || 0)
|
||||
)
|
||||
|
||||
return {
|
||||
day: startDay,
|
||||
startDay,
|
||||
endDay,
|
||||
label: startDay && endDay
|
||||
? formatAnalyticsTrendLabel(
|
||||
period,
|
||||
startDay,
|
||||
endDay,
|
||||
today
|
||||
)
|
||||
: '',
|
||||
visitors: Number(row.visitors || 0),
|
||||
trafficSearch: trafficBreakdown.trafficSearch,
|
||||
trafficSns: trafficBreakdown.trafficSns,
|
||||
trafficOther: trafficBreakdown.trafficOther,
|
||||
avgEngagedSeconds: Number(row.avg_engaged_seconds || 0),
|
||||
scroll50Reach: Number(row.scroll_50 || 0)
|
||||
}
|
||||
day: startDay,
|
||||
startDay,
|
||||
endDay,
|
||||
label: startDay && endDay
|
||||
? formatAnalyticsTrendLabel(
|
||||
period,
|
||||
startDay,
|
||||
endDay,
|
||||
today
|
||||
)
|
||||
: '',
|
||||
visitors: Number(row.visitors || 0),
|
||||
trafficSearch: trafficBreakdown.trafficSearch,
|
||||
trafficSns: trafficBreakdown.trafficSns,
|
||||
trafficOther: trafficBreakdown.trafficOther,
|
||||
avgEngagedSeconds: Number(row.avg_engaged_seconds || 0),
|
||||
scroll50Reach
|
||||
}
|
||||
}),
|
||||
period,
|
||||
days
|
||||
@@ -1322,8 +1376,12 @@ export const getAnalyticsRealtimeSummary = async () => {
|
||||
|
||||
const rows = await sql`
|
||||
SELECT
|
||||
COUNT(*)::int AS online_now,
|
||||
COUNT(*) FILTER (WHERE user_id IS NOT NULL)::int AS logged_in_now
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN user_id IS NOT NULL THEN 'user:' || user_id::text
|
||||
ELSE 'ip:' || COALESCE(NULLIF(ip_address, ''), session_hash)
|
||||
END)::int AS online_now,
|
||||
COUNT(DISTINCT user_id) FILTER (WHERE user_id IS NOT NULL)::int AS logged_in_now,
|
||||
COUNT(DISTINCT COALESCE(NULLIF(ip_address, ''), session_hash)) FILTER (WHERE user_id IS NULL)::int AS anonymous_now
|
||||
FROM analytics_active_sessions
|
||||
WHERE last_seen_at >= now() - (${ANALYTICS_ACTIVE_SESSION_TTL_SECONDS} * interval '1 second')
|
||||
`
|
||||
@@ -1334,7 +1392,7 @@ export const getAnalyticsRealtimeSummary = async () => {
|
||||
return {
|
||||
onlineNow,
|
||||
loggedInNow,
|
||||
anonymousNow: Math.max(onlineNow - loggedInNow, 0)
|
||||
anonymousNow: Number(rows[0]?.anonymous_now || 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1346,6 +1404,7 @@ export const getAnalyticsRealtimeSummary = async () => {
|
||||
export const getAnalyticsActiveSessions = async (options = {}) => {
|
||||
const sql = getPostgresClient()
|
||||
const limit = Math.min(Math.max(Number(options.limit) || 20, 1), 50)
|
||||
const rawLimit = Math.min(limit * 8, 200)
|
||||
|
||||
if (!sql) {
|
||||
return []
|
||||
@@ -1375,29 +1434,53 @@ export const getAnalyticsActiveSessions = async (options = {}) => {
|
||||
LEFT JOIN users ON users.id = analytics_active_sessions.user_id
|
||||
WHERE analytics_active_sessions.last_seen_at >= now() - (${ANALYTICS_ACTIVE_SESSION_TTL_SECONDS} * interval '1 second')
|
||||
ORDER BY analytics_active_sessions.last_seen_at DESC
|
||||
LIMIT ${limit}
|
||||
LIMIT ${rawLimit}
|
||||
`
|
||||
|
||||
return rows.map((row) => ({
|
||||
sessionHash: row.session_hash,
|
||||
path: row.path,
|
||||
postSlug: row.post_slug || '',
|
||||
pageSlug: row.page_slug || '',
|
||||
ipAddress: row.ip_address || '',
|
||||
postTitle: row.post_title || '',
|
||||
pageTitle: row.page_title || '',
|
||||
durationSeconds: Number(row.duration_seconds || 0),
|
||||
maxScrollRatio: Number(row.max_scroll_ratio || 0),
|
||||
lastSeenAt: row.last_seen_at ? new Date(row.last_seen_at).toISOString() : null,
|
||||
isLoggedIn: Boolean(row.user_id),
|
||||
user: row.user_id
|
||||
? {
|
||||
id: row.user_id,
|
||||
username: row.username,
|
||||
avatarUrl: row.avatar_url || ''
|
||||
}
|
||||
: null
|
||||
}))
|
||||
/** @type {Map<string, Object>} */
|
||||
const activeVisitors = new Map()
|
||||
|
||||
for (const row of rows) {
|
||||
const activeKey = row.user_id
|
||||
? `user:${row.user_id}`
|
||||
: `ip:${row.ip_address || row.session_hash}`
|
||||
const existingVisitor = activeVisitors.get(activeKey)
|
||||
const durationSeconds = Number(row.duration_seconds || 0)
|
||||
|
||||
if (existingVisitor) {
|
||||
existingVisitor.pageCount += 1
|
||||
existingVisitor.durationSeconds = Math.max(existingVisitor.durationSeconds, durationSeconds)
|
||||
existingVisitor.maxScrollRatio = Math.max(
|
||||
existingVisitor.maxScrollRatio,
|
||||
Number(row.max_scroll_ratio || 0)
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
activeVisitors.set(activeKey, {
|
||||
activeKey,
|
||||
path: row.path,
|
||||
postSlug: row.post_slug || '',
|
||||
pageSlug: row.page_slug || '',
|
||||
ipAddress: row.ip_address || '',
|
||||
postTitle: row.post_title || '',
|
||||
pageTitle: row.page_title || '',
|
||||
pageCount: 1,
|
||||
durationSeconds,
|
||||
maxScrollRatio: Number(row.max_scroll_ratio || 0),
|
||||
lastSeenAt: row.last_seen_at ? new Date(row.last_seen_at).toISOString() : null,
|
||||
isLoggedIn: Boolean(row.user_id),
|
||||
user: row.user_id
|
||||
? {
|
||||
id: row.user_id,
|
||||
username: row.username,
|
||||
avatarUrl: row.avatar_url || ''
|
||||
}
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
return Array.from(activeVisitors.values()).slice(0, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1531,7 +1614,9 @@ export const getAnalyticsRangeDetail = async (options = {}) => {
|
||||
`
|
||||
|
||||
const scrollRows = await sql`
|
||||
SELECT COALESCE(SUM(post_analytics_daily.scroll_50), 0)::int AS scroll_50
|
||||
SELECT
|
||||
COALESCE(SUM(post_analytics_daily.views), 0)::int AS views,
|
||||
COALESCE(SUM(post_analytics_daily.scroll_50), 0)::int AS scroll_50
|
||||
FROM post_analytics_daily
|
||||
WHERE post_analytics_daily.day BETWEEN ${startDay}::date AND ${endDay}::date
|
||||
`
|
||||
@@ -1634,7 +1719,10 @@ export const getAnalyticsRangeDetail = async (options = {}) => {
|
||||
avgEngagedSeconds: engagedViews > 0
|
||||
? Math.round(totalEngagedSeconds / engagedViews)
|
||||
: 0,
|
||||
scroll50Reach: Number(scrollRows[0]?.scroll_50 || 0)
|
||||
scroll50Reach: Math.min(
|
||||
Number(scrollRows[0]?.scroll_50 || 0),
|
||||
Number(scrollRows[0]?.views || 0)
|
||||
)
|
||||
},
|
||||
sources: sourceRows.map((row) => ({
|
||||
sourceGroup: row.source_group,
|
||||
@@ -1654,31 +1742,50 @@ export const getAnalyticsRangeDetail = async (options = {}) => {
|
||||
pageViews: Number(row.page_views || 0),
|
||||
visitors: Number(row.visitors || 0)
|
||||
})),
|
||||
topPosts: postRows.map((row) => ({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null,
|
||||
views: Number(row.views || 0),
|
||||
monthlyViews: Number(row.monthly_views || 0),
|
||||
reads: Number(row.reads || 0),
|
||||
visitors: Number(row.visitors || 0),
|
||||
scroll50: Number(row.scroll_50 || 0),
|
||||
scroll75: Number(row.scroll_75 || 0),
|
||||
scroll100: Number(row.scroll_100 || 0)
|
||||
})),
|
||||
topPages: pageRows.map((row) => ({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null,
|
||||
views: Number(row.views || 0),
|
||||
monthlyViews: Number(row.monthly_views || 0),
|
||||
visitors: Number(row.visitors || 0),
|
||||
scroll50: Number(row.scroll_50 || 0),
|
||||
scroll75: Number(row.scroll_75 || 0),
|
||||
scroll100: Number(row.scroll_100 || 0)
|
||||
}))
|
||||
topPosts: postRows.map((row) => {
|
||||
const engagementStats = normalizeContentEngagementStats({
|
||||
views: row.views,
|
||||
reads: row.reads,
|
||||
scroll50: row.scroll_50,
|
||||
scroll75: row.scroll_75,
|
||||
scroll100: row.scroll_100
|
||||
})
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null,
|
||||
views: engagementStats.views,
|
||||
monthlyViews: Number(row.monthly_views || 0),
|
||||
reads: engagementStats.reads,
|
||||
visitors: Number(row.visitors || 0),
|
||||
scroll50: engagementStats.scroll50,
|
||||
scroll75: engagementStats.scroll75,
|
||||
scroll100: engagementStats.scroll100
|
||||
}
|
||||
}),
|
||||
topPages: pageRows.map((row) => {
|
||||
const engagementStats = normalizeContentEngagementStats({
|
||||
views: row.views,
|
||||
scroll50: row.scroll_50,
|
||||
scroll75: row.scroll_75,
|
||||
scroll100: row.scroll_100
|
||||
})
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null,
|
||||
views: engagementStats.views,
|
||||
monthlyViews: Number(row.monthly_views || 0),
|
||||
visitors: Number(row.visitors || 0),
|
||||
scroll50: engagementStats.scroll50,
|
||||
scroll75: engagementStats.scroll75,
|
||||
scroll100: engagementStats.scroll100
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1887,17 +1994,24 @@ export const getPostAnalyticsDetail = async (options = {}) => {
|
||||
const mapSummaryRow = (row) => {
|
||||
const engagedViews = Number(row?.engaged_views || 0)
|
||||
const totalEngagedSeconds = Number(row?.total_engaged_seconds || 0)
|
||||
const engagementStats = normalizeContentEngagementStats({
|
||||
views: row?.views,
|
||||
reads: row?.reads,
|
||||
scroll50: row?.scroll_50,
|
||||
scroll75: row?.scroll_75,
|
||||
scroll100: row?.scroll_100
|
||||
})
|
||||
|
||||
return {
|
||||
views: Number(row?.views || 0),
|
||||
views: engagementStats.views,
|
||||
visitors: Number(row?.visitors || 0),
|
||||
reads: Number(row?.reads || 0),
|
||||
reads: engagementStats.reads,
|
||||
avgEngagedSeconds: engagedViews > 0
|
||||
? Math.round(totalEngagedSeconds / engagedViews)
|
||||
: 0,
|
||||
scroll50: Number(row?.scroll_50 || 0),
|
||||
scroll75: Number(row?.scroll_75 || 0),
|
||||
scroll100: Number(row?.scroll_100 || 0)
|
||||
scroll50: engagementStats.scroll50,
|
||||
scroll75: engagementStats.scroll75,
|
||||
scroll100: engagementStats.scroll100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1908,14 +2022,21 @@ export const getPostAnalyticsDetail = async (options = {}) => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
total: mapSummaryRow(totalRows[0]),
|
||||
period: mapSummaryRow(periodRows[0]),
|
||||
trends: trendRows.map((row) => ({
|
||||
day: row.start_day ? new Date(row.start_day).toISOString().slice(0, 10) : '',
|
||||
startDay: row.start_day ? new Date(row.start_day).toISOString().slice(0, 10) : '',
|
||||
endDay: row.end_day ? new Date(row.end_day).toISOString().slice(0, 10) : '',
|
||||
views: Number(row.views || 0),
|
||||
visitors: Number(row.visitors || 0),
|
||||
reads: Number(row.reads || 0)
|
||||
})),
|
||||
trends: trendRows.map((row) => {
|
||||
const engagementStats = normalizeContentEngagementStats({
|
||||
views: row.views,
|
||||
reads: row.reads
|
||||
})
|
||||
|
||||
return {
|
||||
day: row.start_day ? new Date(row.start_day).toISOString().slice(0, 10) : '',
|
||||
startDay: row.start_day ? new Date(row.start_day).toISOString().slice(0, 10) : '',
|
||||
endDay: row.end_day ? new Date(row.end_day).toISOString().slice(0, 10) : '',
|
||||
views: engagementStats.views,
|
||||
visitors: Number(row.visitors || 0),
|
||||
reads: engagementStats.reads
|
||||
}
|
||||
}),
|
||||
sources: sourceRows.map((row) => ({
|
||||
sourceGroup: row.source_group,
|
||||
sourceName: row.source_name,
|
||||
@@ -2087,16 +2208,22 @@ export const getPageAnalyticsDetail = async (options = {}) => {
|
||||
const mapPageSummaryRow = (row) => {
|
||||
const engagedViews = Number(row?.engaged_views || 0)
|
||||
const totalEngagedSeconds = Number(row?.total_engaged_seconds || 0)
|
||||
const engagementStats = normalizeContentEngagementStats({
|
||||
views: row?.views,
|
||||
scroll50: row?.scroll_50,
|
||||
scroll75: row?.scroll_75,
|
||||
scroll100: row?.scroll_100
|
||||
})
|
||||
|
||||
return {
|
||||
views: Number(row?.views || 0),
|
||||
views: engagementStats.views,
|
||||
visitors: Number(row?.visitors || 0),
|
||||
avgEngagedSeconds: engagedViews > 0
|
||||
? Math.round(totalEngagedSeconds / engagedViews)
|
||||
: 0,
|
||||
scroll50: Number(row?.scroll_50 || 0),
|
||||
scroll75: Number(row?.scroll_75 || 0),
|
||||
scroll100: Number(row?.scroll_100 || 0)
|
||||
scroll50: engagementStats.scroll50,
|
||||
scroll75: engagementStats.scroll75,
|
||||
scroll100: engagementStats.scroll100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2164,22 +2291,29 @@ export const getAnalyticsTopPosts = async (options = {}) => {
|
||||
return rows.map((row) => {
|
||||
const engagedViews = Number(row.engaged_views || 0)
|
||||
const totalEngagedSeconds = Number(row.total_engaged_seconds || 0)
|
||||
const engagementStats = normalizeContentEngagementStats({
|
||||
views: row.views,
|
||||
reads: row.reads,
|
||||
scroll50: row.scroll_50,
|
||||
scroll75: row.scroll_75,
|
||||
scroll100: row.scroll_100
|
||||
})
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null,
|
||||
views: Number(row.views || 0),
|
||||
views: engagementStats.views,
|
||||
monthlyViews: Number(row.monthly_views || 0),
|
||||
reads: Number(row.reads || 0),
|
||||
reads: engagementStats.reads,
|
||||
visitors: Number(row.visitors || 0),
|
||||
avgEngagedSeconds: engagedViews > 0
|
||||
? Math.round(totalEngagedSeconds / engagedViews)
|
||||
: 0,
|
||||
scroll50: Number(row.scroll_50 || 0),
|
||||
scroll75: Number(row.scroll_75 || 0),
|
||||
scroll100: Number(row.scroll_100 || 0)
|
||||
scroll50: engagementStats.scroll50,
|
||||
scroll75: engagementStats.scroll75,
|
||||
scroll100: engagementStats.scroll100
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2225,19 +2359,25 @@ export const getAnalyticsTopPages = async (options = {}) => {
|
||||
return rows.map((row) => {
|
||||
const engagedViews = Number(row.engaged_views || 0)
|
||||
const totalEngagedSeconds = Number(row.total_engaged_seconds || 0)
|
||||
const engagementStats = normalizeContentEngagementStats({
|
||||
views: row.views,
|
||||
scroll50: row.scroll_50,
|
||||
scroll75: row.scroll_75,
|
||||
scroll100: row.scroll_100
|
||||
})
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
views: Number(row.views || 0),
|
||||
views: engagementStats.views,
|
||||
visitors: Number(row.visitors || 0),
|
||||
avgEngagedSeconds: engagedViews > 0
|
||||
? Math.round(totalEngagedSeconds / engagedViews)
|
||||
: 0,
|
||||
scroll50: Number(row.scroll_50 || 0),
|
||||
scroll75: Number(row.scroll_75 || 0),
|
||||
scroll100: Number(row.scroll_100 || 0)
|
||||
scroll50: engagementStats.scroll50,
|
||||
scroll75: engagementStats.scroll75,
|
||||
scroll100: engagementStats.scroll100
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user