Skip to content

Commit d377718

Browse files
authored
Setting up package:unified_analytics within devtools (#7084)
* Edits to case statements for unified_analytics * Update server_api.dart * Pass the consent message from the server to app * Hook function to confirm message has been shown * Automatically onboard devtools if message shown * Update pubspec.yaml * Format fix * `AnalyticsController. consentMessage` non-nullable * Return null from `confirmConsentMessageShown` * Swap for `apiAnalyticsConsentMessageShown` * Fixing build time errors for non-nullable strings * `get` --> `fetch` for methods getting consent msg * Format fix * Remove unused parameter * Fix test * Fix test to check that consent message is shown * Update NEXT_RELEASE_NOTES.md * Use hyperlink to link to the policy page * Dart format fix * Handle errors when parsing the consent message * Use stubbed function to get fake message * `confirmConsentMessageShown` -> `markConsentMessageAsShown` * Fix typo * Documentation added per code review * Add analytics package to devtools_app * Have `Analytics` instance created in dds * Fix merge errors * Fix nits from code review * Use `LinkTextSpan` for url in consent message * Fix test * `devtools_tool fix-goldens --run-id=7890083211` * Revert "`devtools_tool fix-goldens --run-id=7890083211`" This reverts commit e77804a.
1 parent f0a3f8d commit d377718

21 files changed

+247
-47
lines changed

packages/devtools_app/benchmark/test_infra/automators/devtools_automator.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ class DevToolsAutomater {
4545
Future<void>.delayed(safePumpDuration, automateDevToolsGestures);
4646
return DevToolsApp(
4747
defaultScreens(sampleData: sampleData),
48-
AnalyticsController(enabled: false, firstRun: false),
48+
AnalyticsController(
49+
enabled: false,
50+
firstRun: false,
51+
consentMessage: 'fake message',
52+
),
4953
);
5054
}
5155

packages/devtools_app/lib/src/shared/analytics/_analytics_controller_stub.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44

55
import 'dart:async';
66

7+
import 'analytics.dart' as ga;
78
import 'analytics_controller.dart';
89

9-
FutureOr<AnalyticsController> get devToolsAnalyticsController => _controller;
10-
AnalyticsController _controller =
11-
AnalyticsController(enabled: false, firstRun: false);
10+
FutureOr<AnalyticsController> get devToolsAnalyticsController async {
11+
return AnalyticsController(
12+
enabled: false,
13+
firstRun: false,
14+
consentMessage: await ga.fetchAnalyticsConsentMessage(),
15+
);
16+
}

packages/devtools_app/lib/src/shared/analytics/_analytics_controller_web.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Future<AnalyticsController> get devToolsAnalyticsController async {
3333
ga.initializeGA();
3434
ga.jsHookupListenerForGA();
3535
},
36+
consentMessage: await ga.fetchAnalyticsConsentMessage(),
37+
markConsentMessageAsShown: ga.markConsentMessageAsShown,
3638
),
3739
);
3840
return _controllerCompleter!.future;

packages/devtools_app/lib/src/shared/analytics/_analytics_stub.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ Future<void> enableAnalytics() async {}
3232

3333
Future<void> disableAnalytics() async {}
3434

35+
Future<String> fetchAnalyticsConsentMessage() async =>
36+
'stubbed consent message';
37+
38+
Future<void> markConsentMessageAsShown() async {}
39+
3540
void screen(
3641
String screenName, [
3742
int value = 0,

packages/devtools_app/lib/src/shared/analytics/_analytics_web.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,19 @@ Future<bool> disableAnalytics() async {
795795
return await setAnalyticsEnabled(false);
796796
}
797797

798+
/// Fetch the legal consent message for telemetry collection for
799+
/// package:unified_analyitcs from the server
800+
Future<String> fetchAnalyticsConsentMessage() async {
801+
return await server.fetchAnalyticsConsentMessage();
802+
}
803+
804+
/// Communicates with the server to confirm with package:unified_analyitcs
805+
/// that the consent message has successfully been shown and to allow events
806+
/// to be recorded if the user has decided to remain opted in.
807+
Future<void> markConsentMessageAsShown() async {
808+
return await server.markConsentMessageAsShown();
809+
}
810+
798811
/// Computes the DevTools application. Fills in the devtoolsPlatformType and
799812
/// devtoolsChrome.
800813
void computeDevToolsCustomGTagsData() {

packages/devtools_app/lib/src/shared/analytics/analytics_controller.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ class AnalyticsController {
1818
AnalyticsController({
1919
required bool enabled,
2020
required bool firstRun,
21+
required this.consentMessage,
2122
this.onEnableAnalytics,
2223
this.onDisableAnalytics,
2324
this.onSetupAnalytics,
25+
AsyncAnalyticsCallback? markConsentMessageAsShown,
2426
}) : analyticsEnabled = ValueNotifier<bool>(enabled),
25-
_shouldPrompt = ValueNotifier<bool>(firstRun && !enabled) {
27+
_shouldPrompt =
28+
ValueNotifier<bool>(firstRun && consentMessage.isNotEmpty),
29+
_markConsentMessageAsShown = markConsentMessageAsShown {
2630
if (_shouldPrompt.value) {
2731
unawaited(toggleAnalyticsEnabled(true));
2832
}
@@ -43,8 +47,17 @@ class AnalyticsController {
4347

4448
final AsyncAnalyticsCallback? onDisableAnalytics;
4549

50+
/// Method to call to confirm with package:unified_analytics the user has
51+
/// seen the consent message.
52+
final AsyncAnalyticsCallback? _markConsentMessageAsShown;
53+
Future<void> markConsentMessageAsShown() async =>
54+
await _markConsentMessageAsShown?.call();
55+
4656
final VoidCallback? onSetupAnalytics;
4757

58+
/// Consent message for package:unified_analytics to be shown on first run.
59+
final String consentMessage;
60+
4861
Future<void> toggleAnalyticsEnabled(bool? enable) async {
4962
if (enable == true) {
5063
analyticsEnabled.value = true;

packages/devtools_app/lib/src/shared/analytics/prompt.dart

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
import 'dart:async';
66

77
import 'package:devtools_app_shared/ui.dart';
8-
import 'package:flutter/gestures.dart';
98
import 'package:flutter/material.dart';
109

11-
import '../config_specific/launch_url/launch_url.dart';
10+
import '../common_widgets.dart';
1211
import '../utils.dart';
1312
import 'analytics_controller.dart';
1413

@@ -35,9 +34,15 @@ class _AnalyticsPromptState extends State<AnalyticsPrompt>
3534
Widget build(BuildContext context) {
3635
final theme = Theme.of(context);
3736
final textTheme = theme.textTheme;
37+
3838
return ValueListenableBuilder<bool>(
3939
valueListenable: controller.shouldPrompt,
4040
builder: (context, showPrompt, child) {
41+
// Mark the consent message as shown for unified_analytics so that devtools
42+
// can be onboarded into the config file
43+
// ~/.dart-tool/dart-flutter-telemetry.config
44+
if (showPrompt) unawaited(controller.markConsentMessageAsShown());
45+
4146
return Column(
4247
crossAxisAlignment: CrossAxisAlignment.start,
4348
children: [
@@ -89,30 +94,42 @@ class _AnalyticsPromptState extends State<AnalyticsPrompt>
8994
}
9095

9196
Widget _analyticsDescription(TextTheme textTheme) {
97+
final consentMessageRegExpResults =
98+
parseAnalyticsConsentMessage(controller.consentMessage);
99+
100+
// When failing to parse the consent message, fallback to
101+
// displaying the consent message in its regular form
102+
if (consentMessageRegExpResults == null) {
103+
return RichText(
104+
text: TextSpan(
105+
children: [
106+
TextSpan(
107+
text: controller.consentMessage,
108+
style: textTheme.titleMedium,
109+
),
110+
],
111+
),
112+
);
113+
}
114+
92115
return RichText(
93116
text: TextSpan(
94117
children: [
95118
TextSpan(
96-
text: 'DevTools reports feature usage statistics and basic '
97-
'crash reports to Google in order to help Google improve '
98-
'the tool over time. See Google\'s ',
119+
text: consentMessageRegExpResults[0],
99120
style: textTheme.titleMedium,
100121
),
101-
TextSpan(
102-
text: 'privacy policy',
122+
LinkTextSpan(
123+
link: Link(
124+
display: consentMessageRegExpResults[1],
125+
url: consentMessageRegExpResults[1],
126+
),
127+
context: context,
103128
style:
104129
textTheme.titleMedium?.copyWith(color: const Color(0xFF54C1EF)),
105-
recognizer: TapGestureRecognizer()
106-
..onTap = () {
107-
unawaited(
108-
launchUrl(
109-
'https://www.google.com/intl/en/policies/privacy',
110-
),
111-
);
112-
},
113130
),
114131
TextSpan(
115-
text: '.',
132+
text: consentMessageRegExpResults[2],
116133
style: textTheme.titleMedium,
117134
),
118135
],
@@ -146,3 +163,31 @@ class _AnalyticsPromptState extends State<AnalyticsPrompt>
146163
);
147164
}
148165
}
166+
167+
/// This method helps to parse the consent message from
168+
/// `package:unified_analytics` so that the URL can be
169+
/// separated from the block of text so that we can have a
170+
/// hyperlink in the displayed consent message.
171+
List<String>? parseAnalyticsConsentMessage(String consentMessage) {
172+
final results = <String>[];
173+
final RegExp pattern =
174+
RegExp(r'^([\S\s]*)(https?:\/\/[^\s]+)(\)\.)$', multiLine: true);
175+
176+
final matches = pattern.allMatches(consentMessage);
177+
if (matches.isEmpty) {
178+
return null;
179+
}
180+
181+
matches.first.groups([1, 2, 3]).forEach((element) {
182+
results.add(element!);
183+
});
184+
185+
// There should be 3 groups returned if correctly parsed, one
186+
// for most of the text, one for the URL, and one for what comes
187+
// after the URL
188+
if (results.length != 3) {
189+
return null;
190+
}
191+
192+
return results;
193+
}

packages/devtools_app/lib/src/shared/common_widgets.dart

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,7 +1443,9 @@ class LinkIconLabel extends StatelessWidget {
14431443

14441444
void _onLinkTap() {
14451445
unawaited(launchUrl(link.url));
1446-
ga.select(link.gaScreenName, link.gaSelectedItemDescription);
1446+
if (link.gaScreenName != null && link.gaSelectedItemDescription != null) {
1447+
ga.select(link.gaScreenName!, link.gaSelectedItemDescription!);
1448+
}
14471449
}
14481450
}
14491451

@@ -1457,10 +1459,13 @@ class LinkTextSpan extends TextSpan {
14571459
style: style ?? Theme.of(context).linkTextStyle,
14581460
recognizer: TapGestureRecognizer()
14591461
..onTap = () async {
1460-
ga.select(
1461-
link.gaScreenName,
1462-
link.gaSelectedItemDescription,
1463-
);
1462+
if (link.gaScreenName != null &&
1463+
link.gaSelectedItemDescription != null) {
1464+
ga.select(
1465+
link.gaScreenName!,
1466+
link.gaSelectedItemDescription!,
1467+
);
1468+
}
14641469
await launchUrl(link.url);
14651470
},
14661471
);
@@ -1470,17 +1475,17 @@ class Link {
14701475
const Link({
14711476
required this.display,
14721477
required this.url,
1473-
required this.gaScreenName,
1474-
required this.gaSelectedItemDescription,
1478+
this.gaScreenName,
1479+
this.gaSelectedItemDescription,
14751480
});
14761481

14771482
final String display;
14781483

14791484
final String url;
14801485

1481-
final String gaScreenName;
1486+
final String? gaScreenName;
14821487

1483-
final String gaSelectedItemDescription;
1488+
final String? gaSelectedItemDescription;
14841489
}
14851490

14861491
class Legend extends StatelessWidget {

packages/devtools_app/lib/src/shared/server/_analytics_api.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,27 @@ Future<bool> setAnalyticsEnabled([bool value = true]) async {
5656
return false;
5757
}
5858

59+
/// Fetch the consent message for package:unified_analytics.
60+
Future<String> fetchAnalyticsConsentMessage() async {
61+
String? consentMessage = '';
62+
if (isDevToolsServerAvailable) {
63+
final resp = await request(apiGetConsentMessage);
64+
if (resp?.statusOk ?? false) {
65+
consentMessage = resp!.body;
66+
}
67+
}
68+
69+
return consentMessage;
70+
}
71+
72+
/// Confirm with package:unified_analytics that the consent message
73+
/// has been shown to the user.
74+
Future<void> markConsentMessageAsShown() async {
75+
if (isDevToolsServerAvailable) {
76+
await request(apiMarkConsentMessageAsShown);
77+
}
78+
}
79+
5980
// TODO(terry): Move to an API scheme similar to the VM service extension where
6081
// '/api/devToolsEnabled' returns the value (identical VM service) and
6182
// '/api/devToolsEnabled?value=true' sets the value.

packages/devtools_app/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies:
5656
stack_trace: ^1.10.0
5757
stream_channel: ^2.1.1
5858
string_scanner: ^1.1.0
59+
unified_analytics: ^5.8.1
5960
url_launcher: ^6.1.0
6061
url_launcher_web: ^2.0.6
6162
vm_service: ^14.0.0

0 commit comments

Comments
 (0)