fix: namespace rate limit buckets per endpoint, remove custom analytics
Some checks failed
Gitea CI / test-build (push) Failing after 5m21s
Dev Deployment (Zero Downtime) / deploy-dev (push) Successful in 18m29s

- Add `prefix` param to checkRateLimit/getRateLimitHeaders so each endpoint
  has its own bucket (previously all shared `admin_${ip}`, causing 429s when
  analytics/track incremented past n8n endpoints' lower limits)
- n8n/hardcover/currently-reading → prefix 'n8n-reading'
- n8n/status → prefix 'n8n-status'
- analytics/track → prefix 'analytics-track'
- Remove custom analytics system (AnalyticsProvider, lib/analytics,
  lib/useWebVitals, all /api/analytics/* routes) — was causing 500s in
  production due to missing PostgreSQL PageView table
- Remove analytics consent toggle from ConsentBanner/ConsentProvider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 23:12:50 +01:00
parent 6c5297836c
commit 32e621df14
17 changed files with 17 additions and 1655 deletions

View File

@@ -5,8 +5,7 @@ import { usePathname } from "next/navigation";
import dynamic from "next/dynamic";
import { ToastProvider } from "@/components/Toast";
import ErrorBoundary from "@/components/ErrorBoundary";
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
import { ConsentProvider, useConsent } from "./ConsentProvider";
import { ConsentProvider } from "./ConsentProvider";
import { ThemeProvider } from "./ThemeProvider";
import { motion, AnimatePresence } from "framer-motion";
@@ -97,12 +96,7 @@ function GatedProviders({
mounted: boolean;
is404Page: boolean;
}) {
const { consent } = useConsent();
// If consent is not decided yet, treat optional features as off
const analyticsEnabled = !!consent?.analytics;
const content = (
return (
<ErrorBoundary>
<ToastProvider>
{mounted && <BackgroundBlobs />}
@@ -110,6 +104,4 @@ function GatedProviders({
</ToastProvider>
</ErrorBoundary>
);
return analyticsEnabled ? <AnalyticsProvider>{content}</AnalyticsProvider> : content;
}

View File

@@ -6,7 +6,7 @@ import { useTranslations } from "next-intl";
export default function ConsentBanner() {
const { consent, ready, setConsent } = useConsent();
const [draft, setDraft] = useState<ConsentState>({ analytics: false, chat: false });
const [draft, setDraft] = useState<ConsentState>({ chat: false });
const [minimized, setMinimized] = useState(false);
const t = useTranslations("consent");
@@ -19,7 +19,6 @@ export default function ConsentBanner() {
title: t("title"),
description: t("description"),
essential: t("essential"),
analytics: t("analytics"),
chat: t("chat"),
alwaysOn: t("alwaysOn"),
acceptAll: t("acceptAll"),
@@ -68,16 +67,6 @@ export default function ConsentBanner() {
<div className="text-[11px] text-stone-500">{s.alwaysOn}</div>
</div>
<label className="flex items-center justify-between gap-3 py-1">
<span className="text-sm font-semibold text-stone-800">{s.analytics}</span>
<input
type="checkbox"
checked={draft.analytics}
onChange={(e) => setDraft((p) => ({ ...p, analytics: e.target.checked }))}
className="w-4 h-4 accent-stone-900"
/>
</label>
<label className="flex items-center justify-between gap-3 py-1">
<span className="text-sm font-semibold text-stone-800">{s.chat}</span>
<input
@@ -91,7 +80,7 @@ export default function ConsentBanner() {
<div className="mt-3 flex flex-col gap-2">
<button
onClick={() => setConsent({ analytics: true, chat: true })}
onClick={() => setConsent({ chat: true })}
className="px-4 py-2 rounded-xl bg-stone-900 text-stone-50 font-semibold hover:bg-stone-800 transition-colors"
>
{s.acceptAll}
@@ -103,7 +92,7 @@ export default function ConsentBanner() {
{s.acceptSelected}
</button>
<button
onClick={() => setConsent({ analytics: false, chat: false })}
onClick={() => setConsent({ chat: false })}
className="px-4 py-2 rounded-xl bg-transparent text-stone-600 font-semibold hover:text-stone-900 transition-colors"
>
{s.rejectAll}

View File

@@ -3,7 +3,6 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
export type ConsentState = {
analytics: boolean;
chat: boolean;
};
@@ -20,7 +19,6 @@ function readConsentFromCookie(): ConsentState | null {
try {
const parsed = JSON.parse(value) as Partial<ConsentState>;
return {
analytics: !!parsed.analytics,
chat: !!parsed.chat,
};
} catch {

View File

@@ -2,13 +2,6 @@
import React, { useEffect, useState } from "react";
// Lazy load providers to avoid webpack module resolution issues
const AnalyticsProvider = React.lazy(() =>
import("@/components/AnalyticsProvider").then((mod) => ({
default: mod.AnalyticsProvider,
}))
);
const ToastProvider = React.lazy(() =>
import("@/components/Toast").then((mod) => ({
default: mod.ToastProvider,
@@ -38,14 +31,11 @@ export default function RootProviders({
return (
<React.Suspense fallback={<div className="relative z-10">{children}</div>}>
<AnalyticsProvider>
<ToastProvider>
<BackgroundBlobs />
<div className="relative z-10">{children}</div>
<ChatWidget />
</ToastProvider>
</AnalyticsProvider>
<ToastProvider>
<BackgroundBlobs />
<div className="relative z-10">{children}</div>
<ChatWidget />
</ToastProvider>
</React.Suspense>
);
}