From 7e8955f3735e56d45a27bbb3190cad65589da6ca Mon Sep 17 00:00:00 2001 From: Radoslaw Olejniczak Date: Sat, 25 Oct 2025 17:50:42 +0200 Subject: [PATCH 1/3] onetrust gdpr cookie consent banner --- components/navigation/cookiePolicyButton.js | 11 + components/navigation/footer.js | 14 +- components/utilities/cookieSettingsModal.js | 73 ------ .../utilities/cookieSettingsModal.module.css | 167 -------------- components/utilities/gdpr.js | 217 ------------------ components/utilities/gdpr.module.css | 42 ---- content/cookie-settings.md | 21 -- content/gdpr-banner.md | 8 - netlify.toml | 6 + next-sitemap.config.js | 9 +- next.config.mjs | 6 + pages/404.js | 13 ++ pages/[...slug].js | 58 ++--- pages/index.js | 60 ++--- public/scripts/segment.js | 63 +++++ styles/cookie-banner.scss | 148 ++++++++++++ styles/main.scss | 1 + 17 files changed, 285 insertions(+), 632 deletions(-) create mode 100644 components/navigation/cookiePolicyButton.js delete mode 100644 components/utilities/cookieSettingsModal.js delete mode 100644 components/utilities/cookieSettingsModal.module.css delete mode 100644 components/utilities/gdpr.js delete mode 100644 components/utilities/gdpr.module.css delete mode 100644 content/cookie-settings.md delete mode 100644 content/gdpr-banner.md create mode 100644 public/scripts/segment.js create mode 100644 styles/cookie-banner.scss diff --git a/components/navigation/cookiePolicyButton.js b/components/navigation/cookiePolicyButton.js new file mode 100644 index 000000000..3ffd3cc7f --- /dev/null +++ b/components/navigation/cookiePolicyButton.js @@ -0,0 +1,11 @@ +const CookiePolicyButton = ({ children, className, ...props }) => { + const classes = `ot-sdk-show-settings${className ? ` ${className}` : ""}`; + + return ( + + ); +}; + +export default CookiePolicyButton; diff --git a/components/navigation/footer.js b/components/navigation/footer.js index 474b8e874..b43d84c23 100644 --- a/components/navigation/footer.js +++ b/components/navigation/footer.js @@ -1,8 +1,13 @@ import Link from "next/link"; +import dynamic from "next/dynamic"; import styles from "./footer.module.css"; -const Footer = ({ setIsTelemetryModalVisible }) => { +const CookiePolicyButton = dynamic(() => import("./cookiePolicyButton"), { + ssr: false, +}); + +const Footer = () => { return ( diff --git a/components/utilities/cookieSettingsModal.js b/components/utilities/cookieSettingsModal.js deleted file mode 100644 index 25536dfae..000000000 --- a/components/utilities/cookieSettingsModal.js +++ /dev/null @@ -1,73 +0,0 @@ -import classNames from "classnames"; - -import content from "../../content/cookie-settings.md"; - -import styles from "./cookieSettingsModal.module.css"; - -export default function CookieSettingsModal({ - setIsTelemetryModalVisible, - declineTelemetryAndCloseBanner, - allowTelemetryAndCloseBanner, -}) { - return ( -
-
- -
- - -
-
- ); -} diff --git a/components/utilities/cookieSettingsModal.module.css b/components/utilities/cookieSettingsModal.module.css deleted file mode 100644 index 825ba029a..000000000 --- a/components/utilities/cookieSettingsModal.module.css +++ /dev/null @@ -1,167 +0,0 @@ -.Container > div { - @apply text-gray-90 grid grid-cols-1 gap-4 text-base leading-loose; -} - -.Container h1, -.Container h2, -.Container h3, -.Container h4, -.Container h5, -.Container h6 { - @apply scroll-mt-0 md:scroll-mt-32; -} - -.Container h2, -.Container h3, -.Container h4, -.Container h5 { - @apply mt-4 text-lg font-bold text-gray-90; -} - -.Container h2 { - @apply text-xl; -} - -.Container h3 { - @apply text-2xl; -} - -.Container h2:first-child, -.Container h3:first-child, -.Container h4:first-child, -.Container h5:first-child { - @apply mt-0; -} - -.Container p { - @apply text-gray-90; -} - -.Container a { - @apply underline; -} - -.Container a:hover { - @apply opacity-80; -} - -.Container ol { - @apply list-decimal pl-8 grid grid-cols-1 gap-4; -} - -.Container section ol { - @apply list-none pl-0 gap-0 mb-5; -} - -.Container ul { - @apply list-disc pl-8 grid grid-cols-1 gap-4; -} - -.Container li { - @apply pl-2; -} - -.Container blockquote { - @apply p-6 md:p-8 lg:p-16 - -mx-6 md:-mx-8 lg:-mx-16 - my-0 md:my-4 lg:my-12 - grid grid-cols-1 gap-4 - bg-lightBlue-10 - text-gray-80 - rounded-lg; -} - -.PrivacyContainer blockquote ~ h4 { - @apply mt-12 md:mt-8 lg:mt-0 text-gray-90; -} - -.PrivacyContainer blockquote h2 { - @apply hidden; -} - -.Container blockquote h2 { - @apply text-gray-90 text-3xl; -} - -.Container hr { - @apply border-gray-40 border-t-0 border-l-0 border-r-0 border-b my-8; -} - -.Container address { - @apply whitespace-pre-line pl-4 border-l-2 border-gray-30 mt-4; -} - -.Container pre { - @apply bg-gray-10 p-4 rounded-md overflow-x-auto text-gray-90; -} - -.Container pre code { - @apply text-lg; -} - -.Container figure { - @apply flex flex-col items-start; -} - -.Container figure img { - @apply h-24 ml-2 mb-2; -} - -.Container figure figcaption { - @apply text-sm text-gray-70; -} - -/* For table in Privacy notice */ -.Container table { - @apply table-fixed my-8 w-full; -} - -.Container table thead tr th { - @apply px-4 py-3 text-left bg-gray-20 bg-opacity-50 border border-gray-30 text-gray-90 font-semibold text-lg leading-normal align-top; -} - -.Container table tbody tr td { - @apply px-4 py-3 border border-gray-30 text-gray-90 text-lg leading-normal align-top; -} - -.DeploymentTerms ul, -.DeploymentTerms ol { - @apply list-none; -} - -/* Specific classes for colors */ -.Container:global(.red) a { - @apply text-red-70; -} - -.Container:global(.orange) a { - @apply text-orange-70; -} - -.Container:global(.yellow) a { - @apply text-yellow-90; -} - -.Container:global(.green) a { - @apply text-green-70; -} - -.Container:global(.acqua) a { - @apply text-acqua-70; -} - -.Container:global(.lightBlue) a { - @apply text-lightBlue-70; -} - -.Container:global(.darkBlue) a { - @apply text-darkBlue-70; -} - -.Container:global(.indigo) a { - @apply text-indigo-70; -} - -.Container:global(.gray) a { - @apply text-gray-70; -} diff --git a/components/utilities/gdpr.js b/components/utilities/gdpr.js deleted file mode 100644 index 110ab94f1..000000000 --- a/components/utilities/gdpr.js +++ /dev/null @@ -1,217 +0,0 @@ -import React, { useEffect } from "react"; -import classNames from "classnames"; - -import content from "../../content/gdpr-banner.md"; - -import styles from "./gdpr.module.css"; - -const TELEMETRY_PREFERENCE = "InsertTelemetry"; -const TELEMETRY_PREFERENCE_DATE = "TelemetryDate"; - -export function setTelemetryPreference(accepted) { - localStorage.setItem(TELEMETRY_PREFERENCE, JSON.stringify(accepted)); - localStorage.setItem(TELEMETRY_PREFERENCE_DATE, JSON.stringify(Date.now())); -} - -// Check if the stored date is > 6 months old -const isConsentStale = (timestamp) => { - const consent_date = new Date(parseInt(timestamp * 1000)); - const today = new Date(); - - const six_months_in_ms = - 1000 /*ms*/ * 60 /*s*/ * 60 /*min*/ * 24 /*h*/ * 30 /*days*/ * 6; /*months*/ - return today - consent_date > six_months_in_ms; -}; - -function getTelemetryPreference() { - // Returns true/false if user accepted/denied telemetry. - // Returns null if user never accepted/denied or consent is stale. - - const telemetryPref = localStorage.getItem(TELEMETRY_PREFERENCE); - const consentIsStale = isConsentStale( - localStorage.getItem(TELEMETRY_PREFERENCE_DATE), - ); - - if (telemetryPref == null || consentIsStale) return null; - - return telemetryPref == "true"; -} - -export default function GDPRBanner({ - isTelemetryModalVisible, - setIsTelemetryModalVisible, - isTelemetryBannerVisible, - setIsTelemetryBannerVisible, - insertTelemetryCode, - setInsertTelemetryCode, - allowTelemetryAndCloseBanner, - declineTelemetryAndCloseBanner, -}) { - useEffect(() => { - const pref = getTelemetryPreference(); - - switch (pref) { - case true: - setInsertTelemetryCode(true); - return; - - case false: - // This is already false at initialization, but it doesn't hurt to do the right thing - // here and make sure it's indeed false. - setInsertTelemetryCode(false); - return; - - case null: - localStorage.clear(); // Do we even need this line?? Seems dangerous to just clear the entire localStorage, as maybe some other library could be using it. - setIsTelemetryBannerVisible(true); - return; - - default: - console.log(`Unexpected telemetry preference: ${pref}`); - return; - } - }, []); - - useEffect(() => { - if (insertTelemetryCode) { - insertTelemetry(); - } - }, [insertTelemetryCode]); - - return ( - <> - {isTelemetryBannerVisible && ( -
-
-
-
- - - -
-
-
- )} - - ); -} - -function insertTelemetry() { - (function () { - var analytics = (window.analytics = window.analytics || []); - if (!analytics.initialize) - if (analytics.invoked) - window.console && - console.error && - console.error("Segment snippet included twice."); - else { - analytics.invoked = !0; - analytics.methods = [ - "trackSubmit", - "trackClick", - "trackLink", - "trackForm", - "pageview", - "identify", - "reset", - "group", - "track", - "ready", - "alias", - "debug", - "page", - "once", - "off", - "on", - "addSourceMiddleware", - "addIntegrationMiddleware", - "setAnonymousId", - "addDestinationMiddleware", - ]; - analytics.factory = function (e) { - return function () { - var t = Array.prototype.slice.call(arguments); - t.unshift(e); - analytics.push(t); - return analytics; - }; - }; - for (var e = 0; e < analytics.methods.length; e++) { - var key = analytics.methods[e]; - analytics[key] = analytics.factory(key); - } - analytics.load = function (key, e) { - var t = document.createElement("script"); - t.type = "text/javascript"; - t.async = !0; - t.src = - "https://cdn.segment.com/analytics.js/v1/" + - key + - "/analytics.min.js"; - var n = document.getElementsByTagName("script")[0]; - n.parentNode.insertBefore(t, n); - analytics._loadOptions = e; - }; - analytics._writeKey = "pUoB6ihRTAFLDtLp2NWEuJvBNtiooQwE"; - analytics.SNIPPET_VERSION = "4.13.2"; - analytics.load("pUoB6ihRTAFLDtLp2NWEuJvBNtiooQwE"); - analytics.page(); - } - })(); -} diff --git a/components/utilities/gdpr.module.css b/components/utilities/gdpr.module.css deleted file mode 100644 index 78d5d8bb3..000000000 --- a/components/utilities/gdpr.module.css +++ /dev/null @@ -1,42 +0,0 @@ -.Container { - backdrop-filter: blur(10px); - background: #fffd; -} - -.Markdown div { - @apply flex flex-col; -} - -.Markdown h1 { - @apply text-gray-90 text-lg sm:text-xl font-bold mb-1; -} - -.Markdown p { - @apply text-gray-90 text-base mb-0; -} - -.Markdown p a { - @apply text-gray-90 underline hover:text-gray-70; -} - -:global(.dark) .Container { - @apply bg-gray-100 border-gray-90; -} - -:global(.dark) .Markdown p, -:global(.dark) .Markdown p a { - @apply text-gray-40; -} - -:global(.dark) .Link, -:global(.dark) .Button { - @apply text-gray-40; -} - -:global(.dark) .Button { - @apply border-gray-80; -} - -:global(.dark) .Markdown h1 { - @apply text-gray-40; -} diff --git a/content/cookie-settings.md b/content/cookie-settings.md deleted file mode 100644 index b422adc73..000000000 --- a/content/cookie-settings.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -visible: false ---- - -### Cookie settings - -##### Strictly necessary cookies - -These cookies are necessary for the website to function and cannot be switched off. They are usually only set in response to actions made by you which amount to a request for services, such as setting your privacy preferences, logging in or filling in forms. - -##### Performance cookies - -These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site. They help us understand how visitors move around the site and which pages are most frequently visited. - -##### Functional cookies - -These cookies are used to record your choices and settings, maintain your preferences over time and recognize you when you return to our website. These cookies help us to personalize our content for you and remember your preferences. - -##### Targeting cookies - -These cookies may be deployed to our site by our advertising partners to build a profile of your interest and provide you with content that is relevant to you, including showing you relevant ads on other websites. diff --git a/content/gdpr-banner.md b/content/gdpr-banner.md deleted file mode 100644 index af0887a47..000000000 --- a/content/gdpr-banner.md +++ /dev/null @@ -1,8 +0,0 @@ ---- ---- - -# Hello there 👋 - -Thanks for stopping by! We use cookies to help us understand how you interact with our website. - -By clicking “Accept all”, you consent to our use of cookies. For more information, please see our [privacy policy](www.streamlit.io/privacy-policy). diff --git a/netlify.toml b/netlify.toml index 515983015..0de4b8bec 100644 --- a/netlify.toml +++ b/netlify.toml @@ -30,6 +30,9 @@ connect-src \ https://*.algolia.net/ \ https://*.algolianet.com/ \ https://kapa-widget-proxy-la7dkmplpq-uc.a.run.app/ \ + https://cdn.cookielaw.org/ \ + https://cookie-cdn.cookiepro.com/ \ + https://cdn.jsdelivr.net/npm/@segment/ \ ; \ default-src 'none' ; \ font-src 'self' ; \ @@ -62,6 +65,9 @@ script-src \ https://www.google.com/recaptcha/api.js \ https://www.gstatic.com/recaptcha/releases/ \ https://www.google.com/recaptcha/enterprise.js \ + https://cdn.cookielaw.org/ \ + https://cookie-cdn.cookiepro.com/ \ + https://cdn.jsdelivr.net/npm/@segment/ \ ; \ style-src \ 'self' \ diff --git a/next-sitemap.config.js b/next-sitemap.config.js index 2d51e354e..aeab62695 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -1,12 +1,5 @@ module.exports = { siteUrl: process.env.NEXT_PUBLIC_HOSTNAME || "https://docs.streamlit.io", generateRobotsTxt: false, - exclude: [ - "/menu", - "/.keep", - "/style-guide", - "/gdpr-banner", - "/index", - "/develop", - ], + exclude: ["/menu", "/.keep", "/style-guide", "/index", "/develop"], }; diff --git a/next.config.mjs b/next.config.mjs index 14c2bfa4c..c048258e9 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -49,6 +49,9 @@ const CSP_HEADER = [ "https://www.google.com/recaptcha/api.js", // Recaptcha for Kapa.ai "https://www.gstatic.com/recaptcha/releases/", // Recaptchas for Kapa.ai "https://www.google.com/recaptcha/enterprise.js", // Recaptchas for Kapa.ai + "https://cdn.cookielaw.org/", // Onetrust cookie banner + "https://cookie-cdn.cookiepro.com/", // Onetrust cookie banner + "https://cdn.jsdelivr.net/npm/@segment/", // Segment Onetrust Wrapper ";", "default-src 'none';", "font-src 'self';", @@ -82,6 +85,9 @@ const CSP_HEADER = [ "https://www.google.com/recaptcha/api.js", // Recaptcha for Kapa.ai "https://www.gstatic.com/recaptcha/releases/", // Recaptchas for Kapa.ai "https://www.google.com/recaptcha/enterprise.js", // Recaptchas for Kapa.ai + "https://cdn.cookielaw.org/", // Onetrust cookie banner + "https://cookie-cdn.cookiepro.com/", // Onetrust cookie banner + "https://cdn.jsdelivr.net/npm/@segment/", // Segment Onetrust Wrapper ";", "style-src", "'self'", diff --git a/pages/404.js b/pages/404.js index 477713d4c..b1d8696ae 100644 --- a/pages/404.js +++ b/pages/404.js @@ -41,6 +41,19 @@ export default function Home({ window, menu }) { name="twitter:image" content={`https://${process.env.NEXT_PUBLIC_HOSTNAME}/sharing-image-twitter.jpg`} /> + + + {/* Add Segment's OneTrust Consent Wrapper */} + + {/* Add Segment Analytics Snippet */} +
diff --git a/pages/[...slug].js b/pages/[...slug].js index a89fb3407..a92158cc3 100644 --- a/pages/[...slug].js +++ b/pages/[...slug].js @@ -18,10 +18,6 @@ const { serverRuntimeConfig, publicRuntimeConfig } = getConfig(); // Site Components import { looksLikeVersionAndPlatformString } from "../lib/next/utils"; -import CookieSettingsModal from "../components/utilities/cookieSettingsModal"; -import GDPRBanner, { - setTelemetryPreference, -} from "../components/utilities/gdpr"; import { getArticleSlugs, getArticleSlugFromString, @@ -115,28 +111,6 @@ export default function Article({ let versionWarning; let currentLink; - const [isTelemetryModalVisible, setIsTelemetryModalVisible] = useState(false); - const [isTelemetryBannerVisible, setIsTelemetryBannerVisible] = - useState(false); - const [insertTelemetryCode, setInsertTelemetryCode] = useState(false); - - const allowTelemetryAndCloseBanner = useCallback(() => { - setIsTelemetryBannerVisible(false); - setIsTelemetryModalVisible(false); - setInsertTelemetryCode(true); - setTelemetryPreference(true); - }, [isTelemetryBannerVisible, insertTelemetryCode]); - - const declineTelemetryAndCloseBanner = useCallback(() => { - setIsTelemetryBannerVisible(false); - setIsTelemetryModalVisible(false); - setInsertTelemetryCode(false); - setTelemetryPreference(false); - - // If previous state was true, and now it's false, reload the page to remove telemetry JS - if (insertTelemetryCode) router.reload(); - }, [isTelemetryBannerVisible, insertTelemetryCode]); - const { version, platform, goToLatest, goToOpenSource } = useVersionContext(); const isVersionedPage = currMenuItem && currMenuItem.isVersioned; const isUnversionedURL = !versionFromSlug || !platformFromSlug; @@ -263,23 +237,6 @@ export default function Article({ }} > - {isTelemetryModalVisible && ( - - )} -
@@ -329,6 +286,19 @@ export default function Article({ name="twitter:image" content={`https://${process.env.NEXT_PUBLIC_HOSTNAME}/sharing-image-twitter.jpg`} /> + + + {/* Add Segment's OneTrust Consent Wrapper */} + + {/* Add Segment Analytics Snippet */} +
{versionWarning} @@ -345,7 +315,7 @@ export default function Article({
-