From 7bedda78943495a0de96eb6db8bdc4cc11aacf17 Mon Sep 17 00:00:00 2001 From: Kunal Purohit Date: Wed, 10 Jun 2026 09:44:26 -0700 Subject: [PATCH] Add session replay privacy masking for images and prices - Enable maskAllImages and maskAssetImages to mask all product photos in replays - Add maskCallback to mask price Text widgets (starting with '$') in replays - Transform cart prices to $1XX.XX format (first digit visible, rest masked) so cart amounts are partially obscured in both the UI and session replay Co-Authored-By: Claude Sonnet 4.6 --- lib/cart.dart | 17 +++++++++++++++-- lib/se_config.dart | 2 +- lib/sentry_setup.dart | 32 ++++++++++++++++++-------------- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/lib/cart.dart b/lib/cart.dart index bb45953..e55b19e 100644 --- a/lib/cart.dart +++ b/lib/cart.dart @@ -3,6 +3,19 @@ import 'package:provider/provider.dart'; import 'models/cart_state_model.dart'; import 'checkout.dart'; +/// Masks a dollar amount for session replay privacy. +/// Keeps only the first digit and replaces the rest with X. +/// e.g. "12.99" → "$1XX.XX", "149.00" → "$1XX.XX" +String _maskPrice(String price) { + // Strip leading '$' if present + final raw = price.startsWith('\$') ? price.substring(1) : price; + if (raw.isEmpty) return '\$$price'; + final firstDigit = raw[0]; + // Replace every digit after the first with 'X', preserve '.' separators + final masked = raw.substring(1).replaceAll(RegExp(r'\d'), 'X'); + return '\$$firstDigit$masked'; +} + class CartView extends StatefulWidget { const CartView({super.key}); @@ -34,7 +47,7 @@ class _CartViewState extends State { ), SizedBox(width: 8), Text( - '\$${cart.computeSubtotal().toStringAsFixed(2)}', + _maskPrice(cart.computeSubtotal().toStringAsFixed(2)), style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, @@ -122,7 +135,7 @@ class _CartViewState extends State { Text(cartItem.id.toString()), Padding(padding: EdgeInsets.fromLTRB(0, 5, 0, 5)), Text( - '\$${cartItem.price}', + _maskPrice(cartItem.price.toString()), style: TextStyle(color: Colors.red[900], fontSize: 17), ), ], diff --git a/lib/se_config.dart b/lib/se_config.dart index 22b1356..12e38bd 100644 --- a/lib/se_config.dart +++ b/lib/se_config.dart @@ -1,3 +1,3 @@ // Each engineer should set their name or identifier here. // This tags all Sentry events with your identifier for separation. -const String se = 'tda'; // <-- Change this to your name or ID +const String se = 'kunal'; // <-- Change this to your name or ID diff --git a/lib/sentry_setup.dart b/lib/sentry_setup.dart index 14eccc6..4e28b94 100644 --- a/lib/sentry_setup.dart +++ b/lib/sentry_setup.dart @@ -188,21 +188,25 @@ Future initSentry({required VoidCallback appRunner}) async { // ======================================== // Session Replay Privacy Configuration // ======================================== - // Disable default masking - everything is visible in session replays - // WARNING: Only use this for demo environments without sensitive data + // Mask all images (product photos) in session replays + options.privacy.maskAllImages = true; + // Also mask asset images (AssetImage widgets loaded from the bundle) + options.privacy.maskAssetImages = true; + // Leave text unmasked by default — we use a callback to selectively mask prices options.privacy.maskAllText = false; - options.privacy.maskAllImages = false; - - // To enable masking again, set the above to true and add custom rules: - // options.privacy.mask(); - // options.privacy.unmask(); - // options.privacy.maskCallback( - // (element, widget) { - // final text = widget.data?.toLowerCase() ?? ''; - // // Add your masking logic here - // return SentryMaskingDecision.continueProcessing; - // }, - // ); + + // Mask any Text widget that displays a price (starts with '$') unless it has + // already been partially obscured in the cart (e.g. '$1XX.XX' format). + options.privacy.maskCallback( + (element, widget) { + final text = widget.data ?? ''; + // Already-masked cart prices contain 'X' — let them render as-is + if (text.startsWith('\$') && !text.contains('X')) { + return SentryMaskingDecision.mask; + } + return SentryMaskingDecision.continueProcessing; + }, + ); // ======================================== // Additional Configuration