From db28a4ea2377cb53b59f6fd5df35d8fabb391498 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Wed, 15 Feb 2023 15:32:53 -0500 Subject: [PATCH 1/9] adding event for analyzer startup --- pkgs/dash_analytics/lib/src/enums.dart | 7 +++++- .../test/dash_analytics_test.dart | 23 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/pkgs/dash_analytics/lib/src/enums.dart b/pkgs/dash_analytics/lib/src/enums.dart index cab435abe2..d4dcd8666e 100644 --- a/pkgs/dash_analytics/lib/src/enums.dart +++ b/pkgs/dash_analytics/lib/src/enums.dart @@ -7,8 +7,13 @@ /// The [label] for each enum value is what will be logged, the [description] /// is here for documentation purposes enum DashEvent { + analyzerServerStarted( + label: 'analyzer-server-started', + description: 'Dart Analyzer Server Started', + toolOwner: DashTool.dartAnalyzer, + ), hotReloadTime( - label: 'hot_reload_time', + label: 'hot-reload-time', description: 'Hot reload duration', toolOwner: DashTool.flutterTools, ), diff --git a/pkgs/dash_analytics/test/dash_analytics_test.dart b/pkgs/dash_analytics/test/dash_analytics_test.dart index 81fc92b967..c69386cba5 100644 --- a/pkgs/dash_analytics/test/dash_analytics_test.dart +++ b/pkgs/dash_analytics/test/dash_analytics_test.dart @@ -657,15 +657,36 @@ $initialToolName=${ConfigHandler.dateStamp},$toolsMessageVersion // Regex pattern to match only letters or hyphens final RegExp toolLabelPattern = RegExp(r'^[a-zA-Z\-]+$'); bool valid = true; + final List invalidTools = []; for (DashTool tool in DashTool.values) { if (!toolLabelPattern.hasMatch(tool.label)) { valid = false; + invalidTools.add(tool); } } expect(valid, true, reason: 'All tool labels should have letters and hyphens ' - 'as a delimiter if needed'); + 'as a delimiter if needed; invalid tools below\n$invalidTools'); + }); + + test( + 'All DashEvents labels are made of characters that are letters or hyphens', + () { + // Regex pattern to match only letters or hyphens + final RegExp eventLabelPattern = RegExp(r'^[a-zA-Z\-]+$'); + bool valid = true; + final List invalidEvents = []; + for (DashEvent event in DashEvent.values) { + if (!eventLabelPattern.hasMatch(event.label)) { + valid = false; + invalidEvents.add(event); + } + } + + expect(valid, true, + reason: 'All event labels should have letters and hyphens ' + 'as a delimiter if needed; invalid events below\n$invalidEvents'); }); test('Check that log file is correctly persisting events sent', () { From f83f19cf2fc54477bb6ea819f8287992af55340a Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Wed, 15 Feb 2023 15:50:15 -0500 Subject: [PATCH 2/9] switching from hyphens to underscore; GA4 restriction --- pkgs/dash_analytics/lib/src/enums.dart | 10 +++++----- .../dash_analytics/test/dash_analytics_test.dart | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkgs/dash_analytics/lib/src/enums.dart b/pkgs/dash_analytics/lib/src/enums.dart index d4dcd8666e..943ed7b1a5 100644 --- a/pkgs/dash_analytics/lib/src/enums.dart +++ b/pkgs/dash_analytics/lib/src/enums.dart @@ -8,12 +8,12 @@ /// is here for documentation purposes enum DashEvent { analyzerServerStarted( - label: 'analyzer-server-started', + label: 'analyzer_server_started', description: 'Dart Analyzer Server Started', toolOwner: DashTool.dartAnalyzer, ), hotReloadTime( - label: 'hot-reload-time', + label: 'hot_reload_time', description: 'Hot reload duration', toolOwner: DashTool.flutterTools, ), @@ -31,14 +31,14 @@ enum DashEvent { /// Officially-supported clients of this package. /// -/// All [label] values should use a hyphen as a delimiter +/// All [label] values should use an underscore as a delimiter enum DashTool { flutterTools( - label: 'flutter-tools', + label: 'flutter_tools', description: 'Runs flutter applications from CLI', ), dartAnalyzer( - label: 'dart-analyzer', + label: 'dart_analyzer', description: 'Analyzes dart code in workspace', ); diff --git a/pkgs/dash_analytics/test/dash_analytics_test.dart b/pkgs/dash_analytics/test/dash_analytics_test.dart index c69386cba5..dc129c719b 100644 --- a/pkgs/dash_analytics/test/dash_analytics_test.dart +++ b/pkgs/dash_analytics/test/dash_analytics_test.dart @@ -652,10 +652,10 @@ $initialToolName=${ConfigHandler.dateStamp},$toolsMessageVersion }); test( - 'All DashTools labels are made of characters that are letters or hyphens', + 'All DashTools labels are made of characters that are letters or underscores', () { - // Regex pattern to match only letters or hyphens - final RegExp toolLabelPattern = RegExp(r'^[a-zA-Z\-]+$'); + // Regex pattern to match only letters or underscores + final RegExp toolLabelPattern = RegExp(r'^[a-zA-Z\_]+$'); bool valid = true; final List invalidTools = []; for (DashTool tool in DashTool.values) { @@ -666,15 +666,15 @@ $initialToolName=${ConfigHandler.dateStamp},$toolsMessageVersion } expect(valid, true, - reason: 'All tool labels should have letters and hyphens ' + reason: 'All tool labels should have letters and underscores ' 'as a delimiter if needed; invalid tools below\n$invalidTools'); }); test( - 'All DashEvents labels are made of characters that are letters or hyphens', + 'All DashEvents labels are made of characters that are letters or underscores', () { - // Regex pattern to match only letters or hyphens - final RegExp eventLabelPattern = RegExp(r'^[a-zA-Z\-]+$'); + // Regex pattern to match only letters or underscores + final RegExp eventLabelPattern = RegExp(r'^[a-zA-Z\_]+$'); bool valid = true; final List invalidEvents = []; for (DashEvent event in DashEvent.values) { @@ -685,7 +685,7 @@ $initialToolName=${ConfigHandler.dateStamp},$toolsMessageVersion } expect(valid, true, - reason: 'All event labels should have letters and hyphens ' + reason: 'All event labels should have letters and underscores ' 'as a delimiter if needed; invalid events below\n$invalidEvents'); }); From 9d0dd2e2c95f6196ea1945e12c1c2cf8132a64fe Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Wed, 15 Feb 2023 16:03:29 -0500 Subject: [PATCH 3/9] making eventData nullable for events that don't need it --- pkgs/dash_analytics/example/dash_analytics_example.dart | 2 +- pkgs/dash_analytics/lib/src/analytics.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkgs/dash_analytics/example/dash_analytics_example.dart b/pkgs/dash_analytics/example/dash_analytics_example.dart index c7ec99fe3f..b46f47cde5 100644 --- a/pkgs/dash_analytics/example/dash_analytics_example.dart +++ b/pkgs/dash_analytics/example/dash_analytics_example.dart @@ -25,7 +25,7 @@ void main() { print(analytics.telemetryEnabled); analytics.sendEvent( eventName: DashEvent.hotReloadTime, - eventData: {'time_ns': 345}, + eventData: {'time_ns': 345}, // Optional map to add relevant data ); print(analytics.logFileStats()); analytics.close(); diff --git a/pkgs/dash_analytics/lib/src/analytics.dart b/pkgs/dash_analytics/lib/src/analytics.dart index fa678c9424..ba5d074138 100644 --- a/pkgs/dash_analytics/lib/src/analytics.dart +++ b/pkgs/dash_analytics/lib/src/analytics.dart @@ -127,7 +127,7 @@ abstract class Analytics { /// API to send events to Google Analytics to track usage Future? sendEvent({ required DashEvent eventName, - required Map eventData, + Map eventData, }); /// Pass a boolean to either enable or disable telemetry and make @@ -243,7 +243,7 @@ class AnalyticsImpl implements Analytics { @override Future? sendEvent({ required DashEvent eventName, - required Map eventData, + Map eventData = const {}, }) { if (!telemetryEnabled) return null; @@ -290,7 +290,7 @@ class TestAnalytics extends AnalyticsImpl { @override Future? sendEvent({ required DashEvent eventName, - required Map eventData, + Map eventData = const {}, }) { if (!telemetryEnabled) return null; From f10526774bc2c3663b7d1bc354383e56894562a2 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Wed, 15 Feb 2023 16:05:39 -0500 Subject: [PATCH 4/9] Update dash_analytics_example.dart --- pkgs/dash_analytics/example/dash_analytics_example.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkgs/dash_analytics/example/dash_analytics_example.dart b/pkgs/dash_analytics/example/dash_analytics_example.dart index b46f47cde5..5ff43decfc 100644 --- a/pkgs/dash_analytics/example/dash_analytics_example.dart +++ b/pkgs/dash_analytics/example/dash_analytics_example.dart @@ -23,9 +23,11 @@ void main() { print('###### START ###### $start'); print(analytics.telemetryEnabled); + // [eventData] is an optional map to add relevant data + // for the [eventName] being sent analytics.sendEvent( eventName: DashEvent.hotReloadTime, - eventData: {'time_ns': 345}, // Optional map to add relevant data + eventData: {'time_ns': 345}, ); print(analytics.logFileStats()); analytics.close(); From 9825399fb2493c4aa77e67df6c395cfa690e55f3 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Wed, 15 Feb 2023 17:14:07 -0500 Subject: [PATCH 5/9] added test to check for limitations below - Events can have a maximum of 25 user properties - User property names must be 24 characters or fewer - User property values must be 36 characters or fewer (only for `tool` name) - Event names must be 40 characters or fewer, may only contain alpha-numeric characters and underscores, and must start with an alphabetic character --- .../test/dash_analytics_test.dart | 86 ++++++++++++++----- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/pkgs/dash_analytics/test/dash_analytics_test.dart b/pkgs/dash_analytics/test/dash_analytics_test.dart index dc129c719b..566ac8a95d 100644 --- a/pkgs/dash_analytics/test/dash_analytics_test.dart +++ b/pkgs/dash_analytics/test/dash_analytics_test.dart @@ -654,8 +654,9 @@ $initialToolName=${ConfigHandler.dateStamp},$toolsMessageVersion test( 'All DashTools labels are made of characters that are letters or underscores', () { - // Regex pattern to match only letters or underscores - final RegExp toolLabelPattern = RegExp(r'^[a-zA-Z\_]+$'); + // Regex pattern to match only letters or underscores and must start + // with an alphabet char + final RegExp toolLabelPattern = RegExp(r'^[a-zA-Z]{1}[a-zA-Z\_]*$'); bool valid = true; final List invalidTools = []; for (DashTool tool in DashTool.values) { @@ -670,25 +671,6 @@ $initialToolName=${ConfigHandler.dateStamp},$toolsMessageVersion 'as a delimiter if needed; invalid tools below\n$invalidTools'); }); - test( - 'All DashEvents labels are made of characters that are letters or underscores', - () { - // Regex pattern to match only letters or underscores - final RegExp eventLabelPattern = RegExp(r'^[a-zA-Z\_]+$'); - bool valid = true; - final List invalidEvents = []; - for (DashEvent event in DashEvent.values) { - if (!eventLabelPattern.hasMatch(event.label)) { - valid = false; - invalidEvents.add(event); - } - } - - expect(valid, true, - reason: 'All event labels should have letters and underscores ' - 'as a delimiter if needed; invalid events below\n$invalidEvents'); - }); - test('Check that log file is correctly persisting events sent', () { final int numberOfEvents = max((kLogFileLength * 0.1).floor(), 5); @@ -871,4 +853,66 @@ $initialToolName=${ConfigHandler.dateStamp},$toolsMessageVersion expect(query2.flutterChannelCount, 1, reason: 'The first instance has flutter information initialized'); }); + + test('Payload sent to GA follows limitations', () { + // Link to limitations documentation + // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#limitations + + final Map userPropPayload = userProperty.preparePayload(); + + // Check that each key in the user property object is less than 24 chars and + // that we have less than 25 keys + const int maxUserPropLength = 24; + const int maxUserPropKeys = 25; + bool userPropLengthValid = true; + final List invalidUserProps = []; + for (String key in userPropPayload.keys.toList()) { + if (key.length > maxUserPropLength) { + userPropLengthValid = false; + invalidUserProps.add(key); + } + } + expect(userPropLengthValid, true, + reason: + 'The max length for each user prop is $maxUserPropLength chars\n' + 'The below keys are too long:\n$invalidUserProps'); + expect( + userPropPayload.keys.length < maxUserPropKeys, + true, + reason: 'There are too many keys in the UserProperty payload', + ); + + // Check that each event name is less than 40 chars and starts with + // an alphabetic character; the entire string has to be alphanumeric + // and underscores + final RegExp eventLabelPattern = + RegExp(r'^[a-zA-Z]{1}[a-zA-Z0-9\_]{0,39}$'); + bool eventValid = true; + final List invalidEvents = []; + for (DashEvent event in DashEvent.values) { + if (!eventLabelPattern.hasMatch(event.label)) { + eventValid = false; + invalidEvents.add(event); + } + } + + expect(eventValid, true, + reason: 'All event labels should have letters and underscores ' + 'as a delimiter if needed; invalid events below\n$invalidEvents'); + + // All dash tools must be under 36 characters + const int maxDashToolLength = 36; + bool dashToolLengthValid = true; + final List invalidTools = []; + for (DashTool tool in DashTool.values) { + if (tool.label.length > maxDashToolLength) { + dashToolLengthValid = false; + invalidTools.add(tool); + } + } + + expect(dashToolLengthValid, true, + reason: 'All dash tool labels have to be less than $maxDashToolLength\n' + 'The following are too long below\n$invalidTools'); + }); } From a19d8bd50ddba10bc5ea7990a126d748cf57b173 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Tue, 28 Feb 2023 15:03:54 -0500 Subject: [PATCH 6/9] Empty commit From f35de81caf7d98e290b11bf4b5df152bd71c9522 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Tue, 28 Feb 2023 15:20:01 -0500 Subject: [PATCH 7/9] Update enums.dart --- pkgs/dash_analytics/lib/src/enums.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/dash_analytics/lib/src/enums.dart b/pkgs/dash_analytics/lib/src/enums.dart index 84d1f67ca7..d43bf18f2d 100644 --- a/pkgs/dash_analytics/lib/src/enums.dart +++ b/pkgs/dash_analytics/lib/src/enums.dart @@ -59,7 +59,7 @@ enum DashEvent { /// Officially-supported clients of this package. /// -/// All [label] values should use an underscore as a delimiter +/// All [label] values should use an underscore as a delimiter. enum DashTool { flutterTools( label: 'flutter_tools', From bdaf29e8cb95ad9cd2cce61886d6fcdf906231e0 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:53:29 -0500 Subject: [PATCH 8/9] Added new event + modified setTelemetry to send toggle status to GA --- pkgs/unified_analytics/lib/src/analytics.dart | 23 +++++++++++++++++-- pkgs/unified_analytics/lib/src/enums.dart | 12 ++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index ad7749b85f..a623a0a749 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -130,6 +130,9 @@ abstract class Analytics { /// Pass a boolean to either enable or disable telemetry and make /// the necessary changes in the persisted configuration file + /// + /// Setting the telemetry status will also send an event to GA + /// indicating the latest status of the telemetry from [reportingBool] Future setTelemetry(bool reportingBool); } @@ -260,8 +263,24 @@ class AnalyticsImpl implements Analytics { } @override - Future setTelemetry(bool reportingBool) => - _configHandler.setTelemetry(reportingBool); + Future setTelemetry(bool reportingBool) { + _configHandler.setTelemetry(reportingBool); + + // Construct the body of the request to signal + // telemetry status toggling + // + // We use don't use the sendEvent method because it may + // be blocked by the [telemetryEnabled] getter + final Map body = generateRequestBody( + clientId: _clientId, + eventName: DashEvent.analyticsCollectionEnabled, + eventData: {'status': reportingBool}, + userProperty: userProperty, + ); + + // Pass to the google analytics client to send + return _gaClient.sendData(body); + } } /// This class extends [AnalyticsImpl] and subs out any methods that diff --git a/pkgs/unified_analytics/lib/src/enums.dart b/pkgs/unified_analytics/lib/src/enums.dart index 94073c8436..6c3f734cdb 100644 --- a/pkgs/unified_analytics/lib/src/enums.dart +++ b/pkgs/unified_analytics/lib/src/enums.dart @@ -7,6 +7,14 @@ /// The [label] for each enum value is what will be logged, the [description] /// is here for documentation purposes enum DashEvent { + // Events that can be sent by all tools; these + // events should not be tool specific; toolOwner + // not necessary for these events + analyticsCollectionEnabled( + label: 'analytics_collection_enabled', + description: 'The opt-in status for analytics collection', + ), + // Events for flutter_tools hotReloadTime( label: 'hot_reload_time', @@ -49,11 +57,11 @@ enum DashEvent { final String label; final String description; - final DashTool toolOwner; + final DashTool? toolOwner; const DashEvent({ required this.label, required this.description, - required this.toolOwner, + this.toolOwner, }); } From b5a47897de0620fa86760e1658001615e42efe8a Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Fri, 3 Mar 2023 15:35:49 -0500 Subject: [PATCH 9/9] setTelemetry saves the event + tests added --- pkgs/unified_analytics/lib/src/analytics.dart | 2 ++ .../test/unified_analytics_test.dart | 24 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index a623a0a749..67a782c37c 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -278,6 +278,8 @@ class AnalyticsImpl implements Analytics { userProperty: userProperty, ); + _logHandler.save(data: body); + // Pass to the google analytics client to send return _gaClient.sendData(body); } diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart index 5d2a2ee88a..ffa770642b 100644 --- a/pkgs/unified_analytics/test/unified_analytics_test.dart +++ b/pkgs/unified_analytics/test/unified_analytics_test.dart @@ -142,16 +142,38 @@ void main() { expect(analytics.telemetryEnabled, true, reason: 'Telemetry should be enabled by default ' 'when initialized for the first time'); - // Use the API to disable analytics + expect(logFile.readAsLinesSync().length, 0); await analytics.setTelemetry(false); expect(analytics.telemetryEnabled, false, reason: 'Analytics telemetry should be disabled'); + expect(logFile.readAsLinesSync().length, 1, + reason: 'One event should have been logged for disabling analytics'); + + // Extract the last log item to check for the keys + Map lastLogItem = + jsonDecode(logFile.readAsLinesSync().last); + expect((lastLogItem['events'] as List).last['name'], + 'analytics_collection_enabled', + reason: 'Check on event name'); + expect((lastLogItem['events'] as List).last['params']['status'], false, + reason: 'Status should be false'); // Toggle it back to being enabled await analytics.setTelemetry(true); expect(analytics.telemetryEnabled, true, reason: 'Analytics telemetry should be enabled'); + expect(logFile.readAsLinesSync().length, 2, + reason: 'Second event should have been logged toggling ' + 'analytics back on'); + + // Extract the last log item to check for the keys + lastLogItem = jsonDecode(logFile.readAsLinesSync().last); + expect((lastLogItem['events'] as List).last['name'], + 'analytics_collection_enabled', + reason: 'Check on event name'); + expect((lastLogItem['events'] as List).last['params']['status'], true, + reason: 'Status should be false'); }); test(