mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat(toolbar): Add PII masking functionality to toolbar (#40909)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ? <IconEye /> : <IconHide />,
|
||||
label: piiMaskingEnabled ? 'Show PII' : 'Hide PII',
|
||||
onClick: (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
togglePiiMasking()
|
||||
},
|
||||
custom: true,
|
||||
},
|
||||
piiMaskingEnabled
|
||||
? {
|
||||
icon: (
|
||||
<div
|
||||
className="w-4 h-4 rounded border"
|
||||
// eslint-disable-next-line react/forbid-dom-props
|
||||
style={{ backgroundColor: piiMaskingColor }}
|
||||
/>
|
||||
),
|
||||
label: 'PII masking color',
|
||||
placement: 'right',
|
||||
disabled: !piiMaskingEnabled,
|
||||
items: PII_MASKING_PRESET_COLORS.map((preset) => ({
|
||||
icon: (
|
||||
<div
|
||||
className="w-4 h-4 rounded border"
|
||||
// eslint-disable-next-line react/forbid-dom-props
|
||||
style={{ backgroundColor: preset.value }}
|
||||
/>
|
||||
),
|
||||
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: <IconQuestion />,
|
||||
label: 'Help',
|
||||
|
||||
96
frontend/src/toolbar/bar/piiMaskingStyles.ts
Normal file
96
frontend/src/toolbar/bar/piiMaskingStyles.ts
Normal file
@@ -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
|
||||
@@ -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<toolbarLogicType>([
|
||||
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<toolbarLogicType>([
|
||||
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<toolbarLogicType>([
|
||||
)
|
||||
}
|
||||
},
|
||||
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<toolbarLogicType>([
|
||||
'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
|
||||
|
||||
Reference in New Issue
Block a user