'use client'
import { useFeatureFlags } from 'common'
import { Copy, EyeOff, Search, X } from 'lucide-react'
import Image from 'next/image'
import {
useCallback,
useEffect,
useState,
type ChangeEvent,
type Dispatch,
type SetStateAction,
} from 'react'
import {
Badge,
Button,
cn,
Input,
InputGroup,
InputGroupAddon,
InputGroupInput,
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
Switch,
Tabs_Shadcn_ as Tabs,
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { useDevToolbar } from './DevToolbarContext'
import type { DevTelemetryEvent, ExtraTab } from './types'
import {
CC_ORIGINALS_KEY,
deleteCookie,
getCookie,
parseOverrideValue,
PH_ORIGINALS_KEY,
readOriginals,
safeJsonParse,
setCookie,
valuesAreEqual,
writeOriginals,
} from './utils'
// Duplicated for tree-shaking — bundler must see literal process.env reference.
// Keep in sync: index.ts, DevToolbarContext.tsx, DevToolbarTrigger.tsx, feature-flags.tsx
const env = process.env.NEXT_PUBLIC_ENVIRONMENT
const IS_TOOLBAR_ENABLED = env === 'local' || env === 'staging'
const IS_LOCAL_DEV = env === 'local'
function EventRow({ event }: { event: DevTelemetryEvent }) {
const [isExpanded, setIsExpanded] = useState(false)
const time = new Date(event.timestamp).toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
const handleCopy = (e: React.MouseEvent) => {
e.stopPropagation()
navigator.clipboard
.writeText(JSON.stringify({ name: event.eventName, properties: event.properties }, null, 2))
.catch((error) => console.warn('Copy failed', error))
}
return (
Copy JSON
{isExpanded && (
{JSON.stringify(event.properties, null, 2)}
)}
)
}
function FlagRow({
flagName,
currentValue,
originalValue,
isOverridden,
onToggle,
}: {
flagName: string
currentValue: unknown
originalValue: unknown
isOverridden: boolean
onToggle: (value: unknown) => void
}) {
const valueType = typeof originalValue
const isNull = originalValue === null
const inputProps = {
size: 'tiny' as const,
value: String(currentValue),
onChange: (event: ChangeEvent) => onToggle(event.target.value),
className: 'w-32',
}
return (
)
}
export function DevToolbar({ extraTabs = [] }: { extraTabs?: ExtraTab[] }) {
const { isEnabled, isOpen, setIsOpen, events, setEvents, dismissToolbar } = useDevToolbar()
const [activeTab, setActiveTab] = useState('events')
const [flagsSubTab, setFlagsSubTab] = useState<'posthog' | 'configcat'>('posthog')
const [eventFilter, setEventFilter] = useState('')
const { posthog: posthogFlags, configcat: configcatFlags } = useFeatureFlags()
const [phFlagOverrides, setPhFlagOverrides] = useState>({})
const [ccFlagOverrides, setCcFlagOverrides] = useState>({})
const [phFlagOriginals, setPhFlagOriginals] = useState>({})
const [ccFlagOriginals, setCcFlagOriginals] = useState>({})
const loadOverrides = useCallback(
(cookieName: string, label: string, setter: (value: Record) => void) => {
const parsed = safeJsonParse>(getCookie(cookieName), {}, label)
if (Object.keys(parsed).length > 0) {
setter(parsed)
}
},
[]
)
const saveOverrides = useCallback(
(
cookieName: string,
overrides: Record,
setter: (value: Record) => void
) => {
setter(overrides)
if (Object.keys(overrides).length > 0) {
setCookie(cookieName, JSON.stringify(overrides), '/')
} else {
deleteCookie(cookieName)
}
},
[]
)
const updateOriginals = useCallback(
(
storageKey: typeof PH_ORIGINALS_KEY | typeof CC_ORIGINALS_KEY,
setter: Dispatch>>
) =>
(updater: (prev: Record) => Record) => {
setter((prev) => {
const next = updater(prev)
writeOriginals(storageKey, next)
return next
})
},
[]
)
useEffect(() => {
loadOverrides('x-ph-flag-overrides', 'PostHog flag overrides', setPhFlagOverrides)
loadOverrides('x-cc-flag-overrides', 'ConfigCat flag overrides', setCcFlagOverrides)
}, [loadOverrides])
useEffect(() => {
setPhFlagOriginals(readOriginals(PH_ORIGINALS_KEY))
setCcFlagOriginals(readOriginals(CC_ORIGINALS_KEY))
}, [])
useEffect(() => {
const STYLE_ID = 'dev-toolbar-hide-native-devtools'
const existing = document.getElementById(STYLE_ID)
if (isOpen) {
if (!existing) {
const style = document.createElement('style')
style.id = STYLE_ID
style.textContent = `
.tsqd-open-btn, .tsqd-open-btn-container { display: none !important; }
`
document.head.appendChild(style)
}
} else {
existing?.remove()
}
}, [isOpen])
const updatePhOriginals = updateOriginals(PH_ORIGINALS_KEY, setPhFlagOriginals)
const updateCcOriginals = updateOriginals(CC_ORIGINALS_KEY, setCcFlagOriginals)
const togglePhFlagOverride = (flagName: string, value: unknown) => {
const originalValue = phFlagOriginals[flagName] ?? posthogFlags[flagName]
const parsedValue = parseOverrideValue(value, originalValue)
if (valuesAreEqual(parsedValue, originalValue)) {
const newOverrides = { ...phFlagOverrides }
delete newOverrides[flagName]
saveOverrides('x-ph-flag-overrides', newOverrides, setPhFlagOverrides)
return
}
updatePhOriginals((prev) =>
flagName in prev ? prev : { ...prev, [flagName]: posthogFlags[flagName] }
)
const newOverrides = { ...phFlagOverrides, [flagName]: parsedValue }
saveOverrides('x-ph-flag-overrides', newOverrides, setPhFlagOverrides)
}
const toggleCcFlagOverride = (flagName: string, value: unknown) => {
const originalValue = ccFlagOriginals[flagName] ?? configcatFlags[flagName]
const parsedValue = parseOverrideValue(value, originalValue)
if (valuesAreEqual(parsedValue, originalValue)) {
const newOverrides = { ...ccFlagOverrides }
delete newOverrides[flagName]
saveOverrides('x-cc-flag-overrides', newOverrides, setCcFlagOverrides)
return
}
updateCcOriginals((prev) =>
flagName in prev ? prev : { ...prev, [flagName]: configcatFlags[flagName] }
)
const newOverrides = { ...ccFlagOverrides, [flagName]: parsedValue }
saveOverrides('x-cc-flag-overrides', newOverrides, setCcFlagOverrides)
}
const clearAllOverrides = () => {
setPhFlagOverrides({})
setCcFlagOverrides({})
setPhFlagOriginals({})
setCcFlagOriginals({})
deleteCookie('x-ph-flag-overrides')
deleteCookie('x-cc-flag-overrides')
writeOriginals(PH_ORIGINALS_KEY, {})
writeOriginals(CC_ORIGINALS_KEY, {})
window.location.reload()
}
const normalizedFilter = eventFilter.trim().toLowerCase()
const filteredEvents = (
normalizedFilter
? events.filter(
(e) =>
e.eventName.toLowerCase().includes(normalizedFilter) ||
e.eventType.toLowerCase().includes(normalizedFilter)
)
: events
)
.slice()
.sort((a, b) => b.timestamp - a.timestamp)
const phOverrideCount = Object.keys(phFlagOverrides).length
const ccOverrideCount = Object.keys(ccFlagOverrides).length
const totalOverrideCount = phOverrideCount + ccOverrideCount
if (!IS_TOOLBAR_ENABLED || !isEnabled) return null
return (
event.preventDefault()}
>
Dev Toolbar
View telemetry events and feature flags for local development
Events ({filteredEvents.length})
Flags {totalOverrideCount > 0 && `(${totalOverrideCount})`}
{extraTabs.map((tab) => (
{tab.label}
))}
}
onClick={dismissToolbar}
className="text-foreground-light hover:text-foreground p-1"
/>
Hide Dev Toolbar
}
className="text-foreground-light hover:text-foreground p-1"
/>
Close
{activeTab === 'events' && (
setEventFilter(e.target.value)}
/>
{!IS_LOCAL_DEV && (
Server-side events are only visible when using the toolbar in local development
)}
{filteredEvents.length === 0 ? (
No events yet. Interact with the app to see telemetry events.
) : (
{filteredEvents.map((event) => (
))}
)}
)}
{activeTab === 'flags' && (
{/* Sidebar */}
{totalOverrideCount > 0 && (
)}
{/* Flag list */}
{flagsSubTab === 'posthog' &&
(Object.keys(posthogFlags).length === 0 ? (
No PostHog feature flags loaded yet.
) : (
Object.entries(posthogFlags).map(([flagName, flagValue]) => (
togglePhFlagOverride(flagName, value)}
/>
))
))}
{flagsSubTab === 'configcat' &&
(Object.keys(configcatFlags).length === 0 ? (
No ConfigCat feature flags loaded yet.
) : (
Object.entries(configcatFlags).map(([flagName, flagValue]) => (
toggleCcFlagOverride(flagName, value)}
/>
))
))}
)}
{extraTabs.map((tab) => (
{tab.content}
))}
)
}