Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed apps/web-docs/.gitkeep
Empty file.
1 change: 0 additions & 1 deletion apps/web-docs/README.md

This file was deleted.

2 changes: 2 additions & 0 deletions apps/web-roo-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
"next-themes": "^0.4.6",
"posthog-js": "^1.248.1",
"react": "^18.3.1",
"react-cookie-consent": "^9.0.0",
"react-dom": "^18.3.1",
"react-icons": "^5.5.0",
"recharts": "^2.15.3",
"tailwind-merge": "^3.3.0",
"tailwindcss-animate": "^1.0.7",
"tldts": "^6.1.86",
"zod": "^3.25.61"
},
"devDependencies": {
Expand Down
13 changes: 2 additions & 11 deletions apps/web-roo-code/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import Script from "next/script"
import { SEO } from "@/lib/seo"
import { CookieConsentWrapper } from "@/components/CookieConsentWrapper"

import { Providers } from "@/components/providers"

Expand Down Expand Up @@ -93,22 +93,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
/>
</head>
<body className={inter.className}>
{/* Google tag (gtag.js) */}
<Script src="https://www.googletagmanager.com/gtag/js?id=AW-17391954825" strategy="afterInteractive" />
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'AW-17391954825');
`}
</Script>
<div itemScope itemType="https://schema.org/WebSite">
<link itemProp="url" href={SEO.url} />
<meta itemProp="name" content={SEO.name} />
</div>
<Providers>
<Shell>{children}</Shell>
<CookieConsentWrapper />
</Providers>
</body>
</html>
Expand Down
111 changes: 111 additions & 0 deletions apps/web-roo-code/src/components/CookieConsentWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"use client"

import React, { useState, useEffect } from "react"
import ReactCookieConsent from "react-cookie-consent"
import { Cookie } from "lucide-react"
import { getDomain } from "tldts"
import { CONSENT_COOKIE_NAME } from "@roo-code/types"
import { dispatchConsentEvent } from "@/lib/analytics/consent-manager"

/**
* GDPR-compliant cookie consent banner component
* Handles both the UI and consent event dispatching
*/
export function CookieConsentWrapper() {
const [cookieDomain, setCookieDomain] = useState<string | null>(null)

useEffect(() => {
// Get the appropriate domain using tldts
if (typeof window !== "undefined") {
const domain = getDomain(window.location.hostname)
setCookieDomain(domain)
}
}, [])

const handleAccept = () => {
dispatchConsentEvent(true)
}

const handleDecline = () => {
dispatchConsentEvent(false)
}

const extraCookieOptions = cookieDomain
? {
domain: cookieDomain,
}
: {}

const containerClasses = `
fixed bottom-2 left-2 right-2 z-[999]
bg-black/95 dark:bg-white/95
text-white dark:text-black
border-t-neutral-800 dark:border-t-gray-200
backdrop-blur-xl
border-t
font-semibold
rounded-t-lg
px-4 py-4 md:px-8 md:py-4
flex flex-wrap items-center justify-between gap-4
text-sm font-sans
`.trim()

const buttonWrapperClasses = `
flex
flex-row-reverse
items-center
gap-2
`.trim()

const acceptButtonClasses = `
bg-white text-black border-neutral-800
dark:bg-black dark:text-white dark:border-gray-200
hover:opacity-50
transition-opacity
rounded-md
px-4 py-2 mr-2
text-sm font-bold
cursor-pointer
focus:outline-none focus:ring-2 focus:ring-offset-2
`.trim()

const declineButtonClasses = `
dark:bg-white dark:text-black dark:border-gray-200
bg-black text-white border-neutral-800
hover:opacity-50
border border-border
transition-opacity
rounded-md
px-4 py-2
text-sm font-bold
cursor-pointer
focus:outline-none focus:ring-2 focus:ring-offset-2
`.trim()

return (
<div role="banner" aria-label="Cookie consent banner" aria-live="polite">
<ReactCookieConsent
location="bottom"
buttonText="Accept"
declineButtonText="Decline"
cookieName={CONSENT_COOKIE_NAME}
expires={365}
enableDeclineButton={true}
onAccept={handleAccept}
onDecline={handleDecline}
containerClasses={containerClasses}
buttonClasses={acceptButtonClasses}
buttonWrapperClasses={buttonWrapperClasses}
declineButtonClasses={declineButtonClasses}
extraCookieOptions={extraCookieOptions}
disableStyles={true}
ariaAcceptLabel={`Accept`}
ariaDeclineLabel={`Decline`}>
<div className="flex items-center gap-2">
<Cookie className="size-5 hidden md:block" />
<span>Like most of the internet, we use cookies. Are you OK with that?</span>
</div>
</ReactCookieConsent>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use client"

import { useEffect, useState } from "react"
import Script from "next/script"
import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"

// Google Tag Manager ID
const GTM_ID = "AW-17391954825"

/**
* Google Analytics Provider
* Only loads Google Tag Manager after user gives consent
*/
export function GoogleAnalyticsProvider({ children }: { children: React.ReactNode }) {
const [shouldLoad, setShouldLoad] = useState(false)

useEffect(() => {
// Check initial consent status
if (hasConsent()) {
setShouldLoad(true)
initializeGoogleAnalytics()
}

// Listen for consent changes
const unsubscribe = onConsentChange((consented) => {
if (consented && !shouldLoad) {
setShouldLoad(true)
initializeGoogleAnalytics()
}
})

return unsubscribe
}, [shouldLoad])

const initializeGoogleAnalytics = () => {
// Initialize the dataLayer and gtag function
if (typeof window !== "undefined") {
window.dataLayer = window.dataLayer || []
window.gtag = function (...args: GtagArgs) {
window.dataLayer.push(args)
}
window.gtag("js", new Date())
window.gtag("config", GTM_ID)
}
}

// Only render Google Analytics scripts if consent is given
if (!shouldLoad) {
return <>{children}</>
}

return (
<>
{/* Google tag (gtag.js) - Only loads after consent */}
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GTM_ID}`}
strategy="afterInteractive"
onLoad={() => {
console.log("Google Analytics loaded with consent")
}}
/>
<Script id="google-analytics-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GTM_ID}');
`}
</Script>
{children}
</>
)
}

// Type definitions for Google Analytics
type GtagArgs = ["js", Date] | ["config", string, GtagConfig?] | ["event", string, GtagEventParameters?]

interface GtagConfig {
[key: string]: unknown
}

interface GtagEventParameters {
[key: string]: unknown
}

// Declare global types for TypeScript
declare global {
interface Window {
dataLayer: GtagArgs[]
gtag: (...args: GtagArgs) => void
}
}
62 changes: 42 additions & 20 deletions apps/web-roo-code/src/components/providers/posthog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
import { usePathname, useSearchParams } from "next/navigation"
import posthog from "posthog-js"
import { PostHogProvider as OriginalPostHogProvider } from "posthog-js/react"
import { useEffect, Suspense } from "react"
import { useEffect, Suspense, useState } from "react"
import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"

// Create a separate component for analytics tracking that uses useSearchParams
function PageViewTracker() {
const pathname = usePathname()
const searchParams = useSearchParams()

// Track page views
useEffect(() => {
// Only track page views if PostHog is properly initialized
if (pathname && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
let url = window.location.origin + pathname
if (searchParams && searchParams.toString()) {
Expand All @@ -29,8 +28,10 @@ function PageViewTracker() {
}

export function PostHogProvider({ children }: { children: React.ReactNode }) {
const [isInitialized, setIsInitialized] = useState(false)

useEffect(() => {
// Initialize PostHog only on the client side
// Initialize PostHog only on the client side AND when consent is given
if (typeof window !== "undefined") {
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST
Expand All @@ -51,27 +52,48 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
)
}

posthog.init(posthogKey, {
api_host: posthogHost || "https://us.i.posthog.com",
capture_pageview: false, // We'll handle this manually
loaded: (posthogInstance) => {
if (process.env.NODE_ENV === "development") {
// Log to console in development
posthogInstance.debug()
}
},
respect_dnt: true, // Respect Do Not Track
const initializePosthog = () => {
if (!isInitialized) {
posthog.init(posthogKey, {
api_host: posthogHost || "https://us.i.posthog.com",
capture_pageview: false,
loaded: (posthogInstance) => {
if (process.env.NODE_ENV === "development") {
posthogInstance.debug()
}
},
respect_dnt: true, // Respect Do Not Track
})
setIsInitialized(true)
}
}

// Check initial consent status
if (hasConsent()) {
initializePosthog()
}

// Listen for consent changes
const unsubscribe = onConsentChange((consented) => {
if (consented && !isInitialized) {
initializePosthog()
}
})
}

// No explicit cleanup needed for posthog-js v1.231.0
}, [])
return () => {
unsubscribe()
}
}
}, [isInitialized])

// Only provide PostHog context if it's initialized
return (
<OriginalPostHogProvider client={posthog}>
<Suspense fallback={null}>
<PageViewTracker />
</Suspense>
{isInitialized && (
<Suspense fallback={null}>
<PageViewTracker />
</Suspense>
)}
{children}
</OriginalPostHogProvider>
)
Expand Down
13 changes: 8 additions & 5 deletions apps/web-roo-code/src/components/providers/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ThemeProvider } from "next-themes"

import { PostHogProvider } from "./posthog-provider"
import { GoogleAnalyticsProvider } from "./google-analytics-provider"

const queryClient = new QueryClient()

export const Providers = ({ children }: { children: React.ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>
<PostHogProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
{children}
</ThemeProvider>
</PostHogProvider>
<GoogleAnalyticsProvider>
<PostHogProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
{children}
</ThemeProvider>
</PostHogProvider>
</GoogleAnalyticsProvider>
</QueryClientProvider>
)
}
Loading
Loading