| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| @JS() |
| library gtags; |
| |
| // ignore_for_file: non_constant_identifier_names |
| |
| import 'dart:convert'; |
| import 'dart:html'; |
| |
| import 'package:devtools_shared/devtools_shared.dart' as server; |
| import 'package:js/js.dart'; |
| import 'package:js/js_util.dart'; |
| |
| import '../../devtools.dart' as devtools show version; |
| import '../config_specific/logger/logger.dart'; |
| import '../globals.dart'; |
| import '../ui/analytics_constants.dart'; |
| import '../ui/gtags.dart'; |
| import '../utils.dart'; |
| import '../version.dart'; |
| |
| export '../ui/analytics_constants.dart'; |
| |
| // Dimensions1 AppType values: |
| const String appTypeFlutter = 'flutter'; |
| const String appTypeWeb = 'web'; |
| // Dimensions2 BuildType values: |
| const String buildTypeDebug = 'debug'; |
| const String buildTypeProfile = 'profile'; |
| // Dimensions3 PlatformType values: |
| // android |
| // linux |
| // ios |
| // macos |
| // windows |
| // fuchsia |
| // unknown VM Service before version 3.24 |
| // Dimension4 devToolsPlatformType values: |
| const String devToolsPlatformTypeMac = 'MacIntel'; |
| const String devToolsPlatformTypeLinux = 'Linux'; |
| const String devToolsPlatformTypeWindows = 'Windows'; |
| // Start with Android_n.n.n |
| const String devToolsPlatformTypeAndroid = 'Android_'; |
| // Dimension5 devToolsChrome starts with |
| const String devToolsChromeName = 'Chrome/'; // starts with and ends with n.n.n |
| const String devToolsChromeIos = 'Crios/'; // starts with and ends with n.n.n |
| const String devToolsChromeOS = 'CrOS'; // Chrome OS |
| // Dimension6 devToolsVersion |
| |
| // Dimension7 ideLaunched |
| const String ideLaunchedQuery = 'ide'; // '&ide=' query parameter |
| const String ideLaunchedCLI = 'CLI'; // Command Line Interface |
| |
| @JS('gtagsEnabled') |
| external bool isGtagsEnabled(); |
| |
| /// Is the query parameter >ags= set to reset? |
| @JS('gtagsReset') |
| external bool isGtagsReset(); |
| |
| @JS('initializeGA') |
| external void initializeGA(); |
| |
| @JS() |
| @anonymous |
| class GtagEventDevTools extends GtagEvent { |
| external factory GtagEventDevTools({ |
| String event_category, |
| String event_label, // Event e.g., gaScreenViewEvent, gaSelectEvent, etc. |
| String send_to, // UA ID of target GA property to receive event data. |
| |
| int value, |
| bool non_interaction, |
| dynamic custom_map, |
| String user_app, // dimension1 (flutter or web) |
| String user_build, // dimension2 (debug or profile) |
| String user_platform, // dimension3 (android/ios/fuchsia/linux/mac/windows) |
| String devtools_platform, // dimension4 linux/android/mac/windows |
| String devtools_chrome, // dimension5 Chrome version # |
| String devtools_version, // dimension6 DevTools version # |
| String ide_launched, // dimension7 Devtools launched (CLI, VSCode, Android) |
| String flutter_client_id, // dimension8 Flutter tool client_id (~/.flutter). |
| |
| int raster_duration, |
| int ui_duration, |
| }); |
| |
| @override |
| external String get event_category; |
| |
| @override |
| external String get event_label; |
| |
| @override |
| external String get send_to; |
| |
| @override |
| external int get value; // Positive number. |
| |
| @override |
| external bool get non_interaction; |
| |
| @override |
| external dynamic get custom_map; |
| |
| // Custom dimensions: |
| external String get user_app; |
| |
| external String get user_build; |
| |
| external String get user_platform; |
| |
| external String get devtools_platform; |
| |
| external String get devtools_chrome; |
| |
| external String get devtools_version; |
| |
| external String get ide_launched; |
| |
| external String get flutter_client_id; |
| |
| // Custom metrics: |
| external int get raster_duration; |
| |
| external int get ui_duration; |
| } |
| |
| @JS() |
| @anonymous |
| class GtagExceptionDevTools extends GtagException { |
| external factory GtagExceptionDevTools({ |
| String description, |
| bool fatal, |
| String user_app, // dimension1 (flutter or web) |
| String user_build, // dimension2 (debug or profile) |
| String user_platform, // dimension3 (android or ios) |
| String devtools_platform, // dimension4 linux/android/mac/windows |
| String devtools_chrome, // dimension5 Chrome version # |
| String devtools_version, // dimension6 DevTools version # |
| String ide_launched, // dimension7 IDE launched DevTools |
| String flutter_client_id, // dimension8 Flutter tool clientId |
| }); |
| |
| @override |
| external String get description; // Description of the error. |
| @override |
| external bool get fatal; // Fatal error. |
| |
| // Custom dimensions: |
| external String get user_app; |
| |
| external String get user_build; |
| |
| external String get user_platform; |
| |
| external String get devtools_platform; |
| |
| external String get devtools_chrome; |
| |
| external String get devtools_version; |
| |
| external String get ide_launched; |
| |
| external String get flutter_client_id; |
| } |
| |
| // Code to check if DevTools server is available, will only be true in release |
| // mode, debug mode will be set to false. |
| bool get isDevToolsServerAvailable => !isDebugBuild(); |
| |
| /// Helper to catch any server request which could fail we don't want to fail |
| /// because Analytics had a problem. |
| /// |
| /// Returns HttpRequest or null (if server failure). |
| Future<HttpRequest> _request(String url) async { |
| HttpRequest response; |
| |
| try { |
| response = await HttpRequest.request(url, method: 'POST'); |
| } catch (_) {} |
| |
| return response; |
| } |
| |
| void _logWarning(HttpRequest response, String apiType, [String respText]) { |
| log( |
| 'HttpRequest $apiType failed status = ${response?.status}' |
| '${respText != null ? ', responseText = $respText' : ''}', |
| LogLevel.warning, |
| ); |
| } |
| |
| // 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. |
| |
| /// Request Flutter tool stored property value enabled (GA enabled) stored in |
| /// the file '~\.flutter'. |
| /// |
| /// Return bool. |
| /// Return value of false implies either GA is disabled or the Flutter Tool has |
| /// never been run (null returned from the server). |
| Future<bool> get isFlutterGAEnabled async { |
| bool enabled = false; |
| |
| if (isDevToolsServerAvailable) { |
| final resp = await _request(server.apiGetFlutterGAEnabled); |
| if (resp?.status == HttpStatus.ok) { |
| // A return value of 'null' implies Flutter tool has never been run so |
| // return false for Flutter GA enabled. |
| final responseValue = json.decode(resp.responseText); |
| enabled = responseValue == null ? false : responseValue; |
| } else { |
| _logWarning(resp, server.apiGetFlutterGAEnabled); |
| } |
| } |
| |
| return enabled; |
| } |
| |
| /// Request Flutter tool stored property value clientID (GA enabled) stored in |
| /// the file '~\.flutter'. |
| /// |
| /// Return as a String, empty string implies Flutter Tool has never been run. |
| Future<String> flutterGAClientID() async { |
| // Default empty string, Flutter tool never run. |
| String clientId = ''; |
| |
| if (isDevToolsServerAvailable) { |
| // Test if Flutter is enabled (or if Flutter Tool ever ran) if not enabled |
| // is false, we don't want to be the first to create a ~/.flutter file. |
| if (await isFlutterGAEnabled) { |
| final resp = await _request(server.apiGetFlutterGAClientId); |
| if (resp?.status == HttpStatus.ok) { |
| clientId = json.decode(resp.responseText); |
| if (clientId == null) { |
| // Requested value of 'null' (Flutter tool never ran). Server request |
| // apiGetFlutterGAClientId should not happen because the |
| // isFlutterGAEnabled test should have been false. |
| log('${server.apiGetFlutterGAClientId} is null', LogLevel.warning); |
| } |
| } else { |
| _logWarning(resp, server.apiGetFlutterGAClientId); |
| } |
| } |
| } |
| |
| return clientId; |
| } |
| |
| /// Requests all .devtools properties to be reset to their default values in the |
| /// file '~/.devtools'. |
| Future<void> resetDevToolsFile() async { |
| if (isDevToolsServerAvailable) { |
| final resp = await _request(server.apiResetDevTools); |
| if (resp?.status == HttpStatus.ok) { |
| assert(json.decode(resp.responseText)); |
| } else { |
| _logWarning(resp, server.apiResetDevTools); |
| } |
| } |
| } |
| |
| /// Request DevTools property value 'firstRun' (GA dialog) stored in the file |
| /// '~\.devtools'. |
| Future<bool> get isFirstRun async { |
| bool firstRun = false; |
| |
| if (isDevToolsServerAvailable) { |
| final resp = await _request(server.apiGetDevToolsFirstRun); |
| if (resp?.status == HttpStatus.ok) { |
| firstRun = json.decode(resp.responseText); |
| } else { |
| _logWarning(resp, server.apiGetDevToolsFirstRun); |
| } |
| } |
| |
| return firstRun; |
| } |
| |
| bool _gaEnabled; |
| |
| // Exposed function to JS via allowInterop. |
| bool gaEnabled() => _gaEnabled; |
| |
| /// Request DevTools property value 'enabled' (GA enabled) stored in the file |
| /// '~\.devtools'. |
| Future<bool> get isEnabled async { |
| if (_gaEnabled != null) return _gaEnabled; |
| |
| bool enabled = false; |
| |
| if (isDevToolsServerAvailable) { |
| final resp = await _request(server.apiGetDevToolsEnabled); |
| if (resp?.status == HttpStatus.ok) { |
| enabled = json.decode(resp.responseText); |
| } else { |
| _logWarning(resp, server.apiGetDevToolsEnabled); |
| } |
| } |
| _gaEnabled = enabled; |
| |
| return enabled; |
| } |
| |
| /// Set the DevTools property 'enabled' (GA enabled) stored in the file |
| /// '~/.devtools'. |
| Future<void> setEnabled([bool value = true]) async { |
| if (isDevToolsServerAvailable) { |
| final resp = await _request( |
| '${server.apiSetDevToolsEnabled}' |
| '?${server.devToolsEnabledPropertyName}=$value', |
| ); |
| if (resp?.status == HttpStatus.ok) { |
| assert(json.decode(resp.responseText) == value); |
| _gaEnabled = value; |
| } else { |
| _logWarning(resp, server.apiSetDevToolsEnabled, resp.responseText); |
| } |
| } |
| } |
| |
| /// Set DevTools parameter value for the active survey (e.g. 'Q1-2020'). |
| /// |
| /// The value is stored in the file '~\.devtools'. |
| /// |
| /// This method must be called before calling other survey related methods |
| /// ([isSurveyActionTaken], [setSurveyActionTaken], [surveyShownCount], |
| /// [incrementSurveyShownCount]). If the active survey is not set, warnings are |
| /// logged. |
| Future<bool> setActiveSurvey(String value) async { |
| if (isDevToolsServerAvailable) { |
| final resp = await _request('${server.apiSetActiveSurvey}' |
| '?${server.activeSurveyName}=$value'); |
| if (resp?.status == HttpStatus.ok && json.decode(resp.responseText)) { |
| return true; |
| } |
| if (resp?.status != HttpStatus.ok || !json.decode(resp.responseText)) { |
| _logWarning(resp, server.apiSetActiveSurvey); |
| } |
| } |
| return false; |
| } |
| |
| /// Request DevTools property value 'surveyActionTaken' for the active survey. |
| /// |
| /// The value is stored in the file '~\.devtools'. |
| /// |
| /// Requires [setActiveSurvey] to have been called prior to calling this method. |
| Future<bool> get isSurveyActionTaken async { |
| bool surveyActionTaken = false; |
| |
| if (isDevToolsServerAvailable) { |
| final resp = await _request(server.apiGetSurveyActionTaken); |
| if (resp?.status == HttpStatus.ok) { |
| surveyActionTaken = json.decode(resp.responseText); |
| } else { |
| _logWarning(resp, server.apiGetSurveyActionTaken); |
| } |
| } |
| |
| return surveyActionTaken; |
| } |
| |
| /// Set DevTools property value 'surveyActionTaken' for the active survey. |
| /// |
| /// The value is stored in the file '~\.devtools'. |
| /// |
| /// Requires [setActiveSurvey] to have been called prior to calling this method. |
| Future<void> setSurveyActionTaken() async { |
| if (isDevToolsServerAvailable) { |
| final resp = await _request( |
| '${server.apiSetSurveyActionTaken}' |
| '?${server.surveyActionTakenPropertyName}=true', |
| ); |
| if (resp?.status != HttpStatus.ok || !json.decode(resp.responseText)) { |
| _logWarning(resp, server.apiSetSurveyActionTaken, resp.responseText); |
| } |
| } |
| } |
| |
| /// Request DevTools property value 'surveyShownCount' for the active survey. |
| /// |
| /// The value is stored in the file '~\.devtools'. |
| /// |
| /// Requires [setActiveSurvey] to have been called prior to calling this method. |
| Future<int> get surveyShownCount async { |
| int surveyShownCount = 0; |
| |
| if (isDevToolsServerAvailable) { |
| final resp = await _request(server.apiGetSurveyShownCount); |
| if (resp?.status == HttpStatus.ok) { |
| surveyShownCount = json.decode(resp.responseText); |
| } else { |
| _logWarning(resp, server.apiGetSurveyShownCount); |
| } |
| } |
| |
| return surveyShownCount; |
| } |
| |
| /// Increment DevTools property value 'surveyShownCount' for the active survey. |
| /// |
| /// The value is stored in the file '~\.devtools'. |
| /// |
| /// Requires [setActiveSurvey] to have been called prior to calling this method. |
| Future<int> get incrementSurveyShownCount async { |
| // Any failure will still return 0. |
| int surveyShownCount = 0; |
| |
| if (isDevToolsServerAvailable) { |
| final resp = await _request(server.apiIncrementSurveyShownCount); |
| if (resp?.status == HttpStatus.ok) { |
| surveyShownCount = json.decode(resp.responseText); |
| } else { |
| _logWarning(resp, server.apiIncrementSurveyShownCount); |
| } |
| } |
| return surveyShownCount; |
| } |
| |
| void screen( |
| String screenName, [ |
| int value = 0, |
| ]) { |
| GTag.event( |
| screenName, |
| GtagEventDevTools( |
| event_category: screenViewEvent, |
| value: value, |
| user_app: userAppType, |
| user_build: userBuildType, |
| user_platform: userPlatformType, |
| devtools_platform: devtoolsPlatformType, |
| devtools_chrome: devtoolsChrome, |
| devtools_version: devtoolsVersion, |
| ide_launched: ideLaunched, |
| flutter_client_id: flutterClientId, |
| ), |
| ); |
| } |
| |
| void select( |
| String screenName, |
| String selectedItem, [ |
| int value = 0, |
| ]) { |
| GTag.event( |
| screenName, |
| GtagEventDevTools( |
| event_category: selectEvent, |
| event_label: selectedItem, |
| value: value, |
| user_app: userAppType, |
| user_build: userBuildType, |
| user_platform: userPlatformType, |
| devtools_platform: devtoolsPlatformType, |
| devtools_chrome: devtoolsChrome, |
| devtools_version: devtoolsVersion, |
| ide_launched: ideLaunched, |
| flutter_client_id: flutterClientId, |
| ), |
| ); |
| } |
| |
| // Used only for Timeline Frame selection. |
| void selectFrame( |
| String screenName, |
| String selectedItem, [ |
| int rasterDuration, // Custom metric |
| int uiDuration, // Custom metric |
| ]) { |
| GTag.event( |
| screenName, |
| GtagEventDevTools( |
| event_category: selectEvent, |
| event_label: selectedItem, |
| raster_duration: rasterDuration, |
| ui_duration: uiDuration, |
| user_app: userAppType, |
| user_build: userBuildType, |
| user_platform: userPlatformType, |
| devtools_platform: devtoolsPlatformType, |
| devtools_chrome: devtoolsChrome, |
| devtools_version: devtoolsVersion, |
| ide_launched: ideLaunched, |
| flutter_client_id: flutterClientId, |
| ), |
| ); |
| } |
| |
| String _lastGaError; |
| |
| void error( |
| String errorMessage, |
| bool fatal, |
| ) { |
| // Don't keep recording same last error. |
| if (_lastGaError == errorMessage) return; |
| _lastGaError = errorMessage; |
| |
| GTag.exception( |
| GtagExceptionDevTools( |
| description: errorMessage, |
| fatal: fatal, |
| user_app: userAppType, |
| user_build: userBuildType, |
| user_platform: userPlatformType, |
| devtools_platform: devtoolsPlatformType, |
| devtools_chrome: devtoolsChrome, |
| devtools_version: devtoolsVersion, |
| ide_launched: ideLaunched, |
| flutter_client_id: flutterClientId, |
| ), |
| ); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Utilities to collect all platform and DevTools state for Analytics. |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // GA dimensions: |
| String _userAppType = ''; // dimension1 |
| String _userBuildType = ''; // dimension2 |
| String _userPlatformType = ''; // dimension3 |
| |
| String _devtoolsPlatformType = |
| ''; // dimension4 MacIntel/Linux/Windows/Android_n |
| String _devtoolsChrome = ''; // dimension5 Chrome/n.n.n or Crios/n.n.n |
| |
| const String devtoolsVersion = devtools.version; //dimension6 n.n.n |
| |
| String _ideLaunched = ''; // dimension7 IDE launched DevTools (VSCode, CLI, ...) |
| |
| String _flutterClientId = ''; // dimension8 Flutter tool clientId. |
| |
| String get userAppType => _userAppType; |
| |
| set userAppType(String __userAppType) { |
| _userAppType = __userAppType; |
| } |
| |
| String get userBuildType => _userBuildType; |
| |
| set userBuildType(String __userBuildType) { |
| _userBuildType = __userBuildType; |
| } |
| |
| String get userPlatformType => _userPlatformType; |
| |
| set userPlatformType(String __userPlatformType) { |
| _userPlatformType = __userPlatformType; |
| } |
| |
| String get devtoolsPlatformType => _devtoolsPlatformType; |
| |
| set devtoolsPlatformType(String __devtoolsPlatformType) { |
| _devtoolsPlatformType = __devtoolsPlatformType; |
| } |
| |
| String get devtoolsChrome => _devtoolsChrome; |
| |
| set devtoolsChrome(String __devtoolsChrome) { |
| _devtoolsChrome = __devtoolsChrome; |
| } |
| |
| String get ideLaunched => _ideLaunched; |
| |
| set ideLaunched(String __ideLaunched) { |
| _ideLaunched = __ideLaunched; |
| } |
| |
| String get flutterClientId => _flutterClientId; |
| |
| set flutterClientId(String __flutterClientId) { |
| _flutterClientId = __flutterClientId; |
| } |
| |
| bool _analyticsComputed = false; |
| |
| bool get isDimensionsComputed => _analyticsComputed; |
| |
| void dimensionsComputed() { |
| _analyticsComputed = true; |
| } |
| |
| // Computes the running application. |
| Future<void> computeUserApplicationCustomGTagData() async { |
| if (isDimensionsComputed) return; |
| |
| final isFlutter = await serviceManager.connectedApp.isFlutterApp; |
| final isWebApp = await serviceManager.connectedApp.isDartWebApp; |
| final isProfile = await serviceManager.connectedApp.isProfileBuild; |
| |
| if (isFlutter) { |
| userPlatformType = (await serviceManager.service.isProtocolVersionSupported( |
| supportedVersion: SemanticVersion(major: 3, minor: 24))) |
| ? serviceManager.vm.operatingSystem |
| : 'unknown'; |
| } |
| |
| if (isFlutter) { |
| userAppType = appTypeFlutter; |
| } |
| if (isWebApp) { |
| userAppType = appTypeWeb; |
| } |
| userBuildType = isProfile ? buildTypeProfile : buildTypeDebug; |
| |
| _analyticsComputed = true; |
| } |
| |
| void exposeGaDevToolsEnabledToJs() { |
| setProperty(window, 'gaDevToolsEnabled', allowInterop(gaEnabled)); |
| } |