Skip to content

feat: add wrapper support PostHogMaskWidget for Flutter widgets #153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ class _InitialScreenState extends State<_InitialScreen> {
builder: (context) => const _FirstRoute()),
);
},
child: const Text('Go to Second Route'),
child: const PostHogNoMaskWidget(
child: Text(
'Go to Second Route',
),
),
),
const Padding(
padding: EdgeInsets.all(8.0),
Expand Down Expand Up @@ -204,14 +208,16 @@ class _InitialScreenState extends State<_InitialScreen> {
child: const Text("Flush"),
),
ElevatedButton(
onPressed: () async {
final result = await _posthogFlutterPlugin.getDistinctId();
setState(() {
_result = result;
});
},
child: const Text("distinctId"),
),
onPressed: () async {
final result =
await _posthogFlutterPlugin.getDistinctId();
setState(() {
_result = result;
});
},
child: const PostHogNoMaskWidget(
child: Text("distinctId"),
)),
const Divider(),
const Padding(
padding: EdgeInsets.all(8.0),
Expand Down Expand Up @@ -254,7 +260,8 @@ class _InitialScreenState extends State<_InitialScreen> {
onPressed: () async {
await _posthogFlutterPlugin.reloadFeatureFlags();
},
child: const Text("reloadFeatureFlags"),
child: const PostHogNoMaskWidget(
child: Text("reloadFeatureFlags")),
),
const Divider(),
const Padding(
Expand Down Expand Up @@ -291,15 +298,15 @@ class _FirstRouteState extends State<_FirstRoute> with WidgetsBindingObserver {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First Route'),
title: const PostHogNoMaskWidget(child: Text('First Route')),
),
body: Center(
child: RepaintBoundary(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
child: const Text('Open route'),
child: const PostHogNoMaskWidget(child: Text('Open route')),
onPressed: () {
Navigator.push(
context,
Expand All @@ -310,18 +317,20 @@ class _FirstRouteState extends State<_FirstRoute> with WidgetsBindingObserver {
},
),
const SizedBox(height: 20),
const TextField(
const PostHogNoMaskWidget(
child: TextField(
decoration: InputDecoration(
labelText: 'Sensitive Text Input',
hintText: 'Enter sensitive data',
border: OutlineInputBorder(),
),
),
)),
const SizedBox(height: 20),
Image.asset(
PostHogNoMaskWidget(
child: Image.asset(
'assets/training_posthog.png',
height: 200,
),
)),
const SizedBox(height: 20),
],
),
Expand Down
1 change: 1 addition & 0 deletions lib/posthog_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export 'src/posthog.dart';
export 'src/posthog_config.dart';
export 'src/posthog_observer.dart';
export 'src/posthog_widget_widget.dart';
export 'src/replay/mask/posthog_nomask_widget.dart';
20 changes: 20 additions & 0 deletions lib/src/replay/element_parsers/element_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ class ElementData {
children?.add(elementData);
}

List<Rect> extractNoMaskWidgetRects() {
final rects = <Rect>[];
_collectNoMaskWidgetRects(this, rects);
return rects;
}

List<ElementData> extractRects({bool isRoot = true}) {
List<ElementData> rects = [];

Expand All @@ -35,4 +41,18 @@ class ElementData {
}
return rects;
}

void _collectNoMaskWidgetRects(ElementData element, List<Rect> rectList) {
if (!rectList.contains(element.rect)) {
if (element.type == "PostHogNoMaskWidget") {
rectList.add(element.rect);
}
}

if (element.children != null && element.children!.isNotEmpty) {
for (var child in element.children!) {
_collectNoMaskWidgetRects(child, rectList);
}
}
}
}
39 changes: 39 additions & 0 deletions lib/src/replay/element_parsers/element_object_parser.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:posthog_flutter/src/replay/element_parsers/element_data.dart';
import 'package:posthog_flutter/src/replay/element_parsers/element_parser.dart';
import 'package:posthog_flutter/src/replay/mask/posthog_mask_controller.dart';
import 'package:posthog_flutter/src/replay/mask/posthog_nomask_widget.dart';

class ElementObjectParser {
ElementData? relateRenderObject(
ElementData activeElementData,
Element element,
) {
if (element.widget is PostHogNoMaskWidget) {
final elementData = ElementParser().relate(element, activeElementData);

if (elementData != null) {
activeElementData.addChildren(elementData);
return elementData;
}
}

if (element.widget is Text) {
final elementData = ElementParser().relate(element, activeElementData);

if (elementData != null) {
activeElementData.addChildren(elementData);
return elementData;
}
}

if (element.renderObject is RenderImage) {
final String dataType = element.renderObject.runtimeType.toString();

final parser = PostHogMaskController.instance.parsers[dataType];
if (parser != null) {
final elementData = parser.relate(element, activeElementData);

if (elementData != null) {
activeElementData.addChildren(elementData);
return elementData;
}
}
}

// THIS WAY IN THE FUTURE WE CAN MOUNTED FULL WIREFRAME MORE EASILY
/*
if (element.renderObject is RenderBox) {
final String dataType = element.renderObject.runtimeType.toString();

Expand All @@ -20,6 +57,8 @@ class ElementObjectParser {
}
}
}
*/

return null;
}
}
51 changes: 42 additions & 9 deletions lib/src/replay/mask/image_mask_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,49 @@ import 'package:posthog_flutter/src/replay/element_parsers/element_data.dart';

class ImageMaskPainter {
void drawMaskedImage(
Canvas canvas, List<ElementData> items, double pixelRatio) {
Canvas canvas,
List<ElementData> items,
double pixelRatio,
) {
final paint = Paint()..style = PaintingStyle.fill;
for (var elementData in items) {
paint.color = Colors.black;
final scaled = Rect.fromLTRB(
elementData.rect.left * pixelRatio,
elementData.rect.top * pixelRatio,
elementData.rect.right * pixelRatio,
elementData.rect.bottom * pixelRatio);
canvas.drawRect(scaled, paint);

for (final element in items) {
paint.color = _getColorForElement(element);

final scaledRect = _scaleRect(element.rect, pixelRatio);

canvas.drawRect(scaledRect, paint);
}
}

void drawMaskedImageWrapper(
Canvas canvas,
List<Rect> items,
double pixelRatio,
) {
final paint = Paint()
..style = PaintingStyle.fill
..color = Colors.pinkAccent;

for (final rect in items) {
final scaledRect = _scaleRect(rect, pixelRatio);
canvas.drawRect(scaledRect, paint);
}
}

Color _getColorForElement(ElementData element) {
if (element.type == 'PostHogNoMaskWidget') {
return Colors.pinkAccent;
}
return Colors.black;
}

Rect _scaleRect(Rect rect, double pixelRatio) {
return Rect.fromLTRB(
rect.left * pixelRatio,
rect.top * pixelRatio,
rect.right * pixelRatio,
rect.bottom * pixelRatio,
);
}
}
25 changes: 25 additions & 0 deletions lib/src/replay/mask/posthog_mask_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,29 @@ class PostHogMaskController {
return null;
}
}

List<Rect>? getPostHogWidgetWrapperElements() {
final BuildContext? context = containerKey.currentContext;

if (context == null) {
printIfDebug('Error: containerKey.currentContext is null.');
return null;
}

try {
final ElementData? widgetElementsTree =
_widgetScraper.parseRenderTree(context);

if (widgetElementsTree == null) {
printIfDebug('Error: widgetElementsTree is null after parsing.');
return null;
}

return widgetElementsTree.extractNoMaskWidgetRects();
} catch (e) {
printIfDebug(
'Error during render tree parsing or rectangle extraction: $e');
return null;
}
}
}
35 changes: 35 additions & 0 deletions lib/src/replay/mask/posthog_nomask_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';

class PostHogNoMaskWidget extends StatefulWidget {
final Widget child;

const PostHogNoMaskWidget({
Key? key,
required this.child,
}) : super(key: key);

@override
_PostHogNoMaskWidgetState createState() => _PostHogNoMaskWidgetState();
}

class _PostHogNoMaskWidgetState extends State<PostHogNoMaskWidget> {
final GlobalKey _widgetKey = GlobalKey();

@override
void initState() {
super.initState();
}

@override
void dispose() {
super.dispose();
}

@override
Widget build(BuildContext context) {
return Container(
key: _widgetKey,
child: widget.child,
);
}
}
8 changes: 8 additions & 0 deletions lib/src/replay/screenshot/screenshot_capturer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ class ScreenshotCapturer {

final replayConfig = _config.sessionReplayConfig;

final postHogWidgetWrapperElements =
PostHogMaskController.instance.getPostHogWidgetWrapperElements();

// call getCurrentScreenRects if really necessary
List<ElementData>? elementsDataWidgets;
if (replayConfig.maskAllTexts || replayConfig.maskAllImages) {
Expand Down Expand Up @@ -179,6 +182,11 @@ class ScreenshotCapturer {
picture.dispose();
}
} else {
if (postHogWidgetWrapperElements!.isNotEmpty) {
_imageMaskPainter.drawMaskedImageWrapper(
canvas, postHogWidgetWrapperElements, pixelRatio);
}

final picture = recorder.endRecording();

final finalImage =
Expand Down