diff --git a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx
index 76f26a8137..b4cce19e3c 100644
--- a/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx
+++ b/frontend/src/lib/lemon-ui/LemonMenu/LemonMenu.tsx
@@ -39,13 +39,13 @@ export interface LemonMenuItemNode extends LemonMenuItemBase {
}
export interface LemonMenuItemLeafCallback extends LemonMenuItemBase {
- onClick?: () => void
+ onClick?: (e: React.MouseEvent) => void
items?: never
placement?: never
keyboardShortcut?: KeyboardShortcut
}
export interface LemonMenuItemLeafLink extends LemonMenuItemBase {
- onClick?: () => void
+ onClick?: (e: React.MouseEvent) => void
to: string
disableClientSideRouting?: boolean
targetBlank?: boolean
diff --git a/frontend/src/toolbar/bar/Toolbar.tsx b/frontend/src/toolbar/bar/Toolbar.tsx
index ea44162606..7784705da7 100644
--- a/frontend/src/toolbar/bar/Toolbar.tsx
+++ b/frontend/src/toolbar/bar/Toolbar.tsx
@@ -10,6 +10,8 @@ import {
IconCheck,
IconCursorClick,
IconDay,
+ IconEye,
+ IconHide,
IconLive,
IconLogomark,
IconNight,
@@ -30,6 +32,7 @@ import { IconFlare, IconMenu } from 'lib/lemon-ui/icons'
import { inStorybook, inStorybookTestRunner } from 'lib/utils'
import { ActionsToolbarMenu } from '~/toolbar/actions/ActionsToolbarMenu'
+import { PII_MASKING_PRESET_COLORS } from '~/toolbar/bar/piiMaskingStyles'
import { toolbarLogic } from '~/toolbar/bar/toolbarLogic'
import { EventDebugMenu } from '~/toolbar/debug/EventDebugMenu'
import { ExperimentsToolbarMenu } from '~/toolbar/experiments/ExperimentsToolbarMenu'
@@ -53,7 +56,11 @@ function EnabledStatusItem({ label, value }: { label: string; value: boolean }):
)
}
-function postHogDebugInfo(posthog: PostHog | null, loadingSurveys: boolean, surveysCount: number): LemonMenuItem {
+function postHogDebugInfoMenuItem(
+ posthog: PostHog | null,
+ loadingSurveys: boolean,
+ surveysCount: number
+): LemonMenuItem {
const isAutocaptureEnabled = posthog?.autocapture?.isEnabled
return {
@@ -134,15 +141,65 @@ function postHogDebugInfo(posthog: PostHog | null, loadingSurveys: boolean, surv
}
}
+function piiMaskingMenuItem(
+ piiMaskingEnabled: boolean,
+ piiMaskingColor: string,
+ togglePiiMasking: () => void,
+ setPiiMaskingColor: (color: string) => void
+): LemonMenuItem[] {
+ return [
+ {
+ icon: piiMaskingEnabled ? : ,
+ label: piiMaskingEnabled ? 'Show PII' : 'Hide PII',
+ onClick: (e: React.MouseEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ togglePiiMasking()
+ },
+ custom: true,
+ },
+ piiMaskingEnabled
+ ? {
+ icon: (
+
+ ),
+ label: 'PII masking color',
+ placement: 'right',
+ disabled: !piiMaskingEnabled,
+ items: PII_MASKING_PRESET_COLORS.map((preset) => ({
+ icon: (
+
+ ),
+ label: preset.label,
+ onClick: () => {
+ setPiiMaskingColor(preset.value)
+ },
+ active: piiMaskingColor === preset.value,
+ custom: true,
+ })),
+ }
+ : undefined,
+ ].filter(Boolean) as LemonMenuItem[]
+}
+
function MoreMenu(): JSX.Element {
- const { hedgehogMode, theme, posthog } = useValues(toolbarLogic)
- const { setHedgehogMode, toggleTheme, setVisibleMenu } = useActions(toolbarLogic)
+ const { hedgehogMode, theme, posthog, piiMaskingEnabled, piiMaskingColor } = useValues(toolbarLogic)
+ const { setHedgehogMode, toggleTheme, setVisibleMenu, togglePiiMasking, setPiiMaskingColor } =
+ useActions(toolbarLogic)
const [loadingSurveys, setLoadingSurveys] = useState(true)
const [surveysCount, setSurveysCount] = useState(0)
useEffect(() => {
- posthog?.surveys?.getSurveys((surveys) => {
+ posthog?.surveys?.getSurveys((surveys: any[]) => {
setSurveysCount(surveys.length)
setLoadingSurveys(false)
}, false)
@@ -180,7 +237,8 @@ function MoreMenu(): JSX.Element {
label: `Switch to ${currentlyLightMode ? 'dark' : 'light'} mode`,
onClick: () => toggleTheme(),
},
- postHogDebugInfo(posthog, loadingSurveys, surveysCount),
+ ...piiMaskingMenuItem(piiMaskingEnabled, piiMaskingColor, togglePiiMasking, setPiiMaskingColor),
+ postHogDebugInfoMenuItem(posthog, loadingSurveys, surveysCount),
{
icon: ,
label: 'Help',
diff --git a/frontend/src/toolbar/bar/piiMaskingStyles.ts b/frontend/src/toolbar/bar/piiMaskingStyles.ts
new file mode 100644
index 0000000000..59bd0f38d5
--- /dev/null
+++ b/frontend/src/toolbar/bar/piiMaskingStyles.ts
@@ -0,0 +1,96 @@
+interface Color {
+ r: number
+ g: number
+ b: number
+}
+
+const lighten = (color: Color, amount: number): Color => {
+ const newR = Math.min(255, color.r + amount)
+ const newG = Math.min(255, color.g + amount)
+ const newB = Math.min(255, color.b + amount)
+ return { r: newR, g: newG, b: newB }
+}
+
+const darken = (color: Color, amount: number): Color => {
+ const newR = Math.max(0, color.r - amount)
+ const newG = Math.max(0, color.g - amount)
+ const newB = Math.max(0, color.b - amount)
+ return { r: newR, g: newG, b: newB }
+}
+
+const parseColorFromHex = (hex: string): Color => {
+ const r = parseInt(hex.substring(0, 2), 16)
+ const g = parseInt(hex.substring(2, 4), 16)
+ const b = parseInt(hex.substring(4, 6), 16)
+ return { r, g, b }
+}
+
+const colorToHex = (color: Color): string => {
+ return `#${color.r.toString(16).padStart(2, '0')}${color.g.toString(16).padStart(2, '0')}${color.b.toString(16).padStart(2, '0')}`
+}
+
+const getColorPalette = (color: string): [Color, Color, Color, Color, Color] => {
+ const hex = color.replace('#', '')
+ const baseColor = parseColorFromHex(hex)
+
+ const color1 = darken(baseColor, 30)
+ const color2 = darken(baseColor, 15)
+ const color3 = baseColor
+ const color4 = lighten(baseColor, 15)
+ const color5 = darken(baseColor, 15)
+
+ return [color1, color2, color3, color4, color5]
+}
+
+export const generatePiiMaskingCSS = (baseColor: string): string => {
+ const [color1, color2, color3, color4, color5] = getColorPalette(baseColor).map(colorToHex)
+
+ return `
+ @keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+ }
+ .ph-no-capture {
+ position: relative !important;
+ }
+ .ph-no-capture::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(
+ 90deg,
+ ${color1} 0%,
+ ${color2} 20%,
+ ${color3} 40%,
+ ${color4} 60%,
+ ${color5} 80%,
+ ${color1} 100%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 6s ease-in-out infinite;
+ z-index: 999999;
+ pointer-events: none;
+ }
+ .ph-no-capture img,
+ .ph-no-capture video {
+ opacity: 0 !important;
+ }
+ .ph-no-capture * {
+ color: transparent !important;
+ text-shadow: none !important;
+ }
+ `
+}
+
+export const PII_MASKING_PRESET_COLORS = [
+ { label: 'Grey', value: '#888888' },
+ { label: 'Red', value: '#aa7777' },
+ { label: 'Blue', value: '#7788aa' },
+ { label: 'Green', value: '#88aa88' },
+ { label: 'Purple', value: '#9988aa' },
+ { label: 'Orange', value: '#aa9988' },
+] as const
diff --git a/frontend/src/toolbar/bar/toolbarLogic.ts b/frontend/src/toolbar/bar/toolbarLogic.ts
index 2fa9472ad6..c3f6ab2d94 100644
--- a/frontend/src/toolbar/bar/toolbarLogic.ts
+++ b/frontend/src/toolbar/bar/toolbarLogic.ts
@@ -12,9 +12,11 @@ import { experimentsTabLogic } from '~/toolbar/experiments/experimentsTabLogic'
import { toolbarConfigLogic } from '~/toolbar/toolbarConfigLogic'
import { TOOLBAR_CONTAINER_CLASS, TOOLBAR_ID, inBounds, makeNavigateWrapper } from '~/toolbar/utils'
+import { generatePiiMaskingCSS } from './piiMaskingStyles'
import type { toolbarLogicType } from './toolbarLogicType'
const MARGIN = 2
+const PII_MASKING_STYLESHEET_ID = 'posthog-pii-masking-styles'
export type MenuState =
| 'none'
@@ -91,6 +93,8 @@ export const toolbarLogic = kea([
setFixedPosition: (position: ToolbarPositionType) => ({ position }),
setCurrentPathname: (pathname: string) => ({ pathname }),
maybeSendNavigationMessage: true,
+ togglePiiMasking: (enabled?: boolean) => ({ enabled }),
+ setPiiMaskingColor: (color: string) => ({ color }),
})),
windowValues(() => ({
windowHeight: (window: Window) => window.innerHeight,
@@ -189,6 +193,20 @@ export const toolbarLogic = kea([
setCurrentPathname: (_, { pathname }) => pathname,
},
],
+ piiMaskingEnabled: [
+ false,
+ { persist: true },
+ {
+ togglePiiMasking: (state, { enabled }) => enabled ?? !state,
+ },
+ ],
+ piiMaskingColor: [
+ '#888888' as string,
+ { persist: true },
+ {
+ setPiiMaskingColor: (_, { color }) => color,
+ },
+ ],
})),
selectors({
position: [
@@ -428,6 +446,31 @@ export const toolbarLogic = kea([
)
}
},
+ togglePiiMasking: () => {
+ const styleElement = document.getElementById(PII_MASKING_STYLESHEET_ID) as HTMLStyleElement | null
+
+ if (values.piiMaskingEnabled) {
+ if (!styleElement) {
+ const newStyleElement = document.createElement('style')
+ newStyleElement.id = PII_MASKING_STYLESHEET_ID
+ document.head.appendChild(newStyleElement)
+ newStyleElement.textContent = generatePiiMaskingCSS(values.piiMaskingColor)
+ } else {
+ styleElement.textContent = generatePiiMaskingCSS(values.piiMaskingColor)
+ }
+ } else {
+ if (styleElement) {
+ styleElement.remove()
+ }
+ }
+ },
+ setPiiMaskingColor: async ({ color }) => {
+ const styleElement = document.getElementById(PII_MASKING_STYLESHEET_ID) as HTMLStyleElement | null
+
+ if (styleElement && values.piiMaskingEnabled) {
+ styleElement.textContent = generatePiiMaskingCSS(color)
+ }
+ },
})),
afterMount(({ actions, values, cache }) => {
// Add window event listeners using disposables
@@ -454,6 +497,27 @@ export const toolbarLogic = kea([
'historyProxy'
)
+ // Initialize PII masking if already enabled
+ // Remove stylesheet on unmount
+ cache.disposables.add(() => {
+ if (values.piiMaskingEnabled) {
+ let styleElement = document.getElementById(PII_MASKING_STYLESHEET_ID) as HTMLStyleElement | null
+ if (!styleElement) {
+ styleElement = document.createElement('style')
+ styleElement.id = PII_MASKING_STYLESHEET_ID
+ document.head.appendChild(styleElement)
+ }
+ styleElement.textContent = generatePiiMaskingCSS(values.piiMaskingColor)
+ }
+
+ return () => {
+ const styleElement = document.getElementById(PII_MASKING_STYLESHEET_ID)
+ if (styleElement) {
+ styleElement.remove()
+ }
+ }
+ }, 'piiMasking')
+
// the toolbar can be run within the posthog parent app
// if it is then it listens to parent messages
const isInIframe = window !== window.parent