PrivateBlock

React component that marks a subtree as private. Everything inside is masked in the replay.

Import

import { PrivateBlock } from "@galacha/react";

Basic usage

<PrivateBlock>
  <CreditCardForm />
</PrivateBlock>

All inputs, textareas, and text inside are masked. The recorder serializes the subtree with data-private="true" which the web SDK picks up at capture time.

Block mode

For a stronger guarantee — the subtree is dropped from the DOM snapshot entirely — pass block:

<PrivateBlock block>
  <SSNInput />
</PrivateBlock>
ModeWhat the dashboard sees
defaultInput widget visible, value masked with asterisks, element path captured
blockEmpty placeholder, no inner DOM, click events have no target path

Polymorphic as prop

Render as any HTML element:

<PrivateBlock as="section" className="checkout">
  <PaymentForm />
</PrivateBlock>
 
<PrivateBlock as="td">
  <CardLast4 />
</PrivateBlock>
 
<PrivateBlock as="article" className="sensitive-content">
  <MedicalHistory />
</PrivateBlock>

Default is div. Any HTML element tag works.

Full props

interface PrivateBlockProps {
  as?: keyof JSX.IntrinsicElements;
  block?: boolean;
  className?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
}
PropTypeDefaultPurpose
asHTML tag"div"Render as a different element
blockbooleanfalseDrop subtree instead of masking
classNamestringPassthrough
styleCSSPropertiesPassthrough
childrenReactNodeSubtree to mask

Examples

Credit card form

<PrivateBlock>
  <label>
    Card number
    <input value={card} onChange={(e) => setCard(e.target.value)} />
  </label>
  <label>
    CVV
    <input type="password" value={cvv} onChange={(e) => setCvv(e.target.value)} />
  </label>
  <label>
    Cardholder name
    <input value={name} onChange={(e) => setName(e.target.value)} />
  </label>
</PrivateBlock>

Conditional based on user role

const isAdmin = user?.role === "admin";
 
{isAdmin ? (
  <p>Customer email: {customer.email}</p>
) : (
  <PrivateBlock>
    <p>Customer email: {customer.email}</p>
  </PrivateBlock>
)}

Admins can see the email unmasked (because they're already trusted with the data). Regular users have it masked.

Table with mixed-sensitivity columns

<table>
  <thead>
    <tr>
      <th>Order</th>
      <th>Customer</th>
      <th>Amount</th>
      <th>Card</th>
    </tr>
  </thead>
  <tbody>
    {orders.map((o) => (
      <tr key={o.id}>
        <td>{o.id}</td>
        <PrivateBlock as="td">{o.customer_email}</PrivateBlock>
        <td>{o.amount}</td>
        <PrivateBlock as="td" block>{o.card_last_four}</PrivateBlock>
      </tr>
    ))}
  </tbody>
</table>

Column-level masking. The card column uses block to drop the DOM entirely.

Medical / healthcare

<section>
  <h2>Patient summary</h2>
  <p>Visit date: {visit.date}</p>
 
  <PrivateBlock block>
    <div>
      <h3>Diagnosis</h3>
      <p>{visit.diagnosis}</p>
      <h3>Notes</h3>
      <p>{visit.notes}</p>
    </div>
  </PrivateBlock>
</section>

block mode ensures the diagnosis text never enters the DOM snapshot at all — stronger than default masking for HIPAA-adjacent cases.

Under the hood

PrivateBlock renders the element with data-private set. That's the whole mechanism:

// default
<div data-private="true">{children}</div>
 
// block mode
<div data-private="block">{children}</div>

You can achieve the same effect manually:

<div data-private="true">
  <CreditCardForm />
</div>

The component is just cleaner ergonomics, plus consistent API with @galacha/react-native's <GalachaPrivate>.

React Native equivalent

import { GalachaPrivate } from "@galacha/react-native";
 
<GalachaPrivate>
  <CreditCardForm />
</GalachaPrivate>

Different mechanism: RN renders the view as a solid gray fill in captured frames. Full pixel masking.

Related