관리자 통계 대시보드 개선 및 v1.9.6 반영
통계 추이 차트 공통화, 유입 스택 막대, 집계 시작일 고정, traffic API 오류 수정, deploy 문서 정리를 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
523
lib/analytics-chart-ui.js
Normal file
523
lib/analytics-chart-ui.js
Normal file
@@ -0,0 +1,523 @@
|
||||
import { getAnalyticsDayKey } from './analytics-shared.js'
|
||||
|
||||
/** @type {Record<string, number>} 집계 단위별 차트 페이지 크기 */
|
||||
export const ANALYTICS_CHART_PAGE_SIZES = {
|
||||
daily: 7,
|
||||
weekly: 8,
|
||||
monthly: 12,
|
||||
yearly: 8
|
||||
}
|
||||
|
||||
/** @type {Record<string, number>} 집계 단위별 차트 전체 버킷 수(페이지 이동용) */
|
||||
export const ANALYTICS_CHART_TOTAL_BUCKETS = {
|
||||
daily: 28,
|
||||
weekly: 24,
|
||||
monthly: 24,
|
||||
yearly: 16
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 단위별 차트 페이지 크기를 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @returns {number} 페이지 크기
|
||||
*/
|
||||
export const getAnalyticsChartPageSize = (period) => {
|
||||
return ANALYTICS_CHART_PAGE_SIZES[period] || 8
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 단위별 차트 전체 버킷 수를 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @returns {number} 전체 버킷 수
|
||||
*/
|
||||
export const getAnalyticsChartTotalBuckets = (period) => {
|
||||
return ANALYTICS_CHART_TOTAL_BUCKETS[period] || 16
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 추이 행의 종료일을 반환한다.
|
||||
* @param {Object} row - 추이 행
|
||||
* @returns {string} 종료일
|
||||
*/
|
||||
const getAnalyticsTrendEndDay = (row) => {
|
||||
return String(row?.endDay || row?.day || '').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 추이 행의 시작일을 반환한다.
|
||||
* @param {Object} row - 추이 행
|
||||
* @returns {string} 시작일
|
||||
*/
|
||||
const getAnalyticsTrendStartDay = (row) => {
|
||||
return String(row?.startDay || row?.day || '').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 키를 Date 객체로 변환한다.
|
||||
* @param {string} dayKey - YYYY-MM-DD
|
||||
* @returns {Date | null} Date 객체
|
||||
*/
|
||||
const toAnalyticsDate = (dayKey) => {
|
||||
const value = String(dayKey || '').trim()
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const date = new Date(`${value}T00:00:00.000Z`)
|
||||
return Number.isNaN(date.getTime()) ? null : date
|
||||
}
|
||||
|
||||
/**
|
||||
* Date 객체를 통계 날짜 키로 변환한다.
|
||||
* @param {Date} date - Date 객체
|
||||
* @returns {string} YYYY-MM-DD
|
||||
*/
|
||||
const formatAnalyticsDateKey = (date) => {
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 날짜 키 중 더 늦은 날짜를 반환한다.
|
||||
* @param {string} leftDay - YYYY-MM-DD
|
||||
* @param {string} rightDay - YYYY-MM-DD
|
||||
* @returns {string} 더 늦은 날짜 키
|
||||
*/
|
||||
const getLaterAnalyticsDayKey = (leftDay, rightDay) => {
|
||||
const left = String(leftDay || '').trim()
|
||||
const right = String(rightDay || '').trim()
|
||||
if (!left) {
|
||||
return right
|
||||
}
|
||||
if (!right) {
|
||||
return left
|
||||
}
|
||||
return left > right ? left : right
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 단위에 맞춘 표시용 시작일을 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @param {string} collectedSince - 최초 집계일(YYYY-MM-DD)
|
||||
* @returns {string} 정렬된 시작일
|
||||
*/
|
||||
export const getAnalyticsPeriodAlignedStartDay = (period, collectedSince) => {
|
||||
const date = toAnalyticsDate(collectedSince)
|
||||
if (!date) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (period === 'monthly') {
|
||||
return formatAnalyticsDateKey(new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)))
|
||||
}
|
||||
|
||||
if (period === 'yearly') {
|
||||
return formatAnalyticsDateKey(new Date(Date.UTC(date.getUTCFullYear(), 0, 1)))
|
||||
}
|
||||
|
||||
if (period === 'weekly') {
|
||||
const weekStart = new Date(date)
|
||||
weekStart.setUTCDate(date.getUTCDate() - date.getUTCDay())
|
||||
return formatAnalyticsDateKey(weekStart)
|
||||
}
|
||||
|
||||
return collectedSince
|
||||
}
|
||||
|
||||
/**
|
||||
* 추이 조회 시작일과 집계 시작일 중 유효한 시작일을 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @param {string} requestedStartDay - 요청 시작일
|
||||
* @param {string} collectedSince - 최초 집계일
|
||||
* @returns {string} 유효 시작일
|
||||
*/
|
||||
export const getAnalyticsEffectiveTrendStartDay = (period, requestedStartDay, collectedSince) => {
|
||||
const alignedStartDay = getAnalyticsPeriodAlignedStartDay(period, collectedSince)
|
||||
if (!alignedStartDay) {
|
||||
return requestedStartDay
|
||||
}
|
||||
|
||||
return getLaterAnalyticsDayKey(requestedStartDay, alignedStartDay)
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 시작일을 한국어 안내 문구로 변환한다.
|
||||
* @param {string} dayKey - YYYY-MM-DD
|
||||
* @returns {string} 안내 문구용 날짜
|
||||
*/
|
||||
export const formatAnalyticsCollectedSinceLabel = (dayKey) => {
|
||||
const date = toAnalyticsDate(dayKey)
|
||||
if (!date) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `${date.getUTCFullYear()}년 ${date.getUTCMonth() + 1}월 ${date.getUTCDate()}일`
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 추이 안내 문구를 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @param {string} collectedSince - 최초 집계일
|
||||
* @returns {string} 안내 문구
|
||||
*/
|
||||
export const getAnalyticsTrendDescription = (period, collectedSince) => {
|
||||
if (!collectedSince) {
|
||||
return '아직 집계된 통계 데이터가 없습니다.'
|
||||
}
|
||||
|
||||
const label = formatAnalyticsCollectedSinceLabel(collectedSince)
|
||||
if (!label) {
|
||||
return '아직 집계된 통계 데이터가 없습니다.'
|
||||
}
|
||||
|
||||
return `통계는 ${label}부터 집계한 데이터를 표시합니다.`
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 진행 중인 집계 구간인지 확인한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @param {Object} row - 추이 행
|
||||
* @param {string} [todayKey] - 오늘 날짜 키
|
||||
* @returns {boolean} 현재 구간 여부
|
||||
*/
|
||||
export const isAnalyticsCurrentTrendRow = (period, row, todayKey = getAnalyticsDayKey()) => {
|
||||
const startDay = getAnalyticsTrendStartDay(row)
|
||||
const endDay = getAnalyticsTrendEndDay(row)
|
||||
|
||||
if (!startDay || !endDay || endDay !== todayKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (period === 'daily') {
|
||||
return startDay === todayKey
|
||||
}
|
||||
|
||||
return period === 'weekly' || period === 'monthly' || period === 'yearly'
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 구간의 상대 라벨을 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @returns {string} 상대 라벨
|
||||
*/
|
||||
export const getAnalyticsCurrentPeriodLabel = (period) => {
|
||||
if (period === 'daily') {
|
||||
return '오늘'
|
||||
}
|
||||
|
||||
if (period === 'weekly') {
|
||||
return '이번주'
|
||||
}
|
||||
|
||||
if (period === 'monthly') {
|
||||
return '이번달'
|
||||
}
|
||||
|
||||
if (period === 'yearly') {
|
||||
return '올해'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 추이 툴팁·상세용 라벨을 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @param {Object} row - 추이 행
|
||||
* @param {string} [todayKey] - 오늘 날짜 키
|
||||
* @returns {string} 라벨
|
||||
*/
|
||||
export const getAnalyticsTrendDetailLabel = (period, row, todayKey = getAnalyticsDayKey()) => {
|
||||
if (!row) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (row.label) {
|
||||
return row.label
|
||||
}
|
||||
|
||||
if (isAnalyticsCurrentTrendRow(period, row, todayKey)) {
|
||||
return getAnalyticsCurrentPeriodLabel(period)
|
||||
}
|
||||
|
||||
const startDay = getAnalyticsTrendStartDay(row)
|
||||
const endDay = getAnalyticsTrendEndDay(row)
|
||||
const startDate = toAnalyticsDate(startDay)
|
||||
const endDate = toAnalyticsDate(endDay)
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (period === 'yearly') {
|
||||
return `${startDate.getUTCFullYear()}년`
|
||||
}
|
||||
|
||||
if (period === 'monthly') {
|
||||
return `${startDate.getUTCFullYear()}.${String(startDate.getUTCMonth() + 1).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
if (period === 'weekly') {
|
||||
const startLabel = `${startDate.getUTCMonth() + 1}.${String(startDate.getUTCDate()).padStart(2, '0')}`
|
||||
const endLabel = `${endDate.getUTCMonth() + 1}.${String(endDate.getUTCDate()).padStart(2, '0')}`
|
||||
return startLabel === endLabel ? startLabel : `${startLabel}-${endLabel}`
|
||||
}
|
||||
|
||||
return `${String(startDate.getUTCMonth() + 1).padStart(2, '0')}.${String(startDate.getUTCDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 차트 축 기본 라벨을 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @param {Object} row - 추이 행
|
||||
* @param {boolean} isLast - 마지막 막대 여부
|
||||
* @param {string} [todayKey] - 오늘 날짜 키
|
||||
* @returns {string} 축 라벨
|
||||
*/
|
||||
export const getAnalyticsTrendAxisPrimaryLabel = (period, row, isLast, todayKey = getAnalyticsDayKey()) => {
|
||||
if (isLast && isAnalyticsCurrentTrendRow(period, row, todayKey)) {
|
||||
return getAnalyticsCurrentPeriodLabel(period)
|
||||
}
|
||||
|
||||
const startDate = toAnalyticsDate(getAnalyticsTrendStartDay(row))
|
||||
if (!startDate) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (period === 'daily' || period === 'weekly') {
|
||||
return String(startDate.getUTCDate())
|
||||
}
|
||||
|
||||
if (period === 'monthly') {
|
||||
return String(startDate.getUTCMonth() + 1)
|
||||
}
|
||||
|
||||
if (period === 'yearly') {
|
||||
return String(startDate.getUTCFullYear())
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 차트 축 보조 라벨(월·년)을 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @param {Object} row - 추이 행
|
||||
* @param {Array<Object>} rows - 표시 중인 추이 행
|
||||
* @param {number} index - 행 인덱스
|
||||
* @returns {string} 보조 라벨
|
||||
*/
|
||||
export const getAnalyticsTrendAxisSecondaryLabel = (period, row, rows, index) => {
|
||||
const startDate = toAnalyticsDate(getAnalyticsTrendStartDay(row))
|
||||
if (!startDate) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const previousRow = index > 0 ? rows[index - 1] : null
|
||||
const previousDate = previousRow ? toAnalyticsDate(getAnalyticsTrendStartDay(previousRow)) : null
|
||||
|
||||
if (period === 'daily' || period === 'weekly') {
|
||||
if (!previousDate || startDate.getUTCMonth() !== previousDate.getUTCMonth()) {
|
||||
return `${startDate.getUTCMonth() + 1}월`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
if (period === 'monthly' || period === 'yearly') {
|
||||
if (!previousDate || startDate.getUTCFullYear() !== previousDate.getUTCFullYear()) {
|
||||
return `${startDate.getUTCFullYear()}년`
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 이전 구간 대비 증감을 반환한다.
|
||||
* @param {Array<Object>} rows - 전체 추이 행
|
||||
* @param {number} index - 현재 행 인덱스
|
||||
* @param {string} metricKey - 지표 키
|
||||
* @returns {number | null} 증감값
|
||||
*/
|
||||
export const getAnalyticsTrendDelta = (rows, index, metricKey) => {
|
||||
if (!Array.isArray(rows) || index <= 0 || index >= rows.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const current = Number(rows[index]?.[metricKey] || 0)
|
||||
const previous = Number(rows[index - 1]?.[metricKey] || 0)
|
||||
|
||||
return current - previous
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 추이 행을 페이지 단위로 잘라 반환한다.
|
||||
* @param {Array<Object>} rows - 전체 추이 행
|
||||
* @param {string} period - 집계 단위
|
||||
* @param {number} pageOffset - 0이면 최신 구간, 값이 클수록 과거
|
||||
* @returns {{ rows: Array<Object>, canGoPrev: boolean, canGoNext: boolean, pageOffset: number, pageSize: number }}
|
||||
*/
|
||||
export const paginateAnalyticsTrendRows = (rows, period, pageOffset = 0) => {
|
||||
const list = Array.isArray(rows) ? rows : []
|
||||
const pageSize = getAnalyticsChartPageSize(period)
|
||||
const safeOffset = Math.max(Number(pageOffset) || 0, 0)
|
||||
const endIndex = Math.max(list.length - safeOffset, 0)
|
||||
const startIndex = Math.max(endIndex - pageSize, 0)
|
||||
|
||||
return {
|
||||
rows: list.slice(startIndex, endIndex),
|
||||
canGoPrev: startIndex > 0,
|
||||
canGoNext: safeOffset > 0,
|
||||
pageOffset: safeOffset,
|
||||
pageSize
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 차트 헤더에 표시할 기간 라벨을 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @param {Array<Object>} visibleRows - 표시 중인 추이 행
|
||||
* @param {string} [todayKey] - 오늘 날짜 키
|
||||
* @returns {string} 기간 라벨
|
||||
*/
|
||||
export const formatAnalyticsChartHeaderLabel = (period, visibleRows, todayKey = getAnalyticsDayKey()) => {
|
||||
if (!visibleRows?.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const firstRow = visibleRows[0]
|
||||
const lastRow = visibleRows[visibleRows.length - 1]
|
||||
|
||||
if (visibleRows.length === 1 && isAnalyticsCurrentTrendRow(period, lastRow, todayKey) && period === 'daily') {
|
||||
const date = toAnalyticsDate(getAnalyticsTrendStartDay(lastRow))
|
||||
if (!date) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const weekdays = ['일', '월', '화', '수', '목', '금', '토']
|
||||
return `${date.getUTCFullYear()}.${String(date.getUTCMonth() + 1).padStart(2, '0')}.${String(date.getUTCDate()).padStart(2, '0')} ${weekdays[date.getUTCDay()]}`
|
||||
}
|
||||
|
||||
const startDate = toAnalyticsDate(getAnalyticsTrendStartDay(firstRow))
|
||||
const endDate = toAnalyticsDate(getAnalyticsTrendEndDay(lastRow))
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
return `${date.getUTCFullYear()}.${String(date.getUTCMonth() + 1).padStart(2, '0')}.${String(date.getUTCDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
if (period === 'monthly' && visibleRows.length === 1) {
|
||||
return `${startDate.getUTCFullYear()}.${String(startDate.getUTCMonth() + 1).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
if (formatDate(startDate) === formatDate(endDate)) {
|
||||
return formatDate(startDate)
|
||||
}
|
||||
|
||||
return `${formatDate(startDate)} ~ ${formatDate(endDate)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 단위 표시명을 반환한다.
|
||||
* @param {string} period - 집계 단위
|
||||
* @returns {string} 표시명
|
||||
*/
|
||||
export const getAnalyticsPeriodLabel = (period) => {
|
||||
if (period === 'daily') {
|
||||
return '일간'
|
||||
}
|
||||
|
||||
if (period === 'weekly') {
|
||||
return '주간'
|
||||
}
|
||||
|
||||
if (period === 'monthly') {
|
||||
return '월간'
|
||||
}
|
||||
|
||||
if (period === 'yearly') {
|
||||
return '년간'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/** @type {Record<string, { active: string, muted: string }>} 유입 스택 막대 색상 */
|
||||
export const ANALYTICS_TRAFFIC_STACK_COLORS = {
|
||||
search: { active: '#e5484d', muted: '#f3b6b8' },
|
||||
sns: { active: '#12a594', muted: '#a8e8df' },
|
||||
other: { active: '#8b8d98', muted: '#d5d7de' }
|
||||
}
|
||||
|
||||
/** @type {{ active: string, muted: string }} 단일 막대 색상 */
|
||||
export const ANALYTICS_SINGLE_BAR_COLORS = {
|
||||
active: '#15171a',
|
||||
muted: '#d5d7de'
|
||||
}
|
||||
|
||||
/**
|
||||
* 유입 스택 막대 색상을 반환한다.
|
||||
* @param {'search' | 'sns' | 'other'} key - 유입 구분
|
||||
* @param {boolean} emphasized - 강조 여부
|
||||
* @returns {string} 색상
|
||||
*/
|
||||
export const getAnalyticsTrafficStackColor = (key, emphasized) => {
|
||||
const colors = ANALYTICS_TRAFFIC_STACK_COLORS[key] || ANALYTICS_TRAFFIC_STACK_COLORS.other
|
||||
return emphasized ? colors.active : colors.muted
|
||||
}
|
||||
|
||||
/**
|
||||
* 추이 행의 유입 스택 구간을 반환한다.
|
||||
* @param {Object} row - 추이 행
|
||||
* @returns {{ total: number, segments: Array<{ key: 'search' | 'sns' | 'other', value: number }> }}
|
||||
*/
|
||||
export const getAnalyticsTrafficStackSegments = (row) => {
|
||||
const total = Number(row?.visitors || 0)
|
||||
const search = Number(row?.trafficSearch || 0)
|
||||
const sns = Number(row?.trafficSns || 0)
|
||||
const other = Number(row?.trafficOther || 0)
|
||||
const trafficSum = search + sns + other
|
||||
|
||||
if (total <= 0) {
|
||||
return { total: 0, segments: [] }
|
||||
}
|
||||
|
||||
if (trafficSum <= 0) {
|
||||
return {
|
||||
total,
|
||||
segments: [{ key: 'other', value: total }]
|
||||
}
|
||||
}
|
||||
|
||||
const scale = total / trafficSum
|
||||
const scaledSearch = Math.round(search * scale)
|
||||
const scaledSns = Math.round(sns * scale)
|
||||
let scaledOther = total - scaledSearch - scaledSns
|
||||
|
||||
if (scaledOther < 0) {
|
||||
scaledOther = 0
|
||||
}
|
||||
|
||||
/** @type {Array<{ key: 'search' | 'sns' | 'other', value: number }>} */
|
||||
const segments = []
|
||||
|
||||
if (scaledSearch > 0) {
|
||||
segments.push({ key: 'search', value: scaledSearch })
|
||||
}
|
||||
|
||||
if (scaledSns > 0) {
|
||||
segments.push({ key: 'sns', value: scaledSns })
|
||||
}
|
||||
|
||||
if (scaledOther > 0) {
|
||||
segments.push({ key: 'other', value: scaledOther })
|
||||
}
|
||||
|
||||
if (!segments.length) {
|
||||
segments.push({ key: 'other', value: total })
|
||||
}
|
||||
|
||||
return { total, segments }
|
||||
}
|
||||
@@ -93,6 +93,21 @@ export const ANALYTICS_RETENTION_PURGE_INTERVAL_MS = 6 * 60 * 60 * 1000
|
||||
/** @type {number[]} 스크롤 구간 임계값 */
|
||||
export const ANALYTICS_SCROLL_THRESHOLDS = [0.25, 0.5, 0.75, 1]
|
||||
|
||||
/** @type {string} 통계 집계 기본 시작일(YYYY-MM-DD) */
|
||||
export const ANALYTICS_COLLECTED_SINCE_DEFAULT = '2026-06-01'
|
||||
|
||||
/**
|
||||
* 통계 집계 시작일을 YYYY-MM-DD 형식으로 정규화한다.
|
||||
* @param {string} [value] - 후보 시작일
|
||||
* @returns {string} 정규화된 시작일
|
||||
*/
|
||||
export const normalizeAnalyticsCollectedSinceDay = (value) => {
|
||||
const raw = String(value || ANALYTICS_COLLECTED_SINCE_DEFAULT).trim()
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(raw)
|
||||
? raw
|
||||
: ANALYTICS_COLLECTED_SINCE_DEFAULT
|
||||
}
|
||||
|
||||
/**
|
||||
* 체류시간(초)을 상한 내로 보정한다.
|
||||
* @param {number} seconds - 체류시간
|
||||
|
||||
@@ -3,11 +3,13 @@ import { createHash } from 'node:crypto'
|
||||
export {
|
||||
ANALYTICS_ACTIVE_SESSION_TTL_SECONDS,
|
||||
ANALYTICS_CHART_MAX_DAYS,
|
||||
ANALYTICS_COLLECTED_SINCE_DEFAULT,
|
||||
ANALYTICS_ENGAGED_MIN_SECONDS,
|
||||
ANALYTICS_MAX_DURATION_SECONDS,
|
||||
ANALYTICS_RETENTION_PURGE_INTERVAL_MS,
|
||||
ANALYTICS_SCROLL_THRESHOLDS,
|
||||
ANALYTICS_VISITOR_HASH_RETENTION_DAYS,
|
||||
normalizeAnalyticsCollectedSinceDay,
|
||||
clampAnalyticsDurationSeconds,
|
||||
clampAnalyticsScrollRatio,
|
||||
getAnalyticsDayBefore,
|
||||
|
||||
Reference in New Issue
Block a user