PrivateBlock
Mark any subtree as private. Everything inside is masked in the session replay.
Web / React / Next.js
Import from @galacha/react:
import { PrivateBlock } from "@galacha/react";
<PrivateBlock>
<CreditCardForm />
</PrivateBlock>All <input>, <textarea>, <select>, and visible text inside are replaced with asterisks in the captured events. Click positions are captured (you see where the user clicked) but element paths are stripped of identifying text.
React Native
Import from @galacha/react-native:
import { GalachaPrivate } from "@galacha/react-native";
<GalachaPrivate>
<CreditCardForm />
</GalachaPrivate>The view is rendered as a solid gray block in the captured frame. Full pixel masking — even the widget shape is hidden.
Block mode (stronger)
Pass block to drop the subtree entirely from the DOM snapshot, not just mask its values:
<PrivateBlock block>
<SSNInput />
</PrivateBlock>| Mode | What the dashboard sees |
|---|---|
| default | Input widget visible, value masked, element path captured |
block | Empty placeholder, no inner DOM, click events have no target |
Use block for the most sensitive fields (SSN, bank routing numbers, medical identifiers). Use default mode for everything else.
Polymorphic as prop
Render as any HTML element:
<PrivateBlock as="section" className="checkout">
<PaymentForm />
</PrivateBlock><PrivateBlock as="td">
<CardLast4 />
</PrivateBlock>Default is div. Any HTML element works.
Full props
interface PrivateBlockProps {
as?: keyof JSX.IntrinsicElements;
block?: boolean;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}| Prop | Type | Default | Purpose |
|---|---|---|---|
as | Element tag | "div" | Render as a different element |
block | boolean | false | Drop the subtree, not just mask |
className | string | — | Passthrough |
style | CSSProperties | — | Passthrough |
children | ReactNode | — | Content to mask |
What it does under the hood
PrivateBlock renders a standard React element with data-private="true" (or data-private="block" in block mode). The recorder's selector picks these up and masks them during serialization. That's the whole mechanism.
// PrivateBlock output
<div data-private="true">{children}</div>
// In block mode
<div data-private="block">{children}</div>You can achieve the same effect without the component by adding the attribute yourself:
<div data-private="true">
<CreditCardForm />
</div>The component is just cleaner ergonomics.
Examples
Credit card form
<PrivateBlock>
<label>
Card number
<input value={cardNumber} onChange={(e) => setCardNumber(e.target.value)} />
</label>
<label>
CVV
<input type="password" value={cvv} onChange={(e) => setCvv(e.target.value)} />
</label>
</PrivateBlock>Table row with sensitive columns
<tr>
<td>{order.id}</td>
<PrivateBlock as="td">{order.customer_email}</PrivateBlock>
<td>{order.amount}</td>
<PrivateBlock as="td" block>{order.last_four}</PrivateBlock>
</tr>Conditional masking
const isAdmin = user?.role === "admin";
{isAdmin ? (
<p>Customer email: {customer.email}</p>
) : (
<PrivateBlock>
<p>Customer email: {customer.email}</p>
</PrivateBlock>
)}