'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 (
{flagName} {isOverridden && ( Overridden )} {isNull && ( null )}
Original:{' '} {JSON.stringify(originalValue)}
{isNull ? ( ) : valueType === 'boolean' ? ( onToggle(checked)} /> ) : valueType === 'number' ? ( ) : ( )}
) } 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} ))}
{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}
))}
) }