mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): Cookie consent banner and settings (#11306)
Implements a cookie consent banner and settings modal for GDPR compliance, allowing users to manage preferences for analytics and monitoring cookies. Integrates consent checks with Sentry, Vercel Analytics, and Google Analytics, ensuring tracking is only enabled with user permission. Refactors dialog components for improved layout and adds consent management utilities and hooks. #### For code changes: - [x] Banner appears at bottom of page on first visit with rounded corners and proper spacing (40px margins) - [x] Banner shows three buttons: "Reject All", "Settings", and "Accept All" - [x] Clicking "Accept All" hides banner and enables analytics/monitoring - [x] Clicking "Reject All" hides banner and keeps analytics/monitoring disabled - [x] Banner does not reappear after consent is given (check localStorage: `autogpt_cookie_consent`) **Cookie Settings Modal:** - [x] Clicking "Settings" button opens the Cookie Settings modal - [x] Modal displays three categories: Essential Cookies (always active), Analytics & Performance (toggle), Error Monitoring & Session Replay (toggle) - [x] Clicking "Save Preferences" saves custom settings and closes modal - [x] Clicking "Accept All" enables all cookies and closes modal - [x] Clicking "Reject All" disables optional cookies and closes modal - [x] Modal can be closed with X button or clicking outside **Consent Persistence:** - [x] Refresh page after giving consent - banner should not reappear - [x] Clear localStorage and refresh - banner should reappear - [x] Consent choices persist across browser sessions <img width="1123" height="126" alt="image" src="https://github.com/user-attachments/assets/7425efab-b5cc-4449-802d-0e12bd65053b" /> <img width="1124" height="372" alt="image" src="https://github.com/user-attachments/assets/2f28919a-97e8-44f5-9021-70d3836bb996" />
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
// The config you add here will be used whenever a users loads a page in their browser.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import { consent } from "@/services/consent/cookies";
|
||||
import { environment } from "@/services/environment";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
@@ -11,6 +12,9 @@ const isDisabled = process.env.DISABLE_SENTRY === "true";
|
||||
|
||||
const shouldEnable = !isDisabled && isProdOrDev && isCloud;
|
||||
|
||||
// Check for monitoring consent (includes session replay)
|
||||
const hasMonitoringConsent = consent.hasConsentFor("monitoring");
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://fe4e4aa4a283391808a5da396da20159@o4505260022104064.ingest.us.sentry.io/4507946746380288",
|
||||
|
||||
@@ -50,10 +54,12 @@ Sentry.init({
|
||||
// Define how likely Replay events are sampled.
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: 0.1,
|
||||
// GDPR: Only enable if user has consented to monitoring
|
||||
replaysSessionSampleRate: hasMonitoringConsent ? 0.1 : 0,
|
||||
|
||||
// Define how likely Replay events are sampled when an error occurs.
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
// GDPR: Only enable if user has consented to monitoring
|
||||
replaysOnErrorSampleRate: hasMonitoringConsent ? 1.0 : 0,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
@@ -37,6 +37,27 @@ export default defineConfig({
|
||||
/* Helps debugging failures */
|
||||
trace: "retain-on-failure",
|
||||
video: "retain-on-failure",
|
||||
|
||||
/* Auto-accept cookies in all tests to prevent banner interference */
|
||||
storageState: {
|
||||
cookies: [],
|
||||
origins: [
|
||||
{
|
||||
origin: "http://localhost:3000",
|
||||
localStorage: [
|
||||
{
|
||||
name: "autogpt_cookie_consent",
|
||||
value: JSON.stringify({
|
||||
hasConsented: true,
|
||||
timestamp: Date.now(),
|
||||
analytics: true,
|
||||
monitoring: true,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
/* Maximum time one test can run for */
|
||||
timeout: 25000,
|
||||
|
||||
@@ -5,13 +5,13 @@ import React from "react";
|
||||
import "./globals.css";
|
||||
|
||||
import { Providers } from "@/app/providers";
|
||||
import { CookieConsentBanner } from "@/components/molecules/CookieConsentBanner/CookieConsentBanner";
|
||||
import TallyPopupSimple from "@/components/molecules/TallyPoup/TallyPopup";
|
||||
import { Toaster } from "@/components/molecules/Toast/toaster";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { headers } from "next/headers";
|
||||
import { SetupAnalytics } from "@/services/analytics";
|
||||
import { VercelAnalyticsWrapper } from "@/services/analytics/VercelAnalyticsWrapper";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AutoGPT Platform",
|
||||
@@ -51,8 +51,7 @@ export default async function RootLayout({
|
||||
<div className="flex min-h-screen flex-col items-stretch justify-items-stretch">
|
||||
{children}
|
||||
<TallyPopupSimple />
|
||||
<SpeedInsights />
|
||||
<Analytics />
|
||||
<VercelAnalyticsWrapper />
|
||||
|
||||
{/* React Query DevTools is only available in development */}
|
||||
{process.env.NEXT_PUBLIC_REACT_QUERY_DEVTOOL && (
|
||||
@@ -63,6 +62,7 @@ export default async function RootLayout({
|
||||
)}
|
||||
</div>
|
||||
<Toaster />
|
||||
<CookieConsentBanner />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { CookieIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import { useCookieConsentBanner } from "./useCookieConsentBanner";
|
||||
import { CookieSettingsModal } from "./components/CookieSettingsModal/CookieSettingsModal";
|
||||
|
||||
export function CookieConsentBanner() {
|
||||
const {
|
||||
shouldShowBanner,
|
||||
showSettings,
|
||||
handleAcceptAll,
|
||||
handleRejectAll,
|
||||
handleOpenSettings,
|
||||
handleCloseSettings,
|
||||
} = useCookieConsentBanner();
|
||||
|
||||
if (!shouldShowBanner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 px-10 pb-4">
|
||||
<div
|
||||
className="mx-auto max-w-6xl rounded-lg border border-neutral-200 bg-white p-4 shadow-lg dark:border-neutral-800 dark:bg-neutral-950"
|
||||
role="dialog"
|
||||
aria-label="Cookie consent banner"
|
||||
>
|
||||
<div className="flex flex-col items-start gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-1 items-start gap-3">
|
||||
<CookieIcon className="mt-0.5 h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-300" />
|
||||
<div className="flex-1">
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="mb-1 text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
We use cookies
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
AutoGPT uses essential cookies for login and optional cookies
|
||||
for analytics and error tracking to improve our service.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-2 md:w-auto md:flex-row md:items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={handleRejectAll}
|
||||
className="w-full md:w-auto"
|
||||
>
|
||||
Reject All
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={handleOpenSettings}
|
||||
className="w-full md:w-auto"
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={handleAcceptAll}
|
||||
className="w-full md:w-auto"
|
||||
>
|
||||
Accept All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSettings && (
|
||||
<CookieSettingsModal
|
||||
isOpen={showSettings}
|
||||
onClose={handleCloseSettings}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { COOKIE_CATEGORIES } from "@/services/consent/cookies";
|
||||
import { CheckIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import { useCookieSettingsModal } from "./useCookieSettingsModal";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function CookieSettingsModal({ isOpen, onClose }: Props) {
|
||||
const {
|
||||
analytics,
|
||||
setAnalytics,
|
||||
monitoring,
|
||||
setMonitoring,
|
||||
handleSavePreferences,
|
||||
handleAcceptAll,
|
||||
handleRejectAll,
|
||||
} = useCookieSettingsModal({ onClose });
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Cookie Settings"
|
||||
controlled={{
|
||||
isOpen,
|
||||
set: (open) => {
|
||||
if (!open) onClose();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div className="space-y-6 pb-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
{COOKIE_CATEGORIES.essential.name}
|
||||
</Text>
|
||||
<span className="rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-medium text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400">
|
||||
Always Active
|
||||
</span>
|
||||
</div>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
{COOKIE_CATEGORIES.essential.description}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<CheckIcon className="h-5 w-5 text-green-600" weight="bold" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-neutral-200 dark:border-neutral-800" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
{COOKIE_CATEGORIES.analytics.name}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
{COOKIE_CATEGORIES.analytics.description}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={analytics}
|
||||
onCheckedChange={setAnalytics}
|
||||
aria-label="Toggle analytics cookies"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-neutral-200 dark:border-neutral-800" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
{COOKIE_CATEGORIES.monitoring.name}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
{COOKIE_CATEGORIES.monitoring.description}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={monitoring}
|
||||
onCheckedChange={setMonitoring}
|
||||
aria-label="Toggle monitoring cookies"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<div className="flex w-full flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<Button variant="ghost" size="small" onClick={handleRejectAll}>
|
||||
Reject All
|
||||
</Button>
|
||||
<Button variant="ghost" size="small" onClick={handleAcceptAll}>
|
||||
Accept All
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={handleSavePreferences}
|
||||
>
|
||||
Save Preferences
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useCookieConsent } from "../../useCookieConsent";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function useCookieSettingsModal({ onClose }: Props) {
|
||||
const { consent, handleUpdateConsent } = useCookieConsent();
|
||||
|
||||
const [analytics, setAnalytics] = useState(consent.analytics);
|
||||
const [monitoring, setMonitoring] = useState(consent.monitoring);
|
||||
|
||||
useEffect(() => {
|
||||
setAnalytics(consent.analytics);
|
||||
setMonitoring(consent.monitoring);
|
||||
}, [consent.analytics, consent.monitoring]);
|
||||
|
||||
function handleSavePreferences() {
|
||||
handleUpdateConsent({
|
||||
analytics,
|
||||
monitoring,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleAcceptAll() {
|
||||
setAnalytics(true);
|
||||
setMonitoring(true);
|
||||
handleUpdateConsent({
|
||||
analytics: true,
|
||||
monitoring: true,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleRejectAll() {
|
||||
setAnalytics(false);
|
||||
setMonitoring(false);
|
||||
handleUpdateConsent({
|
||||
analytics: false,
|
||||
monitoring: false,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
|
||||
return {
|
||||
analytics,
|
||||
setAnalytics,
|
||||
monitoring,
|
||||
setMonitoring,
|
||||
handleSavePreferences,
|
||||
handleAcceptAll,
|
||||
handleRejectAll,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
consent,
|
||||
ConsentPreferences,
|
||||
DEFAULT_CONSENT,
|
||||
} from "@/services/consent/cookies";
|
||||
|
||||
export function useCookieConsent() {
|
||||
const [consentState, setConsentState] =
|
||||
useState<ConsentPreferences>(DEFAULT_CONSENT);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = consent.load();
|
||||
setConsentState(stored);
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
const handleUpdateConsent = useCallback(
|
||||
(updates: Partial<ConsentPreferences>) => {
|
||||
const newConsent: ConsentPreferences = {
|
||||
...consentState,
|
||||
...updates,
|
||||
hasConsented: true,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setConsentState(newConsent);
|
||||
consent.save(newConsent);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
[consentState],
|
||||
);
|
||||
|
||||
const handleAcceptAll = useCallback(() => {
|
||||
handleUpdateConsent({
|
||||
analytics: true,
|
||||
monitoring: true,
|
||||
});
|
||||
}, [handleUpdateConsent]);
|
||||
|
||||
const handleRejectAll = useCallback(() => {
|
||||
handleUpdateConsent({
|
||||
analytics: false,
|
||||
monitoring: false,
|
||||
});
|
||||
}, [handleUpdateConsent]);
|
||||
|
||||
const handleResetConsent = useCallback(() => {
|
||||
setConsentState(DEFAULT_CONSENT);
|
||||
consent.save(DEFAULT_CONSENT);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
consent: consentState,
|
||||
isLoaded,
|
||||
handleUpdateConsent,
|
||||
handleAcceptAll,
|
||||
handleRejectAll,
|
||||
handleResetConsent,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCookieConsent } from "./useCookieConsent";
|
||||
|
||||
export function useCookieConsentBanner() {
|
||||
const { consent, isLoaded, handleAcceptAll, handleRejectAll } =
|
||||
useCookieConsent();
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
const shouldShowBanner = isLoaded && !consent.hasConsented;
|
||||
|
||||
function handleAcceptAllClick() {
|
||||
handleAcceptAll();
|
||||
}
|
||||
|
||||
function handleRejectAllClick() {
|
||||
handleRejectAll();
|
||||
}
|
||||
|
||||
function handleOpenSettings() {
|
||||
setShowSettings(true);
|
||||
}
|
||||
|
||||
function handleCloseSettings() {
|
||||
setShowSettings(false);
|
||||
}
|
||||
|
||||
return {
|
||||
shouldShowBanner,
|
||||
showSettings,
|
||||
handleAcceptAll: handleAcceptAllClick,
|
||||
handleRejectAll: handleRejectAllClick,
|
||||
handleOpenSettings,
|
||||
handleCloseSettings,
|
||||
};
|
||||
}
|
||||
@@ -84,15 +84,20 @@ export function DialogWrap({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className={cn("overflow-y-auto overflow-x-hidden", scrollbarStyles)}
|
||||
style={{
|
||||
scrollbarGutter: "stable",
|
||||
marginRight: hasVerticalScrollbar ? "-14px" : "0px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className={cn(
|
||||
"flex-1 overflow-y-auto overflow-x-hidden",
|
||||
scrollbarStyles,
|
||||
)}
|
||||
style={{
|
||||
scrollbarGutter: "stable",
|
||||
marginRight: hasVerticalScrollbar ? "-14px" : "0px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</RXDialog.Content>
|
||||
</RXDialog.Portal>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { consent } from "@/services/consent/cookies";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function VercelAnalyticsWrapper() {
|
||||
const [hasAnalyticsConsent, setHasAnalyticsConsent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setHasAnalyticsConsent(consent.hasConsentFor("analytics"));
|
||||
}, []);
|
||||
|
||||
if (!hasAnalyticsConsent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpeedInsights />
|
||||
<Analytics />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,9 @@
|
||||
"use client";
|
||||
|
||||
import type { GAParams } from "@/types/google";
|
||||
import { consent } from "@/services/consent/cookies";
|
||||
import Script from "next/script";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { environment } from "../environment";
|
||||
|
||||
declare global {
|
||||
@@ -29,10 +30,20 @@ export function SetupAnalytics(props: SetupProps) {
|
||||
const { gaId, debugMode, dataLayerName = "dataLayer", nonce } = ga;
|
||||
const isProductionDomain = host.includes("platform.agpt.co");
|
||||
|
||||
// Datafa.st journey analytics only on production
|
||||
const dataFastEnabled = isProductionDomain;
|
||||
// Check for user consent
|
||||
const [hasAnalyticsConsent, setHasAnalyticsConsent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check consent on mount
|
||||
setHasAnalyticsConsent(consent.hasConsentFor("analytics"));
|
||||
}, []);
|
||||
|
||||
// Datafa.st journey analytics only on production AND with consent
|
||||
const dataFastEnabled = isProductionDomain && hasAnalyticsConsent;
|
||||
// We collect analytics too for open source developers running the platform locally
|
||||
const googleAnalyticsEnabled = environment.isLocal() || isProductionDomain;
|
||||
// BUT only with consent
|
||||
const googleAnalyticsEnabled =
|
||||
(environment.isLocal() || isProductionDomain) && hasAnalyticsConsent;
|
||||
|
||||
if (currDataLayerName === undefined) {
|
||||
currDataLayerName = dataLayerName;
|
||||
|
||||
103
autogpt_platform/frontend/src/services/consent/cookies.ts
Normal file
103
autogpt_platform/frontend/src/services/consent/cookies.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { Key, storage } from "../storage/local-storage";
|
||||
|
||||
export interface ConsentPreferences {
|
||||
hasConsented: boolean;
|
||||
timestamp: number;
|
||||
analytics: boolean;
|
||||
monitoring: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONSENT: ConsentPreferences = {
|
||||
hasConsented: false,
|
||||
timestamp: Date.now(),
|
||||
analytics: false,
|
||||
monitoring: false,
|
||||
};
|
||||
|
||||
export const COOKIE_CATEGORIES = {
|
||||
essential: {
|
||||
name: "Essential Cookies",
|
||||
description: "Required for login, authentication, and core functionality",
|
||||
alwaysActive: true,
|
||||
},
|
||||
analytics: {
|
||||
name: "Analytics & Performance",
|
||||
description:
|
||||
"Help us understand how you use AutoGPT to improve our service (Google Analytics, Vercel Analytics, Datafa.st)",
|
||||
alwaysActive: false,
|
||||
},
|
||||
monitoring: {
|
||||
name: "Error Monitoring & Session Replay",
|
||||
description:
|
||||
"Record errors and user sessions to help us fix bugs faster (Sentry - includes screen recording)",
|
||||
alwaysActive: false,
|
||||
},
|
||||
} as const;
|
||||
|
||||
function load(): ConsentPreferences {
|
||||
try {
|
||||
const stored = storage.get(Key.COOKIE_CONSENT);
|
||||
if (!stored) {
|
||||
return DEFAULT_CONSENT;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(stored) as ConsentPreferences;
|
||||
|
||||
if (
|
||||
typeof parsed.hasConsented !== "boolean" ||
|
||||
typeof parsed.timestamp !== "number" ||
|
||||
typeof parsed.analytics !== "boolean" ||
|
||||
typeof parsed.monitoring !== "boolean"
|
||||
) {
|
||||
console.warn(
|
||||
"Invalid consent data in localStorage, resetting to defaults",
|
||||
);
|
||||
return DEFAULT_CONSENT;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
console.error("Failed to load consent preferences:", error);
|
||||
return DEFAULT_CONSENT;
|
||||
}
|
||||
}
|
||||
|
||||
function save(preferences: ConsentPreferences): void {
|
||||
try {
|
||||
storage.set(Key.COOKIE_CONSENT, JSON.stringify(preferences));
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
console.error("Failed to save consent preferences:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function clear(): void {
|
||||
try {
|
||||
storage.clean(Key.COOKIE_CONSENT);
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
console.error("Failed to clear consent preferences:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function hasConsented(): boolean {
|
||||
const preferences = load();
|
||||
return preferences.hasConsented;
|
||||
}
|
||||
|
||||
function hasConsentFor(
|
||||
category: keyof Omit<ConsentPreferences, "hasConsented" | "timestamp">,
|
||||
): boolean {
|
||||
const preferences = load();
|
||||
return preferences.hasConsented && preferences[category];
|
||||
}
|
||||
|
||||
export const consent = {
|
||||
load,
|
||||
save,
|
||||
clear,
|
||||
hasConsented,
|
||||
hasConsentFor,
|
||||
};
|
||||
@@ -8,6 +8,7 @@ export enum Key {
|
||||
SHEPHERD_TOUR = "shepherd-tour",
|
||||
WALLET_LAST_SEEN_CREDITS = "wallet-last-seen-credits",
|
||||
LIBRARY_AGENTS_CACHE = "library-agents-cache",
|
||||
COOKIE_CONSENT = "autogpt_cookie_consent",
|
||||
}
|
||||
|
||||
function get(key: Key) {
|
||||
|
||||
@@ -30,6 +30,19 @@ export async function createTestUser(
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Auto-accept cookies in test environment to prevent banner from appearing
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem(
|
||||
"autogpt_cookie_consent",
|
||||
JSON.stringify({
|
||||
hasConsented: true,
|
||||
timestamp: Date.now(),
|
||||
analytics: true,
|
||||
monitoring: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
const testUser = await signupTestUser(
|
||||
page,
|
||||
|
||||
Reference in New Issue
Block a user