Skip to content
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

feat: add wrapper support PostHogMaskWidget for Flutter widgets #153

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';
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
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") {
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
rectList.add(element.rect);
}
}

if (element.children != null && element.children!.isNotEmpty) {
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
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);
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

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();
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

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

if (elementData != null) {
activeElementData.addChildren(elementData);
return elementData;
}
}
}
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

// THIS WAY IN THE FUTURE WE CAN MOUNTED FULL WIREFRAME MORE EASILY
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
/*
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;
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

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

try {
final ElementData? widgetElementsTree =
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
_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 {
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
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) {
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
_imageMaskPainter.drawMaskedImageWrapper(
canvas, postHogWidgetWrapperElements, pixelRatio);
}

final picture = recorder.endRecording();

final finalImage =
Expand Down