Skip to content

Commit 40d38f9

Browse files
authored
feat: support masking screen names (#500)
1 parent 9eac08d commit 40d38f9

12 files changed

+389
-30
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [Unreleased](https://github.com/Instabug/Instabug-Flutter/compare/v13.3.0...dev)
4+
5+
### Added
6+
7+
- Add support for masking screen names captured by Instabug through the `Instabug.setScreenNameMaskingCallback` API ([#500](https://github.com/Instabug/Instabug-Flutter/pull/500)).
8+
39
## [13.3.0](https://github.com/Instabug/Instabug-Flutter/compare/v13.2.0...v13.3.0) (August 5, 2024)
410

511
### Added

lib/instabug_flutter.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export 'src/modules/surveys.dart';
1919
export 'src/utils/instabug_navigator_observer.dart';
2020
export 'src/utils/screen_loading/instabug_capture_screen_loading.dart';
2121
export 'src/utils/screen_loading/route_matcher.dart';
22+
export 'src/utils/screen_name_masker.dart' show ScreenNameMaskingCallback;

lib/src/models/instabug_route.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'package:flutter/material.dart';
2+
3+
class InstabugRoute {
4+
final Route<dynamic> route;
5+
final String name;
6+
7+
const InstabugRoute({
8+
required this.route,
9+
required this.name,
10+
});
11+
}

lib/src/modules/instabug.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import 'package:instabug_flutter/src/generated/instabug.api.g.dart';
1919
import 'package:instabug_flutter/src/utils/enum_converter.dart';
2020
import 'package:instabug_flutter/src/utils/ibg_build_info.dart';
2121
import 'package:instabug_flutter/src/utils/instabug_logger.dart';
22+
import 'package:instabug_flutter/src/utils/screen_name_masker.dart';
2223
import 'package:meta/meta.dart';
2324

2425
enum InvocationEvent {
@@ -191,6 +192,14 @@ class Instabug {
191192
);
192193
}
193194

195+
/// Sets a [callback] to be called wehenever a screen name is captured to mask
196+
/// sensitive information in the screen name.
197+
static void setScreenNameMaskingCallback(
198+
ScreenNameMaskingCallback? callback,
199+
) {
200+
ScreenNameMasker.I.setMaskingCallback(callback);
201+
}
202+
194203
/// Shows the welcome message in a specific mode.
195204
/// [welcomeMessageMode] is an enum to set the welcome message mode to live, or beta.
196205
static Future<void> showWelcomeMessageWithMode(

lib/src/utils/instabug_navigator_observer.dart

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,40 @@
11
import 'package:flutter/material.dart';
22
import 'package:instabug_flutter/instabug_flutter.dart';
3+
import 'package:instabug_flutter/src/models/instabug_route.dart';
34
import 'package:instabug_flutter/src/modules/instabug.dart';
45
import 'package:instabug_flutter/src/utils/instabug_logger.dart';
56
import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart';
7+
import 'package:instabug_flutter/src/utils/screen_name_masker.dart';
68

79
class InstabugNavigatorObserver extends NavigatorObserver {
8-
final List<Route> _steps = <Route>[];
10+
final List<InstabugRoute> _steps = [];
911

1012
void screenChanged(Route newRoute) {
1113
try {
1214
final screenName = newRoute.settings.name.toString();
15+
final maskedScreenName = ScreenNameMasker.I.mask(screenName);
16+
17+
final route = InstabugRoute(
18+
route: newRoute,
19+
name: maskedScreenName,
20+
);
21+
1322
// Starts a the new UI trace which is exclusive to screen loading
14-
ScreenLoadingManager.I.startUiTrace(screenName);
23+
ScreenLoadingManager.I.startUiTrace(maskedScreenName, screenName);
1524
// If there is a step that hasn't been pushed yet
1625
if (_steps.isNotEmpty) {
1726
// Report the last step and remove it from the list
18-
Instabug.reportScreenChange(
19-
_steps[_steps.length - 1].settings.name.toString(),
20-
);
21-
_steps.remove(_steps[_steps.length - 1]);
27+
Instabug.reportScreenChange(_steps.last.name);
28+
_steps.removeLast();
2229
}
30+
2331
// Add the new step to the list
24-
_steps.add(newRoute);
32+
_steps.add(route);
2533
Future<dynamic>.delayed(const Duration(milliseconds: 1000), () {
2634
// If this route is in the array, report it and remove it from the list
27-
if (_steps.contains(newRoute)) {
28-
Instabug.reportScreenChange(screenName);
29-
_steps.remove(newRoute);
35+
if (_steps.contains(route)) {
36+
Instabug.reportScreenChange(route.name);
37+
_steps.remove(route);
3038
}
3139
});
3240
} catch (e) {

lib/src/utils/screen_loading/screen_loading_manager.dart

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,16 @@ class ScreenLoadingManager {
126126
return sanitizedScreenName;
127127
}
128128

129+
/// Starts a new UI trace with [screenName] as the public screen name and
130+
/// [matchingScreenName] as the screen name used for matching the UI trace
131+
/// with a Screen Loading trace.
129132
@internal
130-
Future<void> startUiTrace(String screenName) async {
133+
Future<void> startUiTrace(
134+
String screenName, [
135+
String? matchingScreenName,
136+
]) async {
137+
matchingScreenName ??= screenName;
138+
131139
try {
132140
resetDidStartScreenLoading();
133141

@@ -151,10 +159,19 @@ class ScreenLoadingManager {
151159
}
152160

153161
final sanitizedScreenName = sanitizeScreenName(screenName);
162+
final sanitizedMatchingScreenName =
163+
sanitizeScreenName(matchingScreenName);
164+
154165
final microTimeStamp = IBGDateTime.I.now().microsecondsSinceEpoch;
155166
final uiTraceId = IBGDateTime.I.now().millisecondsSinceEpoch;
167+
156168
APM.startCpUiTrace(sanitizedScreenName, microTimeStamp, uiTraceId);
157-
currentUiTrace = UiTrace(sanitizedScreenName, traceId: uiTraceId);
169+
170+
currentUiTrace = UiTrace(
171+
screenName: sanitizedScreenName,
172+
matchingScreenName: sanitizedMatchingScreenName,
173+
traceId: uiTraceId,
174+
);
158175
} catch (error, stackTrace) {
159176
_logExceptionErrorAndStackTrace(error, stackTrace);
160177
}
@@ -183,10 +200,7 @@ class ScreenLoadingManager {
183200
return;
184201
}
185202

186-
final isSameScreen = RouteMatcher.I.match(
187-
routePath: trace.screenName,
188-
actualPath: currentUiTrace?.screenName,
189-
);
203+
final isSameScreen = currentUiTrace?.matches(trace.screenName) == true;
190204

191205
final didStartLoading = currentUiTrace?.didStartScreenLoading == true;
192206

lib/src/utils/screen_loading/ui_trace.dart

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,45 @@
1+
import 'package:instabug_flutter/src/utils/screen_loading/route_matcher.dart';
2+
13
class UiTrace {
24
final String screenName;
5+
6+
/// The screen name used while matching the UI trace with a Screen Loading
7+
/// trace.
8+
///
9+
/// For example, this is set to the original screen name before masking when
10+
/// screen names masking is enabled.
11+
final String _matchingScreenName;
12+
313
final int traceId;
414
bool didStartScreenLoading = false;
515
bool didReportScreenLoading = false;
616
bool didExtendScreenLoading = false;
717

8-
UiTrace(
9-
this.screenName, {
18+
UiTrace({
19+
required this.screenName,
1020
required this.traceId,
11-
});
21+
String? matchingScreenName,
22+
}) : _matchingScreenName = matchingScreenName ?? screenName;
1223

1324
UiTrace copyWith({
1425
String? screenName,
26+
String? matchingScreenName,
1527
int? traceId,
1628
}) {
1729
return UiTrace(
18-
screenName ?? this.screenName,
30+
screenName: screenName ?? this.screenName,
31+
matchingScreenName: matchingScreenName ?? _matchingScreenName,
1932
traceId: traceId ?? this.traceId,
2033
);
2134
}
2235

36+
bool matches(String routePath) {
37+
return RouteMatcher.I.match(
38+
routePath: routePath,
39+
actualPath: _matchingScreenName,
40+
);
41+
}
42+
2343
@override
2444
String toString() {
2545
return 'UiTrace{screenName: $screenName, traceId: $traceId, isFirstScreenLoadingReported: $didReportScreenLoading, isFirstScreenLoading: $didStartScreenLoading}';

lib/src/utils/screen_name_masker.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import 'package:flutter/material.dart';
2+
3+
typedef ScreenNameMaskingCallback = String Function(String screen);
4+
5+
/// Mockable [ScreenNameMasker] responsible for masking screen names
6+
/// before they are sent to the native SDKs.
7+
class ScreenNameMasker {
8+
ScreenNameMasker._();
9+
10+
static ScreenNameMasker _instance = ScreenNameMasker._();
11+
12+
static ScreenNameMasker get instance => _instance;
13+
14+
/// Shorthand for [instance]
15+
static ScreenNameMasker get I => instance;
16+
17+
static const emptyScreenNameFallback = "N/A";
18+
19+
ScreenNameMaskingCallback? _screenNameMaskingCallback;
20+
21+
@visibleForTesting
22+
// ignore: use_setters_to_change_properties
23+
static void setInstance(ScreenNameMasker instance) {
24+
_instance = instance;
25+
}
26+
27+
// ignore: use_setters_to_change_properties
28+
void setMaskingCallback(ScreenNameMaskingCallback? callback) {
29+
_screenNameMaskingCallback = callback;
30+
}
31+
32+
String mask(String screen) {
33+
if (_screenNameMaskingCallback == null) {
34+
return screen;
35+
}
36+
37+
final maskedScreen = _screenNameMaskingCallback!(screen).trim();
38+
39+
if (maskedScreen.isEmpty) {
40+
return emptyScreenNameFallback;
41+
}
42+
43+
return maskedScreen;
44+
}
45+
}

test/instabug_test.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:instabug_flutter/instabug_flutter.dart';
66
import 'package:instabug_flutter/src/generated/instabug.api.g.dart';
77
import 'package:instabug_flutter/src/utils/enum_converter.dart';
88
import 'package:instabug_flutter/src/utils/ibg_build_info.dart';
9+
import 'package:instabug_flutter/src/utils/screen_name_masker.dart';
910
import 'package:mockito/annotations.dart';
1011
import 'package:mockito/mockito.dart';
1112

@@ -14,17 +15,20 @@ import 'instabug_test.mocks.dart';
1415
@GenerateMocks([
1516
InstabugHostApi,
1617
IBGBuildInfo,
18+
ScreenNameMasker,
1719
])
1820
void main() {
1921
TestWidgetsFlutterBinding.ensureInitialized();
2022
WidgetsFlutterBinding.ensureInitialized();
2123

2224
final mHost = MockInstabugHostApi();
2325
final mBuildInfo = MockIBGBuildInfo();
26+
final mScreenNameMasker = MockScreenNameMasker();
2427

2528
setUpAll(() {
2629
Instabug.$setHostApi(mHost);
2730
IBGBuildInfo.setInstance(mBuildInfo);
31+
ScreenNameMasker.setInstance(mScreenNameMasker);
2832
});
2933

3034
test('[setEnabled] should call host method', () async {
@@ -76,6 +80,16 @@ void main() {
7680
).called(1);
7781
});
7882

83+
test(
84+
'[setScreenNameMaskingCallback] should set masking callback on screen name masker',
85+
() async {
86+
String callback(String screen) => 'REDACTED/$screen';
87+
88+
Instabug.setScreenNameMaskingCallback(callback);
89+
90+
verify(mScreenNameMasker.setMaskingCallback(callback)).called(1);
91+
});
92+
7993
test('[show] should call host method', () async {
8094
await Instabug.show();
8195

test/utils/instabug_navigator_observer_test.dart

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
44
import 'package:instabug_flutter/instabug_flutter.dart';
55
import 'package:instabug_flutter/src/generated/instabug.api.g.dart';
66
import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart';
7+
import 'package:instabug_flutter/src/utils/screen_name_masker.dart';
78
import 'package:mockito/annotations.dart';
89
import 'package:mockito/mockito.dart';
910

@@ -35,6 +36,8 @@ void main() {
3536
observer = InstabugNavigatorObserver();
3637
route = createRoute(screen);
3738
previousRoute = createRoute(previousScreen);
39+
40+
ScreenNameMasker.I.setMaskingCallback(null);
3841
});
3942

4043
test('should report screen change when a route is pushed', () {
@@ -44,7 +47,7 @@ void main() {
4447
async.elapse(const Duration(milliseconds: 1000));
4548

4649
verify(
47-
mScreenLoadingManager.startUiTrace(screen),
50+
mScreenLoadingManager.startUiTrace(screen, screen),
4851
).called(1);
4952

5053
verify(
@@ -62,7 +65,7 @@ void main() {
6265
async.elapse(const Duration(milliseconds: 1000));
6366

6467
verify(
65-
mScreenLoadingManager.startUiTrace(previousScreen),
68+
mScreenLoadingManager.startUiTrace(previousScreen, previousScreen),
6669
).called(1);
6770

6871
verify(
@@ -80,14 +83,34 @@ void main() {
8083
async.elapse(const Duration(milliseconds: 1000));
8184

8285
verifyNever(
83-
mScreenLoadingManager.startUiTrace(any),
86+
mScreenLoadingManager.startUiTrace(any, any),
8487
);
8588

8689
verifyNever(
8790
mHost.reportScreenChange(any),
8891
);
8992
});
9093
});
94+
95+
test('should mask screen name when masking callback is set', () {
96+
const maskedScreen = 'maskedScreen';
97+
98+
ScreenNameMasker.I.setMaskingCallback((_) => maskedScreen);
99+
100+
fakeAsync((async) {
101+
observer.didPush(route, previousRoute);
102+
103+
async.elapse(const Duration(milliseconds: 1000));
104+
105+
verify(
106+
mScreenLoadingManager.startUiTrace(maskedScreen, screen),
107+
).called(1);
108+
109+
verify(
110+
mHost.reportScreenChange(maskedScreen),
111+
).called(1);
112+
});
113+
});
91114
}
92115

93116
Route createRoute(String? name) {

0 commit comments

Comments
 (0)