fix: clarify cookie usage and session storage details in About, FAQ, and Security pages; add session replay explanation in FAQ

This commit is contained in:
Usman Baig
2026-01-19 14:12:10 +01:00
parent 8a648a2e5f
commit 2aa25cb3aa
4 changed files with 20 additions and 6 deletions

View File

@@ -18,7 +18,7 @@ export default function AboutPage() {
We believe in privacy by design. Our analytics platform: We believe in privacy by design. Our analytics platform:
</p> </p>
<ul className="list-disc list-inside space-y-2 text-neutral-600 dark:text-neutral-400 mb-6"> <ul className="list-disc list-inside space-y-2 text-neutral-600 dark:text-neutral-400 mb-6">
<li>Uses no cookies or persistent identifiers</li> <li>Uses no cookies or cross-session identifiers; sessionStorage is used only to group events within a single visit</li>
<li>Respects Do Not Track preferences</li> <li>Respects Do Not Track preferences</li>
<li>Complies with GDPR and CCPA regulations</li> <li>Complies with GDPR and CCPA regulations</li>
<li>Does not collect personal data</li> <li>Does not collect personal data</li>

View File

@@ -10,11 +10,15 @@ export default function FAQPage() {
}, },
{ {
question: "How does Ciphera Analytics track visitors?", question: "How does Ciphera Analytics track visitors?",
answer: "We use a lightweight JavaScript snippet that sends anonymous pageview events. No cookies, no persistent identifiers, and no cross-site tracking." answer: "We use a lightweight JavaScript snippet that sends anonymous pageview events. No cookies, no cross-session identifiers (we use sessionStorage only to group events within a single visit), and no cross-site tracking."
}, },
{ {
question: "What data does Ciphera Analytics collect?", question: "What data does Ciphera Analytics collect?",
answer: "We collect anonymous pageview data including page path, referrer, device type, browser, and country (from IP, not stored). No personal information is collected." answer: "We collect anonymous pageview data including page path, referrer, device type, browser, and country (derived from IP at request time; the IP itself is not stored). No personal information is collected. If you enable optional session replay, see 'What about session replay?' below."
},
{
question: "What about session replay?",
answer: "Session replay is optional and off by default. When enabled, we use Anonymous Skeleton mode: all visible text is replaced with blocks, all form inputs are hidden, and we do not record canvas or fonts. We record layout, clicks, and scrolls to help you understand how visitors use your site. Replay metadata (device, browser, OS, country) is stored; we redact common PII-like URL parameters (e.g. email=, token=) before sending. Session replay uses no cookies and does not require a consent banner. We respect Do Not Track—if it is set, replay does not run."
}, },
{ {
question: "How accurate is the data?", question: "How accurate is the data?",

View File

@@ -16,7 +16,7 @@ export default function SecurityPage() {
<li>All data is encrypted in transit using TLS/SSL</li> <li>All data is encrypted in transit using TLS/SSL</li>
<li>No personal data is collected or stored</li> <li>No personal data is collected or stored</li>
<li>IP addresses are hashed immediately and not stored</li> <li>IP addresses are hashed immediately and not stored</li>
<li>No cookies or persistent identifiers are used</li> <li>No cookies or cross-session identifiers; sessionStorage is used only to group events within a single visit</li>
<li>Data is processed anonymously</li> <li>Data is processed anonymously</li>
</ul> </ul>

View File

@@ -382,13 +382,22 @@
} }
} }
// * Redact common PII-like URL query/fragment parameters in replay JSON before sending
function redactPiiInReplayJson(jsonStr) {
return jsonStr.replace(
/([?&])(email|token|session|auth|password|secret|api_key|apikey|access_token|refresh_token)=[^&"'\s]*/gi,
'$1$2=***'
);
}
// * Send chunk of events to server // * Send chunk of events to server
async function sendReplayChunk() { async function sendReplayChunk() {
if (!replayId || replayEvents.length === 0) return; if (!replayId || replayEvents.length === 0) return;
const chunk = replayEvents.splice(0, CHUNK_SIZE); const chunk = replayEvents.splice(0, CHUNK_SIZE);
const eventsCount = chunk.length; const eventsCount = chunk.length;
const data = JSON.stringify(chunk); let data = JSON.stringify(chunk);
data = redactPiiInReplayJson(data);
try { try {
// Try to compress if available // Try to compress if available
@@ -437,7 +446,8 @@
// Send remaining events // Send remaining events
if (replayEvents.length > 0) { if (replayEvents.length > 0) {
const chunk = replayEvents.splice(0); const chunk = replayEvents.splice(0);
const data = JSON.stringify(chunk); let data = JSON.stringify(chunk);
data = redactPiiInReplayJson(data);
navigator.sendBeacon( navigator.sendBeacon(
apiUrl + '/api/v1/replays/' + replayId + '/chunks', apiUrl + '/api/v1/replays/' + replayId + '/chunks',
new Blob([data], { type: 'application/json' }) new Blob([data], { type: 'application/json' })