Summary
With maskAllTexts: true, text drawn by a CustomPainter onto a Canvas is not masked and is captured in the clear in session replay screenshots.
This is not a per-widget opt-out — the config claims all text is masked, the replay looks correctly masked (every Text widget is a blob), and the canvas-drawn values sit in plain view alongside them. There is no signal that anything was missed.
Cause
lib/src/replay/element_parsers/element_parsers_const.dart registers masking for four render object types only:
ElementParsersConst(this._factory, PostHogSessionReplayConfig? config) {
if (config?.maskAllImages ?? true) {
registerElementParser<RenderImage>();
}
if (config?.maskAllTexts ?? true) {
registerElementParser<RenderParagraph>();
registerElementParser<RenderTransform>();
registerElementParser<RenderEditable>();
}
}
RenderCustomPaint is absent, so any subtree painted through CustomPainter.paint is walked past. A TextPainter.paint(canvas, offset) call inside a painter produces no RenderParagraph, so nothing matches and no mask rect is emitted.
Reproduction
class ValuePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final tp = TextPainter(
text: const TextSpan(
text: '₦2,450,000.00',
style: TextStyle(fontSize: 24, color: Colors.black),
),
textDirection: TextDirection.ltr,
)..layout();
tp.paint(canvas, Offset.zero);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// In a PostHogWidget subtree, with sessionReplay: true and maskAllTexts: true:
Column(
children: const [
Text('₦2,450,000.00'), // masked
SizedBox(width: 200, height: 40,
child: CustomPaint(painter: ValuePainter())), // NOT masked
],
)
Both strings are identical. In the recording the first is masked and the second is readable.
Expected
With maskAllTexts: true, canvas-drawn text is masked — or, if that is not feasible, CustomPaint subtrees are masked wholesale by default when either masking flag is on, since their contents cannot be introspected.
Impact
This affects any chart or custom-painted component that renders values — portfolio charts, balance rings, sparklines with labels. These are common in finance apps, which are also the apps most likely to enable masking in the first place. Because the failure is silent and the surrounding widgets are masked, a team reviewing their own recordings has no way to notice.
Environment
posthog_flutter 5.39.0
- Flutter 3.44.7 (stable)
- iOS 26.x simulator and device
Possible workaround for others hitting this
Wrap the painted widget in PostHogMaskWidget. That works, but it is opt-in and per-site — the default posture of maskAllTexts: true gives no protection here.
Summary
With
maskAllTexts: true, text drawn by aCustomPainteronto aCanvasis not masked and is captured in the clear in session replay screenshots.This is not a per-widget opt-out — the config claims all text is masked, the replay looks correctly masked (every
Textwidget is a blob), and the canvas-drawn values sit in plain view alongside them. There is no signal that anything was missed.Cause
lib/src/replay/element_parsers/element_parsers_const.dartregisters masking for four render object types only:RenderCustomPaintis absent, so any subtree painted throughCustomPainter.paintis walked past. ATextPainter.paint(canvas, offset)call inside a painter produces noRenderParagraph, so nothing matches and no mask rect is emitted.Reproduction
Both strings are identical. In the recording the first is masked and the second is readable.
Expected
With
maskAllTexts: true, canvas-drawn text is masked — or, if that is not feasible,CustomPaintsubtrees are masked wholesale by default when either masking flag is on, since their contents cannot be introspected.Impact
This affects any chart or custom-painted component that renders values — portfolio charts, balance rings, sparklines with labels. These are common in finance apps, which are also the apps most likely to enable masking in the first place. Because the failure is silent and the surrounding widgets are masked, a team reviewing their own recordings has no way to notice.
Environment
posthog_flutter5.39.0Possible workaround for others hitting this
Wrap the painted widget in
PostHogMaskWidget. That works, but it is opt-in and per-site — the default posture ofmaskAllTexts: truegives no protection here.