Skip to content

Commit 257c309

Browse files
authored
feat: privacy-first analytics swap and legal page rewrite (#566)
* feat: privacy-first analytics swap and legal page rewrite - Remove @vercel/analytics and @vercel/speed-insights entirely - Add self-hosted Plausible (analytics.librechat.ai) using the outbound-links + tagged-events + hash combined script variant - Add env-gated next-cwv-monitor for Core Web Vitals (LCP, INP, CLS, FCP, TTFB), driven by NEXT_PUBLIC_CWV_PROJECT_ID and NEXT_PUBLIC_CWV_ENDPOINT, with optional NEXT_PUBLIC_CWV_SAMPLE_RATE - Migrate TrackedLink card_click and link_click events from Vercel track() to window.plausible(...) with a Window.plausible global declaration; outbound-link auto-tracking complements them - Rewrite /tos, /privacy, /cookie as enterprise-grade, GDPR/CCPA aware policies that accurately disclose the new cookieless analytics stack, lawful basis, retention, sub-processors, and data-subject rights; the site remains banner-free under ePrivacy 5(3) since no terminal-equipment storage is used - Fix footer label: "Terms of services" -> "Terms of Service" * fix: wire privacy analytics and cwv monitor
1 parent 89371e2 commit 257c309

10 files changed

Lines changed: 919 additions & 226 deletions

File tree

.env.template

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ LOOPS_API_KEY=
1717
NEXT_PUBLIC_POSTHOG_HOST=
1818
# PostHog project API key (optional, public)
1919
NEXT_PUBLIC_POSTHOG_KEY=
20+
# Core Web Vitals ingestion config (optional, public)
21+
NEXT_PUBLIC_CWV_PROJECT_ID=64ddab45-756f-474b-a8c9-266d264c93d8
22+
NEXT_PUBLIC_CWV_ENDPOINT=
23+
NEXT_PUBLIC_CWV_SAMPLE_RATE=0.5
2024

2125
# Crisp live chat widget ID (optional, public)
2226
NEXT_PUBLIC_CRISP_WEBSITE_ID=

app/cookie/page.tsx

Lines changed: 140 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import type { Metadata } from 'next'
66

77
export const metadata: Metadata = {
88
title: 'Cookie Policy',
9-
description: 'Cookie Policy for the LibreChat documentation website.',
9+
description:
10+
'How the LibreChat documentation website handles cookies, browser storage, and cookieless analytics.',
1011
}
1112

1213
export default function CookiePolicyPage() {
@@ -21,79 +22,179 @@ export default function CookiePolicyPage() {
2122
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
2223
Cookie Policy
2324
</h1>
24-
<p className="mt-4 text-sm text-muted-foreground">Last updated: June 13, 2025</p>
25+
<p className="mt-4 text-sm text-muted-foreground">Last updated: April 26, 2026</p>
2526
</header>
2627

27-
<h2>No Cookies Used</h2>
28+
<h2>1. Overview</h2>
2829
<p>
2930
The LibreChat documentation website (
30-
<Link href="https://librechat.ai">librechat.ai</Link>){' '}
31-
<strong>does not use cookies</strong> or any similar tracking technologies.
31+
<Link href="https://librechat.ai">librechat.ai</Link>, the &ldquo;Site&rdquo;) does{' '}
32+
<strong>not use cookies</strong>. This Cookie Policy describes the small amount of
33+
client-side storage we do use, explains how our analytics operate without cookies, and
34+
describes the controls available to you. It supplements our{' '}
35+
<Link href="/privacy">Privacy Policy</Link>.
3236
</p>
3337

34-
<h2>What This Means</h2>
38+
<h2>2. We Do Not Use Cookies</h2>
39+
<p>
40+
The Site does not set any cookies on your device, and we do not read cookies set by
41+
other websites. Specifically, we do not use:
42+
</p>
3543
<ul>
36-
<li>We don&apos;t set or read any cookies</li>
37-
<li>We don&apos;t track your browsing behavior</li>
38-
<li>We don&apos;t collect any personal information</li>
39-
<li>We don&apos;t use analytics or advertising cookies</li>
40-
<li>We don&apos;t use third-party tracking services</li>
44+
<li>Authentication cookies (the Site has no user accounts and no login).</li>
45+
<li>Advertising or marketing cookies.</li>
46+
<li>Cross-site tracking cookies.</li>
47+
<li>Third-party social-network cookies.</li>
48+
<li>Personalization or preference cookies set by us.</li>
4149
</ul>
50+
<p>
51+
Because we do not use cookies or any equivalent device-storage identifier within the
52+
meaning of the EU ePrivacy Directive, we do not display a cookie banner and do not
53+
require your consent for cookies.
54+
</p>
4255

43-
<h2>Browser Storage</h2>
56+
<h2>3. How Our Analytics Work Without Cookies</h2>
4457
<p>
45-
While we don&apos;t use cookies, this documentation site may use browser features like:
58+
We operate cookieless analytics and performance tooling. Both are described in detail in
59+
our <Link href="/privacy">Privacy Policy</Link>; the points most relevant to cookies and
60+
device storage are summarized here.
61+
</p>
62+
63+
<h3>3.1 Plausible Analytics</h3>
64+
<p>
65+
We host an instance of <Link href="https://plausible.io/data-policy">Plausible</Link> at{' '}
66+
<code>plausible.librechat.ai</code>. Plausible:
4667
</p>
4768
<ul>
4869
<li>
49-
<strong>Local Storage</strong>: To remember your theme preference (light/dark mode)
70+
Does <strong>not</strong> set cookies.
71+
</li>
72+
<li>
73+
Does <strong>not</strong> use <code>localStorage</code> or <code>sessionStorage</code>
74+
.
5075
</li>
5176
<li>
52-
<strong>Session Storage</strong>: To maintain navigation state during your visit
77+
Does <strong>not</strong> use any other persistent client-side identifier such as
78+
IndexedDB, ETags, or browser fingerprinting techniques.
5379
</li>
5480
</ul>
55-
<p>This data:</p>
81+
<p>
82+
Visitor de-duplication is achieved on the server side using a one-way, daily-rotating
83+
hash that combines a salt with the IP address and User-Agent. The salt rotates every
84+
twenty-four hours and the IP address itself is never written to disk, which means the
85+
hash cannot be linked across days and cannot be reversed back to a person or a device.
86+
</p>
87+
88+
<h3>3.2 Core Web Vitals Monitoring</h3>
89+
<p>
90+
When performance monitoring is enabled, we collect anonymous Web Vitals measurements
91+
using the open-source{' '}
92+
<Link href="https://github.com/GoogleChrome/web-vitals">web-vitals</Link> library and a
93+
small LibreChat client-side collector. This tool:
94+
</p>
5695
<ul>
57-
<li>Is stored only in your browser</li>
58-
<li>Is never transmitted to any server</li>
59-
<li>Can be cleared by clearing your browser data</li>
60-
<li>Does not identify you personally</li>
96+
<li>
97+
Does <strong>not</strong> set cookies.
98+
</li>
99+
<li>
100+
Does <strong>not</strong> use <code>localStorage</code> or <code>sessionStorage</code>
101+
.
102+
</li>
103+
<li>
104+
Generates a session identifier in browser memory using{' '}
105+
<code>crypto.randomUUID()</code> and{' '}
106+
<strong>rotates it on every page navigation</strong>, so measurements cannot be
107+
correlated across page views or visits.
108+
</li>
61109
</ul>
62110

63-
<h2>Third-Party Links</h2>
111+
<h2>4. Local Browser Storage We Do Use</h2>
64112
<p>
65-
Our documentation may contain links to external websites that might use cookies. We are
66-
not responsible for the cookie practices of external sites. Please review their cookie
67-
policies when visiting them.
113+
For your convenience, the Site itself may write a small amount of non-identifying
114+
information to your browser&apos;s <code>localStorage</code>. This data:
115+
</p>
116+
<ul>
117+
<li>Is stored only on your device.</li>
118+
<li>Is never transmitted to our servers or to any third party.</li>
119+
<li>Does not identify you and is not combined with any other information.</li>
120+
<li>Can be cleared at any time using your browser&apos;s site-data controls.</li>
121+
</ul>
122+
<p>The keys we may set include:</p>
123+
<ul>
124+
<li>
125+
<strong>Theme preference</strong>: whether you have selected light or dark mode.
126+
</li>
127+
<li>
128+
<strong>Navigation state</strong>: the open/closed state of collapsible documentation
129+
sections so the layout is preserved as you browse.
130+
</li>
131+
</ul>
132+
133+
<h2>5. Third-Party Links and Embedded Content</h2>
134+
<p>
135+
The documentation links to a number of third-party resources, including the LibreChat
136+
GitHub repository, package registries, integration partner sites, video platforms, and
137+
community forums. Some of those third parties may set their own cookies when you visit
138+
them. We do not control, and are not responsible for, the cookies or storage practices
139+
of external sites; please review their cookie policies directly.
140+
</p>
141+
<p>
142+
Where we conditionally load a small transparent pixel from{' '}
143+
<Link href="https://about.scarf.sh/privacy">Scarf</Link> (<code>static.scarf.sh</code>)
144+
for anonymous package-pull telemetry, that pixel does not set cookies and does not use{' '}
145+
<code>localStorage</code>.
68146
</p>
69147

70-
<h2>Your Privacy</h2>
71-
<p>Since we don&apos;t use cookies:</p>
148+
<h2>6. Your Choices and Controls</h2>
149+
<p>
150+
You can further restrict the limited information described in this policy at any time
151+
by:
152+
</p>
72153
<ul>
73-
<li>There&apos;s nothing to accept or decline</li>
74-
<li>No tracking occurs</li>
75-
<li>Your browsing is completely private</li>
76-
<li>No consent banner is needed</li>
154+
<li>
155+
Configuring your browser to block third-party requests, to clear site data on close,
156+
or to deny <code>localStorage</code> access for this Site.
157+
</li>
158+
<li>
159+
Enabling <strong>Do Not Track</strong>, <strong>Global Privacy Control</strong>, or
160+
tracker-blocking features in your browser.
161+
</li>
162+
<li>
163+
Installing a content-blocking extension that blocks requests to{' '}
164+
<code>plausible.librechat.ai</code> and any performance-monitoring endpoint we
165+
configure.
166+
</li>
167+
<li>Disabling JavaScript, in which case no analytics or performance data is sent.</li>
77168
</ul>
169+
<p>
170+
Because we do not rely on cookies or persistent identifiers, exercising any of these
171+
controls will not break the documentation experience.
172+
</p>
78173

79-
<h2>Changes to This Policy</h2>
174+
<h2>7. Changes to This Policy</h2>
80175
<p>
81-
We may update this cookie policy for clarity. Any updates will be posted on this page
82-
with a new &ldquo;Last updated&rdquo; date. However, we do not anticipate adding cookies
83-
to this documentation site.
176+
We may update this Cookie Policy from time to time to reflect changes in our tooling or
177+
in applicable law. Material changes will be reflected in the &ldquo;Last updated&rdquo;
178+
date at the top of this page. We do not anticipate adding cookies to the Site, and any
179+
change in that posture would be communicated prominently before being deployed.
84180
</p>
85181

86-
<h2>Contact</h2>
182+
<h2>8. Contact</h2>
87183
<p>
88-
For questions about this policy or the LibreChat project, please visit{' '}
89-
<Link href="https://github.com/danny-avila/LibreChat">our GitHub repository</Link>.
184+
For questions about this Cookie Policy, please contact us at{' '}
185+
<Link href="mailto:contact@librechat.ai">contact@librechat.ai</Link> or open an issue in
186+
the{' '}
187+
<Link href="https://github.com/LibreChat-AI/librechat.ai/issues">
188+
project repository
189+
</Link>
190+
.
90191
</p>
91192

92193
<hr />
93194

94195
<p className="text-sm text-muted-foreground">
95-
By using this documentation site, you acknowledge that no cookies or tracking
96-
technologies are employed.
196+
By using this documentation site, you acknowledge that no cookies are set and that the
197+
cookieless analytics described above operate as part of normal Site operation.
97198
</p>
98199
</article>
99200
</main>

app/layout.tsx

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import Script from 'next/script'
22
import { GeistSans } from 'geist/font/sans'
33
import { GeistMono } from 'geist/font/mono'
4-
import { Analytics } from '@vercel/analytics/react'
5-
import { SpeedInsights } from '@vercel/speed-insights/next'
64
import type { ReactNode } from 'react'
75
import type { Metadata } from 'next'
86
import { Provider } from '@/components/provider'
97
import { AskAILoader } from '@/components/ai/AskAILoader'
8+
import { CoreWebVitalsMonitor } from '@/components/analytics/CoreWebVitalsMonitor'
109
import './global.css'
1110

11+
const DEFAULT_CWV_ENDPOINT = ''
12+
const DEFAULT_CWV_PROJECT_ID = '64ddab45-756f-474b-a8c9-266d264c93d8'
13+
const DEFAULT_CWV_SAMPLE_RATE = 0.5
14+
1215
export const metadata: Metadata = {
1316
title: {
1417
default: 'LibreChat',
@@ -34,14 +37,40 @@ export const metadata: Metadata = {
3437
},
3538
}
3639

40+
function normalizeCwvEndpoint(endpoint: string) {
41+
const trimmedEndpoint = endpoint.trim()
42+
if (!trimmedEndpoint) return ''
43+
44+
const endpointUrl = /^[a-z][a-z\d+.-]*:\/\//i.test(trimmedEndpoint)
45+
? trimmedEndpoint
46+
: `https://${trimmedEndpoint}`
47+
48+
try {
49+
const url = new URL(endpointUrl)
50+
return url.protocol === 'https:' || url.protocol === 'http:' ? url.origin : ''
51+
} catch {
52+
return ''
53+
}
54+
}
55+
3756
export default function RootLayout({ children }: { children: ReactNode }) {
3857
// Sanitize Scarf pixel ID: only allow alphanumeric chars, underscores, and hyphens
3958
// to prevent XSS via dangerouslySetInnerHTML injection if the env var is compromised.
4059
const rawScarfId = process.env.NEXT_PUBLIC_SCARF_PIXEL_ID ?? ''
4160
const scarfPixelId = /^[\w-]+$/.test(rawScarfId) ? rawScarfId : ''
42-
4361
const askAIEnabled = Boolean(process.env.OPENROUTER_API_KEY)
4462

63+
const cwvProjectId = process.env.NEXT_PUBLIC_CWV_PROJECT_ID?.trim() || DEFAULT_CWV_PROJECT_ID
64+
const cwvEndpoint = normalizeCwvEndpoint(
65+
process.env.NEXT_PUBLIC_CWV_ENDPOINT || DEFAULT_CWV_ENDPOINT,
66+
)
67+
const cwvEnabled = /^[\w-]+$/.test(cwvProjectId) && cwvEndpoint !== ''
68+
const cwvSampleRateRaw = Number(process.env.NEXT_PUBLIC_CWV_SAMPLE_RATE)
69+
const cwvSampleRate =
70+
Number.isFinite(cwvSampleRateRaw) && cwvSampleRateRaw >= 0 && cwvSampleRateRaw <= 1
71+
? cwvSampleRateRaw
72+
: DEFAULT_CWV_SAMPLE_RATE
73+
4574
return (
4675
<html
4776
lang="en"
@@ -51,8 +80,30 @@ export default function RootLayout({ children }: { children: ReactNode }) {
5180
<body className="flex min-h-screen flex-col">
5281
<Provider>{children}</Provider>
5382
{askAIEnabled && <AskAILoader />}
54-
<Analytics />
55-
<SpeedInsights />
83+
{/* Privacy-friendly analytics by Plausible */}
84+
<Script
85+
id="plausible-init"
86+
strategy="beforeInteractive"
87+
dangerouslySetInnerHTML={{
88+
__html: `
89+
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
90+
plausible.init()
91+
`,
92+
}}
93+
/>
94+
<Script
95+
async
96+
id="plausible-script"
97+
src="https://plausible.librechat.ai/js/pa-AxQn4zbc0KTWDDkxjlFGs.js"
98+
strategy="afterInteractive"
99+
/>
100+
{cwvEnabled && (
101+
<CoreWebVitalsMonitor
102+
projectId={cwvProjectId}
103+
endpoint={cwvEndpoint}
104+
sampleRate={cwvSampleRate}
105+
/>
106+
)}
56107
{scarfPixelId && (
57108
<Script
58109
id="scarf-pixel"

0 commit comments

Comments
 (0)