Skip to content

Setting up package:unified_analytics within devtools #7084

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 38 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
01b3c7e
Edits to case statements for unified_analytics
eliasyishak Jan 22, 2024
ecb9972
Update server_api.dart
eliasyishak Jan 22, 2024
25e642a
Pass the consent message from the server to app
eliasyishak Jan 23, 2024
786e79c
Hook function to confirm message has been shown
eliasyishak Jan 23, 2024
38d78dd
Automatically onboard devtools if message shown
eliasyishak Jan 23, 2024
32cfdd6
Update pubspec.yaml
eliasyishak Jan 29, 2024
9f929fd
Format fix
eliasyishak Jan 29, 2024
a5c3528
`AnalyticsController. consentMessage` non-nullable
eliasyishak Jan 31, 2024
a7c3daa
Return null from `confirmConsentMessageShown`
eliasyishak Jan 31, 2024
cb8498d
Swap for `apiAnalyticsConsentMessageShown`
eliasyishak Jan 31, 2024
013a5b7
Fixing build time errors for non-nullable strings
eliasyishak Jan 31, 2024
22e3664
`get` --> `fetch` for methods getting consent msg
eliasyishak Jan 31, 2024
70c9019
Format fix
eliasyishak Jan 31, 2024
3120c4a
Remove unused parameter
eliasyishak Jan 31, 2024
180e0fd
Merge remote-tracking branch 'upstream/master' into setup-unified-ana…
eliasyishak Jan 31, 2024
37db4db
Fix test
eliasyishak Feb 2, 2024
2a465a4
Merge remote-tracking branch 'upstream/master' into setup-unified-ana…
eliasyishak Feb 5, 2024
4fed95b
Fix test to check that consent message is shown
eliasyishak Feb 5, 2024
121dfa3
Update NEXT_RELEASE_NOTES.md
eliasyishak Feb 6, 2024
a9d1593
Use hyperlink to link to the policy page
eliasyishak Feb 6, 2024
66c92fe
Dart format fix
eliasyishak Feb 6, 2024
aa16dfc
Handle errors when parsing the consent message
eliasyishak Feb 6, 2024
c39a9a4
Use stubbed function to get fake message
eliasyishak Feb 6, 2024
60ecf83
`confirmConsentMessageShown` -> `markConsentMessageAsShown`
eliasyishak Feb 6, 2024
0414bd8
Fix typo
eliasyishak Feb 6, 2024
a4230b1
Documentation added per code review
eliasyishak Feb 6, 2024
02a07c3
Add analytics package to devtools_app
eliasyishak Feb 7, 2024
06242fb
Have `Analytics` instance created in dds
eliasyishak Feb 8, 2024
ffc97d4
Merge branch 'master' into setup-unified-analytics
eliasyishak Feb 8, 2024
905fed9
Fix merge errors
eliasyishak Feb 8, 2024
97a1405
Merge remote-tracking branch 'upstream/master' into setup-unified-ana…
eliasyishak Feb 8, 2024
bbf2985
Fix nits from code review
eliasyishak Feb 13, 2024
db3bc4c
Use `LinkTextSpan` for url in consent message
eliasyishak Feb 13, 2024
14ab72b
Merge remote-tracking branch 'upstream/master' into setup-unified-ana…
eliasyishak Feb 13, 2024
0ea02df
Fix test
eliasyishak Feb 13, 2024
e77804a
`devtools_tool fix-goldens --run-id=7890083211`
eliasyishak Feb 13, 2024
43738db
Revert "`devtools_tool fix-goldens --run-id=7890083211`"
eliasyishak Feb 13, 2024
36eb36a
Merge remote-tracking branch 'upstream/master' into setup-unified-ana…
eliasyishak Feb 13, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ class DevToolsAutomater {
Future<void>.delayed(safePumpDuration, automateDevToolsGestures);
return DevToolsApp(
defaultScreens(sampleData: sampleData),
AnalyticsController(enabled: false, firstRun: false),
AnalyticsController(
enabled: false,
firstRun: false,
consentMessage: 'fake message',
),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

import 'dart:async';

import 'analytics.dart' as ga;
import 'analytics_controller.dart';

FutureOr<AnalyticsController> get devToolsAnalyticsController => _controller;
AnalyticsController _controller =
AnalyticsController(enabled: false, firstRun: false);
FutureOr<AnalyticsController> get devToolsAnalyticsController async {
return AnalyticsController(
enabled: false,
firstRun: false,
consentMessage: await ga.fetchAnalyticsConsentMessage(),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Future<AnalyticsController> get devToolsAnalyticsController async {
ga.initializeGA();
ga.jsHookupListenerForGA();
},
consentMessage: await ga.fetchAnalyticsConsentMessage(),
markConsentMessageAsShown: ga.markConsentMessageAsShown,
),
);
return _controllerCompleter!.future;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ Future<void> enableAnalytics() async {}

Future<void> disableAnalytics() async {}

Future<String> fetchAnalyticsConsentMessage() async =>
'stubbed consent message';

Future<void> markConsentMessageAsShown() async {}

void screen(
String screenName, [
int value = 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,19 @@ Future<bool> disableAnalytics() async {
return await setAnalyticsEnabled(false);
}

/// Fetch the legal consent message for telemetry collection for
/// package:unified_analyitcs from the server
Future<String> fetchAnalyticsConsentMessage() async {
return await server.fetchAnalyticsConsentMessage();
}

/// Communicates with the server to confirm with package:unified_analyitcs
/// that the consent message has successfully been shown and to allow events
/// to be recorded if the user has decided to remain opted in.
Comment on lines +805 to +806
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"and to allow...". This part is a little confusing to me. What if the user has selected 'no thanks'? We still need to mark the consent message as shown right? But this makes it sound like this method not only marks the consent message as shown, but also opts in to analytics.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I made the decision to automatically mark the message as shown when we show the text. If the user was previously opted out through other dash tools, then they will remain opted out. If they press the accept button, we will change the their telemetry status to opted in for all dash tools.

But in both cases, marking the message as shown just tells the package to add it to the configuration file and log the date the message was shown

Future<void> markConsentMessageAsShown() async {
return await server.markConsentMessageAsShown();
}

/// Computes the DevTools application. Fills in the devtoolsPlatformType and
/// devtoolsChrome.
void computeDevToolsCustomGTagsData() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ class AnalyticsController {
AnalyticsController({
required bool enabled,
required bool firstRun,
required this.consentMessage,
this.onEnableAnalytics,
this.onDisableAnalytics,
this.onSetupAnalytics,
AsyncAnalyticsCallback? markConsentMessageAsShown,
}) : analyticsEnabled = ValueNotifier<bool>(enabled),
_shouldPrompt = ValueNotifier<bool>(firstRun && !enabled) {
_shouldPrompt =
ValueNotifier<bool>(firstRun && consentMessage.isNotEmpty),
_markConsentMessageAsShown = markConsentMessageAsShown {
if (_shouldPrompt.value) {
unawaited(toggleAnalyticsEnabled(true));
}
Expand All @@ -43,8 +47,17 @@ class AnalyticsController {

final AsyncAnalyticsCallback? onDisableAnalytics;

/// Method to call to confirm with package:unified_analytics the user has
/// seen the consent message.
final AsyncAnalyticsCallback? _markConsentMessageAsShown;
Future<void> markConsentMessageAsShown() async =>
await _markConsentMessageAsShown?.call();

final VoidCallback? onSetupAnalytics;

/// Consent message for package:unified_analytics to be shown on first run.
final String consentMessage;

Future<void> toggleAnalyticsEnabled(bool? enable) async {
if (enable == true) {
analyticsEnabled.value = true;
Expand Down
77 changes: 61 additions & 16 deletions packages/devtools_app/lib/src/shared/analytics/prompt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
import 'dart:async';

import 'package:devtools_app_shared/ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

import '../config_specific/launch_url/launch_url.dart';
import '../common_widgets.dart';
import '../utils.dart';
import 'analytics_controller.dart';

Expand All @@ -35,9 +34,15 @@ class _AnalyticsPromptState extends State<AnalyticsPrompt>
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;

return ValueListenableBuilder<bool>(
valueListenable: controller.shouldPrompt,
builder: (context, showPrompt, child) {
// Mark the consent message as shown for unified_analytics so that devtools
// can be onboarded into the config file
// ~/.dart-tool/dart-flutter-telemetry.config
if (showPrompt) unawaited(controller.markConsentMessageAsShown());

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand Down Expand Up @@ -89,30 +94,42 @@ class _AnalyticsPromptState extends State<AnalyticsPrompt>
}

Widget _analyticsDescription(TextTheme textTheme) {
final consentMessageRegExpResults =
parseAnalyticsConsentMessage(controller.consentMessage);

// When failing to parse the consent message, fallback to
// displaying the consent message in its regular form
if (consentMessageRegExpResults == null) {
return RichText(
text: TextSpan(
children: [
TextSpan(
text: controller.consentMessage,
style: textTheme.titleMedium,
),
],
),
);
}

return RichText(
text: TextSpan(
children: [
TextSpan(
text: 'DevTools reports feature usage statistics and basic '
'crash reports to Google in order to help Google improve '
'the tool over time. See Google\'s ',
text: consentMessageRegExpResults[0],
style: textTheme.titleMedium,
),
TextSpan(
text: 'privacy policy',
LinkTextSpan(
link: Link(
display: consentMessageRegExpResults[1],
url: consentMessageRegExpResults[1],
),
context: context,
style:
textTheme.titleMedium?.copyWith(color: const Color(0xFF54C1EF)),
recognizer: TapGestureRecognizer()
..onTap = () {
unawaited(
launchUrl(
'https://www.google.com/intl/en/policies/privacy',
),
);
},
),
TextSpan(
text: '.',
text: consentMessageRegExpResults[2],
style: textTheme.titleMedium,
),
],
Expand Down Expand Up @@ -146,3 +163,31 @@ class _AnalyticsPromptState extends State<AnalyticsPrompt>
);
}
}

/// This method helps to parse the consent message from
/// `package:unified_analytics` so that the URL can be
/// separated from the block of text so that we can have a
/// hyperlink in the displayed consent message.
List<String>? parseAnalyticsConsentMessage(String consentMessage) {
final results = <String>[];
final RegExp pattern =
RegExp(r'^([\S\s]*)(https?:\/\/[^\s]+)(\)\.)$', multiLine: true);

final matches = pattern.allMatches(consentMessage);
if (matches.isEmpty) {
return null;
}

matches.first.groups([1, 2, 3]).forEach((element) {
results.add(element!);
});

// There should be 3 groups returned if correctly parsed, one
// for most of the text, one for the URL, and one for what comes
// after the URL
if (results.length != 3) {
return null;
}

return results;
}
23 changes: 14 additions & 9 deletions packages/devtools_app/lib/src/shared/common_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1443,7 +1443,9 @@ class LinkIconLabel extends StatelessWidget {

void _onLinkTap() {
unawaited(launchUrl(link.url));
ga.select(link.gaScreenName, link.gaSelectedItemDescription);
if (link.gaScreenName != null && link.gaSelectedItemDescription != null) {
ga.select(link.gaScreenName!, link.gaSelectedItemDescription!);
}
}
}

Expand All @@ -1457,10 +1459,13 @@ class LinkTextSpan extends TextSpan {
style: style ?? Theme.of(context).linkTextStyle,
recognizer: TapGestureRecognizer()
..onTap = () async {
ga.select(
link.gaScreenName,
link.gaSelectedItemDescription,
);
if (link.gaScreenName != null &&
link.gaSelectedItemDescription != null) {
ga.select(
link.gaScreenName!,
link.gaSelectedItemDescription!,
);
}
await launchUrl(link.url);
},
);
Expand All @@ -1470,17 +1475,17 @@ class Link {
const Link({
required this.display,
required this.url,
required this.gaScreenName,
required this.gaSelectedItemDescription,
this.gaScreenName,
this.gaSelectedItemDescription,
});

final String display;

final String url;

final String gaScreenName;
final String? gaScreenName;

final String gaSelectedItemDescription;
final String? gaSelectedItemDescription;
}

class Legend extends StatelessWidget {
Expand Down
21 changes: 21 additions & 0 deletions packages/devtools_app/lib/src/shared/server/_analytics_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ Future<bool> setAnalyticsEnabled([bool value = true]) async {
return false;
}

/// Fetch the consent message for package:unified_analytics.
Future<String> fetchAnalyticsConsentMessage() async {
String? consentMessage = '';
if (isDevToolsServerAvailable) {
final resp = await request(apiGetConsentMessage);
if (resp?.statusOk ?? false) {
consentMessage = resp!.body;
}
}

return consentMessage;
}

/// Confirm with package:unified_analytics that the consent message
/// has been shown to the user.
Future<void> markConsentMessageAsShown() async {
if (isDevToolsServerAvailable) {
await request(apiMarkConsentMessageAsShown);
}
}

// TODO(terry): Move to an API scheme similar to the VM service extension where
// '/api/devToolsEnabled' returns the value (identical VM service) and
// '/api/devToolsEnabled?value=true' sets the value.
Expand Down
1 change: 1 addition & 0 deletions packages/devtools_app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dependencies:
stack_trace: ^1.10.0
stream_channel: ^2.1.1
string_scanner: ^1.1.0
unified_analytics: ^5.8.1
url_launcher: ^6.1.0
url_launcher_web: ^2.0.6
vm_service: ^14.0.0
Expand Down
3 changes: 2 additions & 1 deletion packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ significantly improves the user experience when using DevTools embedded in
an IDE. - [#7030](https://github.com/flutter/devtools/pull/7030)
* Removed the "Dense mode" setting. - [#7086](https://github.com/flutter/devtools/pull/7086)
* Added support for filtering with regular expressions in the Logging, Network, and CPU profiler
pages - [#7027](https://github.com/flutter/devtools/pull/7027)
pages. - [#7027](https://github.com/flutter/devtools/pull/7027)
* Add a DevTools server interaction for getting the DTD uri. - [#7054](https://github.com/flutter/devtools/pull/7054), [#7164](https://github.com/flutter/devtools/pull/7164)
* Enabled expression evaluation with scope for the web, allowing evaluation of inspected widgets. - [#7144](https://github.com/flutter/devtools/pull/7144)
* Update `package:vm_service` constraint to `^14.0.0`. - [#6953](https://github.com/flutter/devtools/pull/6953)
* Onboarding devtoools to [`package:unified_analytics`](https://pub.dev/packages/unified_analytics) for unified telemetry logging across Flutter and Dart tooling. - [#7084](https://github.com/flutter/devtools/pull/7084)

## Inspector updates

Expand Down
Loading