/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ /** @jsxRuntime automatic */ /** @jsxImportSource hono/jsx */ import type {LocaleCode} from '@fluxer/constants/src/Locales'; import {parseSession} from '@fluxer/hono/src/Session'; import {getLocaleFromCode, parseAcceptLanguage} from '@fluxer/locale/src/LocaleService'; import {detectArchitecture, detectPlatform} from '@fluxer/marketing/src/DeviceUtils'; import {getCountryCode} from '@fluxer/marketing/src/GeoIp'; import type {MarketingConfig} from '@fluxer/marketing/src/MarketingConfig'; import type {MarketingArchitecture, MarketingPlatform} from '@fluxer/marketing/src/MarketingContext'; import {recordCounter, recordHistogram} from '@fluxer/telemetry/src/Metrics'; import type {Context, MiddlewareHandler} from 'hono'; import {getCookie} from 'hono/cookie'; import {createMiddleware} from 'hono/factory'; export interface MarketingRequestInfo { locale: LocaleCode; platform: MarketingPlatform; architecture: MarketingArchitecture; countryCode: string; referrerDomain: string; } const LOCALE_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365; const REQUEST_INFO_KEY = 'marketing.requestInfo'; interface LocaleCookieSession { locale: string; } export async function getMarketingRequestInfo(c: Context, config: MarketingConfig): Promise { const existing = c.get(REQUEST_INFO_KEY); if (isMarketingRequestInfo(existing)) { return existing; } const locale = getRequestLocale(c, config); const userAgent = c.req.header('user-agent') ?? ''; const platform = detectPlatform(userAgent); const architecture = detectArchitecture(userAgent, platform); const countryCode = await getCountryCode(c.req.raw, { geoipDbPath: config.geoipDbPath, trustCfConnectingIp: config.trustCfConnectingIp, }); const referrerDomain = extractReferrerDomain(c.req.header('referer') ?? c.req.header('referrer')); const info = { locale, platform, architecture, countryCode, referrerDomain, }; c.set(REQUEST_INFO_KEY, info); return info; } export function createMarketingMetricsMiddleware(config: MarketingConfig): MiddlewareHandler { return createMiddleware(async (c, next) => { const startTime = Date.now(); const method = c.req.method; const path = c.req.path; await next(); const durationMs = Date.now() - startTime; const status = c.res.status; const requestInfo = await getMarketingRequestInfo(c, config); const dimensions: Record = { method, path, status: status.toString(), release_channel: config.releaseChannel, base_path: config.basePath, platform: requestInfo.platform, architecture: requestInfo.architecture, country_code: requestInfo.countryCode, locale: requestInfo.locale, referrer_domain: requestInfo.referrerDomain, }; recordHistogram({ name: 'marketing.request.latency', valueMs: durationMs, dimensions, }); recordCounter({ name: 'marketing.request.count', value: 1, dimensions, }); recordCounter({ name: 'marketing.request.outcome', value: 1, dimensions: { ...dimensions, outcome: status >= 400 ? 'failure' : 'success', }, }); }); } function getRequestLocale(c: Context, config: MarketingConfig): LocaleCode { const localeCookie = getCookie(c, 'locale'); if (localeCookie) { const session = parseLocaleCookie(localeCookie, config.secretKeyBase) ?? localeCookie; const locale = getLocaleFromCode(session); if (locale) return locale; } const header = c.req.header('accept-language'); if (header) { return parseAcceptLanguage(header); } return 'en-US'; } function parseLocaleCookie(cookieValue: string, secretKeyBase: string): string | null { const session = parseSession(cookieValue, secretKeyBase, LOCALE_COOKIE_MAX_AGE_SECONDS); return session?.locale ?? null; } function extractReferrerDomain(value?: string): string { if (!value) return 'direct'; try { const url = new URL(value); return url.hostname || 'unknown'; } catch { return 'unknown'; } } function isMarketingRequestInfo(value: unknown): value is MarketingRequestInfo { if (!value || typeof value !== 'object') return false; const record = value as Record; return ( typeof record['locale'] === 'string' && typeof record['platform'] === 'string' && typeof record['architecture'] === 'string' && typeof record['countryCode'] === 'string' && typeof record['referrerDomain'] === 'string' ); }