Sink/server/utils/access-log.ts
2025-05-21 19:54:54 +08:00

162 lines
4.9 KiB
TypeScript

import type { H3Event } from 'h3'
import { parseAcceptLanguage } from 'intl-parse-accept-language'
import { UAParser } from 'ua-parser-js'
import {
CLIs,
Crawlers,
Emails,
ExtraDevices,
Fetchers,
InApps,
MediaPlayers,
Vehicles,
} from 'ua-parser-js/extensions'
import { parseURL } from 'ufo'
import { getFlag } from '@/utils/flag'
function toBlobNumber(blob: string) {
return +blob.replace(/\D/g, '')
}
export const blobsMap = {
blob1: 'slug',
blob2: 'url',
blob3: 'ua',
blob4: 'ip',
blob5: 'referer',
blob6: 'country',
blob7: 'region',
blob8: 'city',
blob9: 'timezone',
blob10: 'language',
blob11: 'os',
blob12: 'browser',
blob13: 'browserType',
blob14: 'device',
blob15: 'deviceType',
blob16: 'COLO',
} as const
export const doublesMap = {
double1: 'latitude',
double2: 'longitude',
} as const
export type BlobsMap = typeof blobsMap
export type BlobsKey = keyof BlobsMap
export type DoublesMap = typeof doublesMap
export type DoublesKey = keyof DoublesMap
export type LogsKey = BlobsMap[BlobsKey] | DoublesMap[DoublesKey]
export type LogsMap = {
[key in BlobsMap[BlobsKey]]: string | undefined
} & {
[key in DoublesMap[DoublesKey]]?: number | undefined
}
export const logsMap = {
...Object.entries(blobsMap).reduce((acc, [k, v]) => ({ ...acc, [v]: k }), {}),
...Object.entries(doublesMap).reduce((acc, [k, v]) => ({ ...acc, [v]: k }), {}),
} as LogsMap
export function logs2blobs(logs: LogsMap) {
return (Object.keys(blobsMap) as BlobsKey[])
.sort((a, b) => toBlobNumber(a) - toBlobNumber(b))
.map(key => String(logs[blobsMap[key] as LogsKey] || ''))
}
export function blobs2logs(blobs: string[]) {
const logsList = Object.keys(blobsMap)
return blobs.reduce((logs, blob, i) => {
const key = blobsMap[logsList[i] as BlobsKey]
logs[key] = blob
return logs
}, {} as Partial<LogsMap>)
}
export function logs2doubles(logs: LogsMap) {
return (Object.keys(doublesMap) as DoublesKey[])
.sort((a, b) => toBlobNumber(a) - toBlobNumber(b))
.map(key => Number(logs[doublesMap[key] as LogsKey] || 0))
}
export function doubles2logs(doubles: number[]) {
const logsList = Object.keys(doublesMap)
return doubles.reduce((logs, double, i) => {
const key = doublesMap[logsList[i] as DoublesKey]
logs[key] = double
return logs
}, {} as Partial<LogsMap>)
}
export function useAccessLog(event: H3Event) {
const ip = getHeader(event, 'cf-connecting-ip') || getHeader(event, 'x-real-ip') || getRequestIP(event, { xForwardedFor: true })
const { host: referer } = parseURL(getHeader(event, 'referer'))
const acceptLanguage = getHeader(event, 'accept-language') || ''
const language = (parseAcceptLanguage(acceptLanguage) || [])[0]
const userAgent = getHeader(event, 'user-agent') || ''
const uaInfo = (new UAParser(userAgent, {
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
browser: [Crawlers.browser || [], CLIs.browser || [], Emails.browser || [], Fetchers.browser || [], InApps.browser || [], MediaPlayers.browser || [], Vehicles.browser || []].flat(),
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
device: [ExtraDevices.device || []].flat(),
})).getResult()
const { request: { cf } } = event.context.cloudflare
const link = event.context.link || {}
const isBot = cf?.botManagement?.verifiedBot
|| ['crawler', 'fetcher'].includes(uaInfo?.browser?.type || '')
|| ['spider', 'bot'].includes(uaInfo?.browser?.name?.toLowerCase() || '')
const { disableBotAccessLog } = useRuntimeConfig(event)
if (isBot && disableBotAccessLog) {
console.log('bot access log disabled:', userAgent)
return Promise.resolve()
}
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' })
const countryName = regionNames.of(cf?.country || 'WD') // fallback to "Worldwide"
const accessLogs = {
url: link.url,
slug: link.slug,
ua: userAgent,
ip,
referer,
country: cf?.country,
region: `${getFlag(cf?.country)} ${[cf?.region, countryName].filter(Boolean).join(',')}`,
city: `${getFlag(cf?.country)} ${[cf?.city, countryName].filter(Boolean).join(',')}`,
timezone: cf?.timezone,
language,
os: uaInfo?.os?.name,
browser: uaInfo?.browser?.name,
browserType: uaInfo?.browser?.type,
device: uaInfo?.device?.model,
deviceType: uaInfo?.device?.type,
COLO: cf?.colo,
// For RealTime Globe
latitude: Number(cf?.latitude || getHeader(event, 'cf-iplatitude') || 0),
longitude: Number(cf?.longitude || getHeader(event, 'cf-iplongitude') || 0),
}
if (process.env.NODE_ENV === 'production') {
return hubAnalytics().put({
indexes: [link.id], // only one index
blobs: logs2blobs(accessLogs),
doubles: logs2doubles(accessLogs),
})
}
else {
console.log('access logs:', accessLogs, logs2blobs(accessLogs), logs2doubles(accessLogs), { ...blobs2logs(logs2blobs(accessLogs)), ...doubles2logs(logs2doubles(accessLogs)) })
return Promise.resolve()
}
}