Files
sori.studio/lib/analytics-shared.js
zenn c43873ce5f v1.3.5: 관리자 로그인·대시보드 차트·통계 보관 정리
운영 HTTP에서 관리자 세션이 유지되지 않던 문제를 쿠키 공통화로 수정하고, 통계 클라이언트 분리·조회 오류·기간별 차트를 보강했다. 방문자 해시는 32일 초과분만 정리하고 일별 집계는 누적 보관한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 13:54:38 +09:00

143 lines
4.0 KiB
JavaScript

/** @type {RegExp} 추적 제외 경로 */
const EXCLUDED_PATH_PATTERN = /^\/(admin|signin|signup|forgot-password|settings)(\/|$)/
/** @type {RegExp} 봇 User-Agent 패턴 */
const BOT_USER_AGENT_PATTERN = /bot|crawl|spider|slurp|preview|headless|lighthouse|bytespider|facebookexternalhit/i
/**
* 오늘 날짜(UTC)를 YYYY-MM-DD로 반환한다.
* @returns {string} 날짜 문자열
*/
export const getAnalyticsDayKey = () => {
const now = new Date()
return now.toISOString().slice(0, 10)
}
/**
* 기준일에서 지정 일수만큼 이전 날짜를 YYYY-MM-DD로 반환한다.
* @param {string} dayKey - 기준일(YYYY-MM-DD)
* @param {number} daysBefore - 며칠 전(0이면 같은 날)
* @returns {string} 시작일
*/
export const getAnalyticsDayBefore = (dayKey, daysBefore) => {
const offset = Math.max(Number(daysBefore) || 0, 0)
const base = new Date(`${dayKey}T00:00:00.000Z`)
base.setUTCDate(base.getUTCDate() - offset)
return base.toISOString().slice(0, 10)
}
/**
* 통계 추적 대상 경로인지 확인한다.
* @param {string} path - 요청 경로
* @returns {boolean} 추적 가능 여부
*/
export const isTrackableAnalyticsPath = (path) => {
const normalized = (path || '').trim()
if (!normalized.startsWith('/')) {
return false
}
if (EXCLUDED_PATH_PATTERN.test(normalized)) {
return false
}
return true
}
/**
* 봇 User-Agent 여부
* @param {string} userAgent - User-Agent
* @returns {boolean} 봇 여부
*/
export const isBotUserAgent = (userAgent) => {
const value = (userAgent || '').trim()
if (!value) {
return false
}
return BOT_USER_AGENT_PATTERN.test(value)
}
/**
* 게시물 slug 정규화
* @param {string} slug - slug
* @returns {string} 정규화된 slug
*/
export const normalizePostSlugForAnalytics = (slug) => (slug || '').trim()
/** @type {number} heartbeat 체류시간 상한(초) */
export const ANALYTICS_MAX_DURATION_SECONDS = 1800
/** @type {number} engaged_views 집계 최소 체류(초) */
export const ANALYTICS_ENGAGED_MIN_SECONDS = 10
/** @type {number} 현재 접속자 판정 TTL(초) */
export const ANALYTICS_ACTIVE_SESSION_TTL_SECONDS = 90
/** @type {number} 관리자 차트 최대 조회 기간(일) */
export const ANALYTICS_CHART_MAX_DAYS = 365
/** @type {number} 일별 방문자 해시 보관 기간(일) */
export const ANALYTICS_VISITOR_HASH_RETENTION_DAYS = 32
/** @type {number} 통계 정리 최소 실행 간격(ms) */
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]
/**
* 체류시간(초)을 상한 내로 보정한다.
* @param {number} seconds - 체류시간
* @returns {number} 보정된 초
*/
export const clampAnalyticsDurationSeconds = (seconds) => {
const value = Number(seconds)
if (!Number.isFinite(value) || value < 0) {
return 0
}
return Math.min(Math.floor(value), ANALYTICS_MAX_DURATION_SECONDS)
}
/**
* 스크롤 비율을 0~1로 보정한다.
* @param {number} ratio - 스크롤 비율
* @returns {number} 보정된 비율
*/
export const clampAnalyticsScrollRatio = (ratio) => {
const value = Number(ratio)
if (!Number.isFinite(value) || value < 0) {
return 0
}
return Math.min(value, 1)
}
/**
* 새로 통과한 스크롤 구간 컬럼명 목록을 반환한다.
* @param {number} previousRatio - 이전 최대 스크롤
* @param {number} nextRatio - 갱신된 최대 스크롤
* @returns {Array<'scroll_25' | 'scroll_50' | 'scroll_75' | 'scroll_100'>} 신규 구간
*/
export const getNewScrollBucketColumns = (previousRatio, nextRatio) => {
const previous = clampAnalyticsScrollRatio(previousRatio)
const next = clampAnalyticsScrollRatio(nextRatio)
const columns = []
if (previous < 0.25 && next >= 0.25) {
columns.push('scroll_25')
}
if (previous < 0.5 && next >= 0.5) {
columns.push('scroll_50')
}
if (previous < 0.75 && next >= 0.75) {
columns.push('scroll_75')
}
if (previous < 1 && next >= 1) {
columns.push('scroll_100')
}
return columns
}