├── .github ├── dependabot.yaml └── workflows │ ├── build.yaml │ └── publish.yaml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── example.dart ├── example.html └── ga.dart ├── lib ├── src │ ├── usage_impl.dart │ ├── usage_impl_html.dart │ └── usage_impl_io.dart ├── usage.dart ├── usage_html.dart ├── usage_io.dart └── uuid │ └── uuid.dart ├── pubspec.yaml └── test ├── all.dart ├── hit_types_test.dart ├── src └── common.dart ├── usage_impl_io_test.dart ├── usage_impl_test.dart ├── usage_test.dart ├── uuid_test.dart ├── web.html └── web_test.dart /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | version: 2 3 | enable-beta-ecosystems: true 4 | 5 | updates: 6 | - package-ecosystem: "pub" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | # TODO: Remove this if / when the default changes (dependabot/dependabot-core/issues/4979) 11 | versioning-strategy: increase-if-necessary 12 | 13 | - package-ecosystem: github-actions 14 | directory: / 15 | schedule: 16 | interval: monthly 17 | labels: 18 | - autosubmit 19 | groups: 20 | github-actions: 21 | patterns: 22 | - "*" 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | schedule: 5 | # “At 00:00 (UTC) on Sunday.” 6 | - cron: '0 0 * * 0' 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 18 | - uses: dart-lang/setup-dart@f0ead981b4d9a35b37f30d36160575d60931ec30 19 | 20 | - name: Install dependencies 21 | run: dart pub get 22 | 23 | - name: Verify formatting 24 | run: dart format --output=none --set-exit-if-changed . 25 | 26 | - name: Analyze project source 27 | run: dart analyze --fatal-infos 28 | 29 | - name: Run tests 30 | run: dart test 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+*' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | .packages 3 | .dart_tool/ 4 | .idea/ 5 | pubspec.lock 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the Dart project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | Oliver Sand 8 | Kasper Peulen 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 4.1.1 2 | 3 | - Updated the readme to indicate that this package is deprecated (see #185). 4 | - Require Dart 2.19. 5 | 6 | ## 4.1.0 7 | - Analytics hits can now be batched. See details in the documentation of the 8 | `AnalyticsIO` constructor. 9 | - Allow sendRaw to send Map (#161). 10 | - Address a `null_argument_to_non_null_type` analysis issue. 11 | - Change to using 'package:lints' for analysis. 12 | 13 | ## 4.0.2 14 | - Fix a bug with the analytics ping throttling algorithm. 15 | 16 | ## 4.0.1 17 | - Force close the http client from `IOAnalytics.close()`. 18 | This prevents lingering requests from making the application hang. 19 | 20 | ## 4.0.0 21 | - Publishing a null safe stable release. 22 | 23 | ## 4.0.0-nullsafety 24 | - Updated to support 2.12.0 and null safety. 25 | 26 | ## 3.4.2 27 | - A number of cleanups to improve the package health score. 28 | 29 | ## 3.4.1 30 | - increase the support SDK range to `'<3.0.0'` 31 | 32 | ## 3.4.0 33 | - bump our minimum SDK constraint to `>=2.0.0-dev.30` 34 | - change to using non-deprecated dart:convert constants 35 | 36 | ## 3.3.0 37 | - added a `close()` method to the `Analytics` class 38 | - change our minimum SDK from `1.24.0-dev` to `1.24.0` stable 39 | 40 | ## 3.2.0 41 | - expose the `Analytics.applicationName` and `Analytics.applicationVersion` 42 | properties 43 | - make it easier for clients to extend the `AnalyticsIO` class 44 | - allow for custom parameters when sending a screenView 45 | 46 | ## 3.1.1 47 | - make Analytics.clientId available immediately 48 | 49 | ## 3.1.0 50 | - switch the technique we use to determine the locale to the new dart:io 51 | `Platform.localeName` field 52 | - change our minimum SDK version to `1.24.0` 53 | 54 | ## 3.0.1 55 | - expose the `Analytics.clientId` field 56 | 57 | ## 3.0.0+1 58 | - fixed an NPE in the `usage_io` `getPlatformLocale()` method 59 | 60 | ## 3.0.0 61 | - removed the use of configurable imports 62 | - removed the Flutter specific entry-point; Flutter apps can now use the 63 | regular `dart:io` entrypoint (AnalyticsIO) 64 | - moved the uuid library from `lib/src/` to `lib/uuid/` 65 | - fixed an issue with reporting the user language for the dart:io provider 66 | - changed to send additional lines for reported exceptions 67 | 68 | ## 2.2.2 69 | - adjust the Flutter usage client to Flutter API changes 70 | 71 | ## 2.2.1 72 | - improve the user agent string for the CLI client 73 | 74 | ## 2.2.0+1 75 | - bug fix to prevent frequently changing the settings file 76 | 77 | ## 2.2.0 78 | - added `Analytics.firstRun` 79 | - added `Analytics.enabled` 80 | - removed `Analytics.optIn` 81 | 82 | ## 2.1.0 83 | - added `Analytics.getSessionValue()` 84 | - added `Analytics.onSend` 85 | - added `AnalyticsImpl.sendRaw()` 86 | 87 | ## 2.0.0 88 | - added a `usage` implementation for Flutter (uses conditional directives) 89 | - removed `lib/usage_html.dart`; use the new Analytics.create() static method 90 | - removed `lib/usage_io.dart`; use the new Analytics.create() static method 91 | - bumped to `2.0.0` for API changes and library refactorings 92 | 93 | ## 1.2.0 94 | - added an optional `analyticsUrl` parameter to the usage constructors 95 | 96 | ## 1.1.0 97 | - fix two strong mode analysis issues (overriding a field declaration with a 98 | setter/getter pair) 99 | 100 | ## 1.0.1 101 | - make strong mode compliant 102 | - update some dev package dependencies 103 | 104 | ## 1.0.0 105 | - Rev'd to 1.0.0! 106 | - No other changes from the `0.0.6` release 107 | 108 | ## 0.0.6 109 | - Added a web example 110 | - Added a utility method to time async events (`Analytics.startTimer()`) 111 | - Updated the readme to add information about when we send analytics info 112 | 113 | ## 0.0.5 114 | 115 | - Catch errors during pings to Google Analytics, for example in case of a 116 | missing internet connection 117 | - Track additional browser data, such as screen size and language 118 | - Added tests for `usage` running in a dart:html context 119 | - Changed to a custom implementation of UUID; saved ~376k in compiled JS size 120 | 121 | ## 0.0.4 122 | 123 | - Moved `sanitizeStacktrace` into the main library 124 | 125 | ## 0.0.3 126 | 127 | - Replaced optional positional arguments with named arguments 128 | - Added code coverage! Thanks to https://github.com/Adracus/dart-coveralls and 129 | coveralls.io. 130 | 131 | ## 0.0.2 132 | 133 | - Fixed a bug in `analytics.sendTiming()` 134 | 135 | ## 0.0.1 136 | 137 | - Initial version, created by Stagehand 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pub package](https://img.shields.io/pub/v/usage.svg)](https://pub.dev/packages/usage) 2 | [![package publisher](https://img.shields.io/pub/publisher/usage.svg)](https://pub.dev/packages/usage/publisher) 3 | 4 | # DEPRECATED 5 | 6 | This package is no longer maintained. 7 | https://github.com/dart-archive/usage/issues/185 has details and 8 | discussion, but briefly: 9 | 10 | - this package uses the older Universal Analytics protocol to record hits to 11 | Google Analytics 12 | - Universal Analytics properties will stop processing new hits on July 1, 2023 13 | - this library does not support the newer GA4 protocol 14 | - we (the Dart team) originally built this package for our own use, but we're no 15 | longer consuming it 16 | 17 | Clients who want to record hits to Google Analytics could: 18 | 19 | - for web clients, wrapping the gtags.js library is a good solution 20 | - for native clients, sending hits to the new protocol is not much more 21 | difficult than sending HTTP POSTs to the right endpoint 22 | 23 | See also https://github.com/dart-archive/usage/issues/185 other potential 24 | remediations. 25 | 26 | ## For web apps 27 | 28 | To use this library as a web app, import the `usage_html.dart` library and 29 | instantiate the `AnalyticsHtml` class. 30 | 31 | ## For Flutter apps 32 | 33 | Flutter applications can use the `AnalyticsIO` version of this library. They will need 34 | to specify the documents directory in the constructor in order to tell the library where 35 | to save the analytics preferences: 36 | 37 | ```dart 38 | import 'package:flutter/services.dart'; 39 | import 'package:usage/usage_io.dart'; 40 | 41 | void main() { 42 | final String UA = ...; 43 | 44 | Analytics ga = new AnalyticsIO(UA, 'ga_test', '3.0', 45 | documentsDirectory: PathProvider.getApplicationDocumentsDirectory()); 46 | ... 47 | } 48 | ``` 49 | 50 | ## For command-line apps 51 | 52 | To use this library as a command-line app, import the `usage_io.dart` library 53 | and instantiate the `AnalyticsIO` class. 54 | 55 | Note, for CLI apps, the usage library will send analytics pings asynchronously. 56 | This is useful in that it doesn't block the app generally. It does have one 57 | side-effect, in that outstanding asynchronous requests will block termination 58 | of the VM until that request finishes. So, for short-lived CLI tools, pinging 59 | Google Analytics can cause the tool to pause for several seconds before it 60 | terminates. This is often undesired - gathering analytics information shouldn't 61 | negatively effect the tool's UX. 62 | 63 | One solution to this is to use the `waitForLastPing({Duration timeout})` method 64 | on the analytics object. This will wait until all outstanding analytics requests 65 | have completed, or until the specified duration has elapsed. So, CLI apps can do 66 | something like: 67 | 68 | ```dart 69 | await analytics.waitForLastPing(timeout: new Duration(milliseconds: 200)); 70 | analytics.close(); 71 | ``` 72 | 73 | or: 74 | 75 | ```dart 76 | await analytics.waitForLastPing(timeout: new Duration(milliseconds: 200)); 77 | exit(0); 78 | ``` 79 | 80 | ## Using the API 81 | 82 | Import the package (in this example we use the `dart:io` version): 83 | 84 | ```dart 85 | import 'package:usage/usage_io.dart'; 86 | ``` 87 | 88 | And call some analytics code: 89 | 90 | ```dart 91 | final String UA = ...; 92 | 93 | Analytics ga = new AnalyticsIO(UA, 'ga_test', '3.0'); 94 | ga.analyticsOpt = AnalyticsOpt.optIn; 95 | 96 | ga.sendScreenView('home'); 97 | ga.sendException('foo exception'); 98 | 99 | ga.sendScreenView('files'); 100 | ga.sendTiming('writeTime', 100); 101 | ga.sendTiming('readTime', 20); 102 | ``` 103 | 104 | ## When do we send analytics data? 105 | 106 | You can use this library in an opt-in manner or an opt-out one. It defaults to 107 | opt-out - data will be sent to Google Analytics unless the user explicitly 108 | opts-out. The mode can be adjusted by changing the value of the 109 | `Analytics.analyticsOpt` field. 110 | 111 | *Opt-out* In opt-out mode, if the user does not explicitly opt-out of collecting 112 | analytics, the usage library will send usage data. 113 | 114 | *Opt-in* In opt-in mode, no data will be sent until the user explicitly opt-in 115 | to collection. This includes screen views, events, timing information, and exceptions. 116 | 117 | ## Other info 118 | 119 | For both classes, you need to provide a Google Analytics tracking ID, the 120 | application name, and the application version. 121 | 122 | *Note:* This library is intended for use with the Google Analytics application / 123 | mobile app style tracking IDs (as opposed to the web site style tracking IDs). 124 | 125 | For more information, please see the Google Analytics Measurement Protocol 126 | [Policy](https://developers.google.com/analytics/devguides/collection/protocol/policy). 127 | 128 | ## Contributing 129 | 130 | Tests can be run using `pub run test`. 131 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_flutter_team_lints/analysis_options.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-inference: true 6 | strict-raw-types: true 7 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// A simple web app to hand-test the usage library. 6 | library usage_example; 7 | 8 | import 'dart:html'; 9 | 10 | import 'package:usage/usage_html.dart'; 11 | 12 | Analytics? _analytics; 13 | String? _lastUa; 14 | int _count = 0; 15 | 16 | void main() { 17 | querySelector('#foo')!.onClick.listen((_) => _handleFoo()); 18 | querySelector('#bar')!.onClick.listen((_) => _handleBar()); 19 | querySelector('#page')!.onClick.listen((_) => _changePage()); 20 | } 21 | 22 | String _ua() => (querySelector('#ua') as InputElement).value!.trim(); 23 | 24 | Analytics getAnalytics() { 25 | if (_analytics == null || _lastUa != _ua()) { 26 | _lastUa = _ua(); 27 | _analytics = AnalyticsHtml(_lastUa!, 'Test app', '1.0'); 28 | _analytics!.sendScreenView(window.location.pathname!); 29 | } 30 | 31 | return _analytics!; 32 | } 33 | 34 | void _handleFoo() { 35 | var analytics = getAnalytics(); 36 | analytics.sendEvent('main', 'foo'); 37 | } 38 | 39 | void _handleBar() { 40 | var analytics = getAnalytics(); 41 | analytics.sendEvent('main', 'bar'); 42 | } 43 | 44 | void _changePage() { 45 | var analytics = getAnalytics(); 46 | window.history.pushState(null, 'new page', '${++_count}.html'); 47 | analytics.sendScreenView(window.location.pathname!); 48 | } 49 | -------------------------------------------------------------------------------- /example/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | Usage Web Example 12 | 24 | 25 | 26 | 27 |

Usage library example

28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example/ga.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// A simple command-line app to hand-test the usage library. 6 | library usage_ga; 7 | 8 | import 'package:usage/usage_io.dart'; 9 | 10 | void main(List args) async { 11 | final defaultUA = 'UA-188575324-1'; 12 | 13 | if (args.isEmpty) { 14 | print('usage: dart ga '); 15 | print('pinging default UA value ($defaultUA)'); 16 | } else { 17 | print('pinging ${args.first}'); 18 | } 19 | 20 | var ua = args.isEmpty ? defaultUA : args.first; 21 | 22 | Analytics ga = AnalyticsIO(ua, 'ga_test', '3.0'); 23 | 24 | await ga.sendScreenView('home'); 25 | await ga.sendScreenView('files'); 26 | await ga 27 | .sendException('foo error:\n${sanitizeStacktrace(StackTrace.current)}'); 28 | await ga.sendTiming('writeDuration', 123); 29 | await ga.sendEvent('create', 'consoleapp', label: 'Console App'); 30 | await ga.sendEvent('destroy', 'consoleapp', label: 'Console App'); 31 | 32 | print('pinged $ua'); 33 | 34 | await ga.waitForLastPing(); 35 | 36 | ga.close(); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/usage_impl.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:collection'; 7 | import 'dart:math' as math; 8 | 9 | import '../usage.dart'; 10 | import '../uuid/uuid.dart'; 11 | 12 | String postEncode(Map map) { 13 | // &foo=bar 14 | return map.keys.map((key) { 15 | var value = '${map[key]}'; 16 | return '$key=${Uri.encodeComponent(value)}'; 17 | }).join('&'); 18 | } 19 | 20 | /// A throttling algorithm. This models the throttling after a bucket with 21 | /// water dripping into it at the rate of 1 drop per second. If the bucket has 22 | /// water when an operation is requested, 1 drop of water is removed and the 23 | /// operation is performed. If not the operation is skipped. This algorithm 24 | /// lets operations be performed in bursts without throttling, but holds the 25 | /// overall average rate of operations to 1 per second. 26 | class ThrottlingBucket { 27 | final int startingCount; 28 | int drops; 29 | late int _lastReplenish; 30 | 31 | ThrottlingBucket(this.startingCount) : drops = startingCount { 32 | _lastReplenish = DateTime.now().millisecondsSinceEpoch; 33 | } 34 | 35 | bool removeDrop() { 36 | _checkReplenish(); 37 | 38 | if (drops <= 0) { 39 | return false; 40 | } else { 41 | drops--; 42 | return true; 43 | } 44 | } 45 | 46 | void _checkReplenish() { 47 | final now = DateTime.now().millisecondsSinceEpoch; 48 | 49 | if (_lastReplenish + 1000 < now) { 50 | final inc = (now - _lastReplenish) ~/ 1000; 51 | drops = math.min(drops + inc, startingCount); 52 | _lastReplenish += 1000 * inc; 53 | } 54 | } 55 | } 56 | 57 | class AnalyticsImpl implements Analytics { 58 | static const String _defaultAnalyticsUrl = 59 | 'https://www.google-analytics.com/collect'; 60 | 61 | static const String _defaultAnalyticsBatchingUrl = 62 | 'https://www.google-analytics.com/batch'; 63 | 64 | @override 65 | final String trackingId; 66 | @override 67 | final String? applicationName; 68 | @override 69 | final String? applicationVersion; 70 | 71 | final PersistentProperties properties; 72 | final PostHandler postHandler; 73 | 74 | final ThrottlingBucket _bucket = ThrottlingBucket(20); 75 | final Map _variableMap = {}; 76 | 77 | final List> _futures = []; 78 | 79 | @override 80 | AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut; 81 | 82 | final Duration? _batchingDelay; 83 | final Queue _batchedEvents = Queue(); 84 | bool _isSendingScheduled = false; 85 | 86 | final String _url; 87 | final String _batchingUrl; 88 | 89 | final StreamController> _sendController = 90 | StreamController.broadcast(sync: true); 91 | 92 | AnalyticsImpl( 93 | this.trackingId, 94 | this.properties, 95 | this.postHandler, { 96 | this.applicationName, 97 | this.applicationVersion, 98 | String? analyticsUrl, 99 | String? analyticsBatchingUrl, 100 | Duration? batchingDelay, 101 | }) : _url = analyticsUrl ?? _defaultAnalyticsUrl, 102 | _batchingDelay = batchingDelay, 103 | _batchingUrl = analyticsBatchingUrl ?? _defaultAnalyticsBatchingUrl { 104 | if (applicationName != null) setSessionValue('an', applicationName); 105 | if (applicationVersion != null) setSessionValue('av', applicationVersion); 106 | } 107 | 108 | bool? _firstRun; 109 | 110 | @override 111 | bool get firstRun { 112 | if (_firstRun == null) { 113 | _firstRun = properties['firstRun'] == null; 114 | 115 | if (properties['firstRun'] != false) { 116 | properties['firstRun'] = false; 117 | } 118 | } 119 | 120 | return _firstRun!; 121 | } 122 | 123 | @override 124 | bool get enabled { 125 | var optIn = analyticsOpt == AnalyticsOpt.optIn; 126 | return optIn 127 | ? properties['enabled'] == true 128 | : properties['enabled'] != false; 129 | } 130 | 131 | @override 132 | set enabled(bool value) { 133 | properties['enabled'] = value; 134 | } 135 | 136 | @override 137 | Future sendScreenView(String viewName, 138 | {Map? parameters}) { 139 | var args = {'cd': viewName, ...?parameters}; 140 | return _enqueuePayload('screenview', args); 141 | } 142 | 143 | @override 144 | Future sendEvent(String category, String action, 145 | {String? label, int? value, Map? parameters}) { 146 | final args = { 147 | 'ec': category, 148 | 'ea': action, 149 | if (label != null) 'el': label, 150 | if (value != null) 'ev': value.toString(), 151 | ...?parameters 152 | }; 153 | 154 | return _enqueuePayload('event', args); 155 | } 156 | 157 | @override 158 | Future sendSocial(String network, String action, String target) { 159 | var args = {'sn': network, 'sa': action, 'st': target}; 160 | return _enqueuePayload('social', args); 161 | } 162 | 163 | @override 164 | Future sendTiming(String variableName, int time, 165 | {String? category, String? label}) { 166 | var args = { 167 | 'utv': variableName, 168 | 'utt': time.toString(), 169 | if (label != null) 'utl': label, 170 | if (category != null) 'utc': category, 171 | }; 172 | 173 | return _enqueuePayload('timing', args); 174 | } 175 | 176 | @override 177 | AnalyticsTimer startTimer(String variableName, 178 | {String? category, String? label}) { 179 | return AnalyticsTimer(this, variableName, category: category, label: label); 180 | } 181 | 182 | @override 183 | Future sendException(String description, {bool? fatal}) { 184 | // We trim exceptions to a max length; google analytics will apply it's own 185 | // truncation, likely around 150 chars or so. 186 | const maxExceptionLength = 1000; 187 | 188 | // In order to ensure that the client of this API is not sending any PII 189 | // data, we strip out any stack trace that may reference a path on the 190 | // user's drive (file:/...). 191 | if (description.contains('file:/')) { 192 | description = description.substring(0, description.indexOf('file:/')); 193 | } 194 | 195 | description = description.replaceAll('\n', '; '); 196 | 197 | if (description.length > maxExceptionLength) { 198 | description = description.substring(0, maxExceptionLength); 199 | } 200 | 201 | var args = { 202 | 'exd': description, 203 | if (fatal != null && fatal) 'exf': '1', 204 | }; 205 | return _enqueuePayload('exception', args); 206 | } 207 | 208 | @override 209 | dynamic getSessionValue(String param) => _variableMap[param]; 210 | 211 | @override 212 | void setSessionValue(String param, dynamic value) { 213 | if (value == null) { 214 | _variableMap.remove(param); 215 | } else { 216 | _variableMap[param] = value; 217 | } 218 | } 219 | 220 | @override 221 | Stream> get onSend => _sendController.stream; 222 | 223 | @override 224 | Future> waitForLastPing({Duration? timeout}) async { 225 | // If there are pending messages, send them now. 226 | if (_batchedEvents.isNotEmpty) { 227 | _trySendBatches(Completer()); 228 | } 229 | var f = Future.wait(_futures); 230 | if (timeout != null) f = f.timeout(timeout, onTimeout: () => []); 231 | return f; 232 | } 233 | 234 | @override 235 | void close() => postHandler.close(); 236 | 237 | @override 238 | String get clientId => 239 | (properties['clientId'] ??= Uuid().generateV4()) as String; 240 | 241 | /// Send raw data to analytics. Callers should generally use one of the typed 242 | /// methods (`sendScreenView`, `sendEvent`, ...). 243 | /// 244 | /// Valid values for [hitType] are: 'pageview', 'screenview', 'event', 245 | /// 'transaction', 'item', 'social', 'exception', and 'timing'. 246 | Future sendRaw(String hitType, Map args) { 247 | return _enqueuePayload(hitType, args); 248 | } 249 | 250 | /// Puts a single hit in the queue. If the queue was empty - start waiting 251 | /// for the result of [_batchingDelay] before sending all enqueued events. 252 | /// 253 | /// Valid values for [hitType] are: 'pageview', 'screenview', 'event', 254 | /// 'transaction', 'item', 'social', 'exception', and 'timing'. 255 | Future _enqueuePayload( 256 | String hitType, 257 | Map args, 258 | ) async { 259 | if (!enabled) return; 260 | // TODO(sigurdm): Really all the 'send' methods should not return Futures 261 | // there is not much point in waiting for it. Only [waitForLastPing]. 262 | final completer = Completer(); 263 | final eventArgs = { 264 | ...args, 265 | ..._variableMap, 266 | 'v': '1', // protocol version 267 | 'tid': trackingId, 268 | 'cid': clientId, 269 | 't': hitType, 270 | }; 271 | 272 | _sendController.add(eventArgs); 273 | _batchedEvents.add(postHandler.encodeHit(eventArgs)); 274 | 275 | // If [_batchingDelay] is null we don't do batching. 276 | // TODO(sigurdm): reconsider this. 277 | final batchingDelay = _batchingDelay; 278 | if (batchingDelay == null) { 279 | _trySendBatches(completer); 280 | } else { 281 | // First check if we have a full batch - if so, send them immediately. 282 | if (_batchedEvents.length >= _maxHitsPerBatch || 283 | _batchedEvents.fold(0, (s, e) => s + e.length) >= 284 | _maxBytesPerBatch) { 285 | _trySendBatches(completer); 286 | } else if (!_isSendingScheduled) { 287 | _isSendingScheduled = true; 288 | // ignore: unawaited_futures 289 | Future.delayed(batchingDelay).then((value) { 290 | _isSendingScheduled = false; 291 | _trySendBatches(completer); 292 | }); 293 | } 294 | } 295 | return completer.future; 296 | } 297 | 298 | // Send no more than 20 messages per batch. 299 | static const _maxHitsPerBatch = 20; 300 | // Send no more than 16K per batch. 301 | static const _maxBytesPerBatch = 16000; 302 | 303 | void _trySendBatches(Completer completer) { 304 | final futures = >[]; 305 | while (_batchedEvents.isNotEmpty) { 306 | final batch = []; 307 | final totalLength = 0; 308 | 309 | while (true) { 310 | if (_batchedEvents.isEmpty) break; 311 | if (totalLength + _batchedEvents.first.length > _maxBytesPerBatch) { 312 | break; 313 | } 314 | batch.add(_batchedEvents.removeFirst()); 315 | if (batch.length == _maxHitsPerBatch) break; 316 | } 317 | if (_bucket.removeDrop()) { 318 | final future = postHandler.sendPost( 319 | batch.length == 1 ? _url : _batchingUrl, batch); 320 | _recordFuture(future); 321 | futures.add(future); 322 | } 323 | } 324 | completer.complete(Future.wait(futures).then((_) {})); 325 | } 326 | 327 | void _recordFuture(Future f) { 328 | _futures.add(f); 329 | f.whenComplete(() => _futures.remove(f)); 330 | } 331 | } 332 | 333 | /// A persistent key/value store. An [AnalyticsImpl] instance expects to have 334 | /// one of these injected into it. 335 | /// 336 | /// There are default implementations for `dart:io` and `dart:html` clients. 337 | /// 338 | /// The [name] parameter is used to uniquely store these properties on disk / 339 | /// persistent storage. 340 | abstract class PersistentProperties { 341 | final String name; 342 | 343 | PersistentProperties(this.name); 344 | 345 | dynamic operator [](String key); 346 | 347 | void operator []=(String key, dynamic value); 348 | 349 | /// Re-read settings from the backing store. This may be a no-op on some 350 | /// platforms. 351 | void syncSettings(); 352 | } 353 | 354 | /// A utility class to perform HTTP POSTs. 355 | /// 356 | /// An [AnalyticsImpl] instance expects to have one of these injected into it. 357 | /// There are default implementations for `dart:io` and `dart:html` clients. 358 | /// 359 | /// The POST information should be sent on a best-effort basis. 360 | /// 361 | /// The `Future` from [sendPost] should complete when the operation is finished, 362 | /// but failures to send the information should be silent. 363 | abstract class PostHandler { 364 | Future sendPost(String url, List batch); 365 | String encodeHit(Map hit); 366 | 367 | /// Free any used resources. 368 | void close(); 369 | } 370 | -------------------------------------------------------------------------------- /lib/src/usage_impl_html.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert' show jsonDecode, jsonEncode; 7 | import 'dart:html'; 8 | 9 | import 'usage_impl.dart'; 10 | 11 | /// An interface to a Google Analytics session, suitable for use in web apps. 12 | /// 13 | /// [analyticsUrl] is an optional replacement for the default Google Analytics 14 | /// URL (`https://www.google-analytics.com/collect`). 15 | /// 16 | /// [batchingDelay] is used to control batching behaviour. Events will be sent 17 | /// batches of 20 after the duration is over from when the first message was 18 | /// sent. 19 | /// 20 | /// If [batchingDelay] is `Duration()` messages will be sent when control 21 | /// returns to the event loop. 22 | /// 23 | /// Batched messages are sent in batches of up to 20 messages. 24 | class AnalyticsHtml extends AnalyticsImpl { 25 | AnalyticsHtml( 26 | String trackingId, 27 | String applicationName, 28 | String applicationVersion, { 29 | String? analyticsUrl, 30 | Duration? batchingDelay, 31 | }) : super( 32 | trackingId, 33 | HtmlPersistentProperties(applicationName), 34 | HtmlPostHandler(), 35 | applicationName: applicationName, 36 | applicationVersion: applicationVersion, 37 | analyticsUrl: analyticsUrl, 38 | batchingDelay: batchingDelay, 39 | ) { 40 | var screenWidth = window.screen!.width; 41 | var screenHeight = window.screen!.height; 42 | 43 | setSessionValue('sr', '${screenWidth}x$screenHeight'); 44 | setSessionValue('sd', '${window.screen!.pixelDepth}-bits'); 45 | setSessionValue('ul', window.navigator.language); 46 | } 47 | } 48 | 49 | typedef HttpRequestor = Future Function(String url, 50 | {String? method, dynamic sendData}); 51 | 52 | class HtmlPostHandler extends PostHandler { 53 | final HttpRequestor? mockRequestor; 54 | 55 | HtmlPostHandler({this.mockRequestor}); 56 | 57 | @override 58 | String encodeHit(Map hit) { 59 | var viewportWidth = document.documentElement!.clientWidth; 60 | var viewportHeight = document.documentElement!.clientHeight; 61 | return postEncode({...hit, 'vp': '${viewportWidth}x$viewportHeight'}); 62 | } 63 | 64 | @override 65 | Future sendPost(String url, List batch) async { 66 | var data = batch.join('\n'); 67 | Future Function(String, {String method, dynamic sendData}) 68 | requestor = mockRequestor ?? HttpRequest.request; 69 | try { 70 | await requestor(url, method: 'POST', sendData: data); 71 | } on Exception { 72 | // Catch errors that can happen during a request, but that we can't do 73 | // anything about, e.g. a missing internet connection. 74 | } 75 | } 76 | 77 | @override 78 | void close() {} 79 | } 80 | 81 | class HtmlPersistentProperties extends PersistentProperties { 82 | late final Map _map; 83 | 84 | HtmlPersistentProperties(String name) : super(name) { 85 | var str = window.localStorage[name]; 86 | if (str == null || str.isEmpty) str = '{}'; 87 | _map = jsonDecode(str) as Map; 88 | } 89 | 90 | @override 91 | dynamic operator [](String key) => _map[key]; 92 | 93 | @override 94 | void operator []=(String key, dynamic value) { 95 | if (value == null) { 96 | _map.remove(key); 97 | } else { 98 | _map[key] = value; 99 | } 100 | 101 | window.localStorage[name] = jsonEncode(_map); 102 | } 103 | 104 | @override 105 | void syncSettings() {} 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/usage_impl_io.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert' show JsonEncoder, jsonDecode; 7 | import 'dart:io'; 8 | 9 | import 'package:meta/meta.dart'; 10 | import 'package:path/path.dart' as path; 11 | 12 | import 'usage_impl.dart'; 13 | 14 | /// An interface to a Google Analytics session, suitable for use in command-line 15 | /// applications. 16 | /// 17 | /// [analyticsUrl] is an optional replacement for the default Google Analytics 18 | /// URL (`https://www.google-analytics.com/collect`). 19 | /// 20 | /// `trackingId`, `applicationName`, and `applicationVersion` values should be 21 | /// supplied. `analyticsUrl` is optional, and lets user's substitute their own 22 | /// analytics URL for the default. 23 | /// 24 | /// `documentDirectory` is where the analytics settings are stored. It 25 | /// defaults to the user home directory. For regular `dart:io` apps this doesn't 26 | /// need to be supplied. For Flutter applications, you should pass in a value 27 | /// like `PathProvider.getApplicationDocumentsDirectory()`. 28 | /// 29 | /// [batchingDelay] is used to control batching behaviour. Events will be sent 30 | /// batches of 20 after the duration is over from when the first message was 31 | /// sent. 32 | /// 33 | /// If [batchingDelay] is `Duration()` messages will be sent when control 34 | /// returns to the event loop. 35 | /// 36 | /// Batched messages are sent in batches of up to 20 messages. They will be sent 37 | /// to [analyticsBatchingUrl] defaulting to 38 | /// `https://www.google-analytics.com/batch`. 39 | class AnalyticsIO extends AnalyticsImpl { 40 | AnalyticsIO( 41 | String trackingId, 42 | String applicationName, 43 | String applicationVersion, { 44 | String? analyticsUrl, 45 | String? analyticsBatchingUrl, 46 | Directory? documentDirectory, 47 | HttpClient? client, 48 | Duration? batchingDelay, 49 | }) : super( 50 | trackingId, 51 | IOPersistentProperties(applicationName, 52 | documentDirPath: documentDirectory?.path), 53 | IOPostHandler(client: client), 54 | applicationName: applicationName, 55 | applicationVersion: applicationVersion, 56 | analyticsUrl: analyticsUrl, 57 | analyticsBatchingUrl: analyticsBatchingUrl, 58 | batchingDelay: batchingDelay, 59 | ) { 60 | final locale = getPlatformLocale(); 61 | if (locale != null) { 62 | setSessionValue('ul', locale); 63 | } 64 | } 65 | } 66 | 67 | @visibleForTesting 68 | String createUserAgent() { 69 | final locale = getPlatformLocale() ?? ''; 70 | 71 | if (Platform.isAndroid) { 72 | return 'Mozilla/5.0 (Android; Mobile; $locale)'; 73 | } else if (Platform.isIOS) { 74 | return 'Mozilla/5.0 (iPhone; U; CPU iPhone OS like Mac OS X; $locale)'; 75 | } else if (Platform.isMacOS) { 76 | return 'Mozilla/5.0 (Macintosh; Intel Mac OS X; Macintosh; $locale)'; 77 | } else if (Platform.isWindows) { 78 | return 'Mozilla/5.0 (Windows; Windows; Windows; $locale)'; 79 | } else if (Platform.isLinux) { 80 | return 'Mozilla/5.0 (Linux; Linux; Linux; $locale)'; 81 | } else { 82 | // Dart/1.8.0 (macos; macos; macos; en_US) 83 | var os = Platform.operatingSystem; 84 | return 'Dart/${getDartVersion()} ($os; $os; $os; $locale)'; 85 | } 86 | } 87 | 88 | String userHomeDir() { 89 | var envKey = Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME'; 90 | var value = Platform.environment[envKey]; 91 | return value ?? '.'; 92 | } 93 | 94 | String getDartVersion() { 95 | var ver = Platform.version; 96 | var index = ver.indexOf(' '); 97 | if (index != -1) ver = ver.substring(0, index); 98 | return ver; 99 | } 100 | 101 | class IOPostHandler extends PostHandler { 102 | final HttpClient _client; 103 | 104 | IOPostHandler({HttpClient? client}) 105 | : _client = (client ?? HttpClient())..userAgent = createUserAgent(); 106 | 107 | @override 108 | String encodeHit(Map hit) { 109 | return postEncode(hit); 110 | } 111 | 112 | @override 113 | Future sendPost(String url, List batch) async { 114 | var data = batch.join('\n'); 115 | try { 116 | var req = await _client.postUrl(Uri.parse(url)); 117 | req.write(data); 118 | var response = await req.close(); 119 | await response.drain(); 120 | } on Exception { 121 | // Catch errors that can happen during a request, but that we can't do 122 | // anything about, e.g. a missing internet connection. 123 | } 124 | } 125 | 126 | @override 127 | void close() { 128 | // Do a force close to ensure that lingering requests will not stall the 129 | // program. 130 | _client.close(force: true); 131 | } 132 | } 133 | 134 | JsonEncoder _jsonEncoder = JsonEncoder.withIndent(' '); 135 | 136 | class IOPersistentProperties extends PersistentProperties { 137 | late final File _file; 138 | late Map _map; 139 | 140 | IOPersistentProperties(String name, {String? documentDirPath}) : super(name) { 141 | var fileName = '.${name.replaceAll(' ', '_')}'; 142 | documentDirPath ??= userHomeDir(); 143 | _file = File(path.join(documentDirPath, fileName)); 144 | if (!_file.existsSync()) { 145 | _file.createSync(); 146 | } 147 | syncSettings(); 148 | } 149 | 150 | IOPersistentProperties.fromFile(File file) : super(path.basename(file.path)) { 151 | _file = file; 152 | if (!_file.existsSync()) { 153 | _file.createSync(); 154 | } 155 | syncSettings(); 156 | } 157 | 158 | @override 159 | dynamic operator [](String key) => _map[key]; 160 | 161 | @override 162 | void operator []=(String key, dynamic value) { 163 | if (value == null && !_map.containsKey(key)) return; 164 | if (_map[key] == value) return; 165 | 166 | if (value == null) { 167 | _map.remove(key); 168 | } else { 169 | _map[key] = value; 170 | } 171 | 172 | try { 173 | _file.writeAsStringSync('${_jsonEncoder.convert(_map)}\n'); 174 | } catch (_) {} 175 | } 176 | 177 | @override 178 | void syncSettings() { 179 | try { 180 | var contents = _file.readAsStringSync(); 181 | if (contents.isEmpty) contents = '{}'; 182 | _map = jsonDecode(contents) as Map; 183 | } catch (_) { 184 | _map = {}; 185 | } 186 | } 187 | } 188 | 189 | /// Return the string for the platform's locale; return's `null` if the locale 190 | /// can't be determined. 191 | String? getPlatformLocale() { 192 | var locale = Platform.localeName; 193 | 194 | // Convert `en_US.UTF-8` to `en_US`. 195 | var index = locale.indexOf('.'); 196 | if (index != -1) locale = locale.substring(0, index); 197 | 198 | // Convert `en_US` to `en-us`. 199 | locale = locale.replaceAll('_', '-').toLowerCase(); 200 | 201 | return locale; 202 | } 203 | -------------------------------------------------------------------------------- /lib/usage.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// `usage` is a wrapper around Google Analytics for both command-line apps 6 | /// and web apps. 7 | /// 8 | /// In order to use this library as a web app, import the `analytics_html.dart` 9 | /// library and instantiate the [AnalyticsHtml] class. 10 | /// 11 | /// In order to use this library as a command-line app, import the 12 | /// `analytics_io.dart` library and instantiate the [AnalyticsIO] class. 13 | /// 14 | /// For both classes, you need to provide a Google Analytics tracking ID, the 15 | /// application name, and the application version. 16 | /// 17 | /// Your application should provide an opt-in option for the user. If they 18 | /// opt-in, set the [optIn] field to `true`. This setting will persist across 19 | /// sessions automatically. 20 | /// 21 | /// For more information, please see the Google Analytics Measurement Protocol 22 | /// [Policy](https://developers.google.com/analytics/devguides/collection/protocol/policy). 23 | library usage; 24 | 25 | import 'dart:async'; 26 | 27 | // Matches file:/, non-ws, /, non-ws, .dart 28 | final RegExp _pathRegex = RegExp(r'file:/\S+/(\S+\.dart)'); 29 | 30 | // Match multiple tabs or spaces. 31 | final RegExp _tabOrSpaceRegex = RegExp(r'[\t ]+'); 32 | 33 | /// An interface to a Google Analytics session. 34 | /// 35 | /// [AnalyticsHtml] and [AnalyticsIO] are concrete implementations of this 36 | /// interface. [AnalyticsMock] can be used for testing or for some variants of 37 | /// an opt-in workflow. 38 | /// 39 | /// The analytics information is sent on a best-effort basis. So, failures to 40 | /// send the GA information will not result in errors from the asynchronous 41 | /// `send` methods. 42 | abstract class Analytics { 43 | /// Tracking ID / Property ID. 44 | String get trackingId; 45 | 46 | /// The application name. 47 | String? get applicationName; 48 | 49 | /// The application version. 50 | String? get applicationVersion; 51 | 52 | /// Is this the first time the tool has run? 53 | bool get firstRun; 54 | 55 | /// Whether the [Analytics] instance is configured in an opt-in or opt-out 56 | /// manner. 57 | AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut; 58 | 59 | /// Will analytics data be sent. 60 | bool get enabled; 61 | 62 | /// Enable or disable sending of analytics data. 63 | set enabled(bool value); 64 | 65 | /// Anonymous client ID in UUID v4 format. 66 | /// 67 | /// The value is randomly-generated and should be reasonably stable for the 68 | /// computer sending analytics data. 69 | String get clientId; 70 | 71 | /// Sends a screen view hit to Google Analytics. 72 | /// 73 | /// [parameters] can be any analytics key/value pair. Useful 74 | /// for custom dimensions, etc. 75 | Future sendScreenView(String viewName, 76 | {Map? parameters}); 77 | 78 | /// Sends an Event hit to Google Analytics. [label] specifies the event label. 79 | /// [value] specifies the event value. Values must be non-negative. 80 | /// 81 | /// [parameters] can be any analytics key/value pair. Useful 82 | /// for custom dimensions, etc. 83 | Future sendEvent(String category, String action, 84 | {String? label, int? value, Map? parameters}); 85 | 86 | /// Sends a Social hit to Google Analytics. 87 | /// 88 | /// [network] specifies the social network, for example Facebook or Google 89 | /// Plus. [action] specifies the social interaction action. For example on 90 | /// Google Plus when a user clicks the +1 button, the social action is 'plus'. 91 | /// [target] specifies the target of a 92 | /// social interaction. This value is typically a URL but can be any text. 93 | Future sendSocial(String network, String action, String target); 94 | 95 | /// Sends a Timing hit to Google Analytics. [variableName] specifies the 96 | /// variable name of the timing. [time] specifies the user timing value (in 97 | /// milliseconds). [category] specifies the category of the timing. [label] 98 | /// specifies the label of the timing. 99 | Future sendTiming(String variableName, int time, 100 | {String? category, String? label}); 101 | 102 | /// Start a timer. The time won't be calculated, and the analytics information 103 | /// sent, until the [AnalyticsTimer.finish] method is called. 104 | AnalyticsTimer startTimer(String variableName, 105 | {String? category, String? label}); 106 | 107 | /// In order to avoid sending any personally identifying information, the 108 | /// [description] field must not contain the exception message. In addition, 109 | /// only the first 100 chars of the description will be sent. 110 | Future sendException(String description, {bool? fatal}); 111 | 112 | /// Gets a session variable value. 113 | dynamic getSessionValue(String param); 114 | 115 | /// Sets a session variable value. The value is persistent for the life of the 116 | /// [Analytics] instance. This variable will be sent in with every analytics 117 | /// hit. A list of valid variable names can be found here: 118 | /// https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters. 119 | void setSessionValue(String param, dynamic value); 120 | 121 | /// Fires events when the usage library sends any data over the network. This 122 | /// will not fire if analytics has been disabled or if the throttling 123 | /// algorithm has been engaged. 124 | /// 125 | /// This method is public to allow library clients to more easily test their 126 | /// analytics implementations. 127 | Stream> get onSend; 128 | 129 | /// Wait for all of the outstanding analytics pings to complete. The returned 130 | /// `Future` will always complete without errors. You can pass in an optional 131 | /// `Duration` to specify to only wait for a certain amount of time. 132 | /// 133 | /// This method is particularly useful for command-line clients. Outstanding 134 | /// I/O requests will cause the VM to delay terminating the process. 135 | /// Generally, users won't want their CLI app to pause at the end of the 136 | /// process waiting for Google analytics requests to complete. This method 137 | /// allows CLI apps to delay for a short time waiting for GA requests to 138 | /// complete, and then do something like call `dart:io`'s `exit()` explicitly 139 | /// themselves (or the [close] method below). 140 | Future waitForLastPing({Duration? timeout}); 141 | 142 | /// Free any used resources. 143 | /// 144 | /// The [Analytics] instance should not be used after this call. 145 | void close(); 146 | } 147 | 148 | enum AnalyticsOpt { 149 | /// Users must opt-in before any analytics data is sent. 150 | optIn, 151 | 152 | /// Users must opt-out for analytics data to not be sent. 153 | optOut 154 | } 155 | 156 | /// An object, returned by [Analytics.startTimer], that is used to measure an 157 | /// asynchronous process. 158 | class AnalyticsTimer { 159 | final Analytics analytics; 160 | final String variableName; 161 | final String? category; 162 | final String? label; 163 | 164 | late final int _startMillis; 165 | int? _endMillis; 166 | 167 | AnalyticsTimer(this.analytics, this.variableName, 168 | {this.category, this.label}) { 169 | _startMillis = DateTime.now().millisecondsSinceEpoch; 170 | } 171 | 172 | int get currentElapsedMillis { 173 | if (_endMillis == null) { 174 | return DateTime.now().millisecondsSinceEpoch - _startMillis; 175 | } else { 176 | return _endMillis! - _startMillis; 177 | } 178 | } 179 | 180 | /// Finish the timer, calculate the elapsed time, and send the information to 181 | /// analytics. Once this is called, any future invocations are no-ops. 182 | Future finish() { 183 | if (_endMillis != null) return Future.value(); 184 | 185 | _endMillis = DateTime.now().millisecondsSinceEpoch; 186 | return analytics.sendTiming(variableName, currentElapsedMillis, 187 | category: category, label: label); 188 | } 189 | } 190 | 191 | /// A no-op implementation of the [Analytics] class. This can be used as a 192 | /// stand-in for that will never ping the GA server, or as a mock in test code. 193 | class AnalyticsMock implements Analytics { 194 | @override 195 | String get trackingId => 'UA-0'; 196 | @override 197 | String get applicationName => 'mock-app'; 198 | @override 199 | String get applicationVersion => '1.0.0'; 200 | 201 | final bool logCalls; 202 | 203 | /// Events are never added to this controller for the mock implementation. 204 | final StreamController> _sendController = 205 | StreamController.broadcast(); 206 | 207 | /// Create a new [AnalyticsMock]. If [logCalls] is true, all calls will be 208 | /// logged to stdout. 209 | AnalyticsMock([this.logCalls = false]); 210 | 211 | @override 212 | bool get firstRun => false; 213 | 214 | @override 215 | AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut; 216 | 217 | @override 218 | bool enabled = true; 219 | 220 | @override 221 | String get clientId => '00000000-0000-4000-0000-000000000000'; 222 | 223 | @override 224 | Future sendScreenView(String viewName, 225 | {Map? parameters}) { 226 | parameters ??= {}; 227 | parameters['viewName'] = viewName; 228 | return _log('screenView', parameters); 229 | } 230 | 231 | @override 232 | Future sendEvent(String category, String action, 233 | {String? label, int? value, Map? parameters}) { 234 | parameters ??= {}; 235 | return _log( 236 | 'event', 237 | {'category': category, 'action': action, 'label': label, 'value': value} 238 | ..addAll(parameters)); 239 | } 240 | 241 | @override 242 | Future sendSocial(String network, String action, String target) => 243 | _log('social', {'network': network, 'action': action, 'target': target}); 244 | 245 | @override 246 | Future sendTiming(String variableName, int time, 247 | {String? category, String? label}) { 248 | return _log('timing', { 249 | 'variableName': variableName, 250 | 'time': time, 251 | 'category': category, 252 | 'label': label 253 | }); 254 | } 255 | 256 | @override 257 | AnalyticsTimer startTimer(String variableName, 258 | {String? category, String? label}) { 259 | return AnalyticsTimer(this, variableName, category: category, label: label); 260 | } 261 | 262 | @override 263 | Future sendException(String description, {bool? fatal}) => 264 | _log('exception', {'description': description, 'fatal': fatal}); 265 | 266 | @override 267 | dynamic getSessionValue(String param) => null; 268 | 269 | @override 270 | void setSessionValue(String param, dynamic value) {} 271 | 272 | @override 273 | Stream> get onSend => _sendController.stream; 274 | 275 | @override 276 | Future waitForLastPing({Duration? timeout}) => Future.value(); 277 | 278 | @override 279 | void close() {} 280 | 281 | Future _log(String hitType, Map m) { 282 | if (logCalls) { 283 | print('analytics: $hitType $m'); 284 | } 285 | 286 | return Future.value(); 287 | } 288 | } 289 | 290 | /// Sanitize a stacktrace. This will shorten file paths in order to remove any 291 | /// PII that may be contained in the full file path. For example, this will 292 | /// shorten `file:///Users/foobar/tmp/error.dart` to `error.dart`. 293 | /// 294 | /// If [shorten] is `true`, this method will also attempt to compress the text 295 | /// of the stacktrace. GA has a 100 char limit on the text that can be sent for 296 | /// an exception. This will try and make those first 100 chars contain 297 | /// information useful to debugging the issue. 298 | String sanitizeStacktrace(dynamic st, {bool shorten = true}) { 299 | var str = '$st'; 300 | 301 | Iterable iter = _pathRegex.allMatches(str); 302 | iter = iter.toList().reversed; 303 | 304 | for (var match in iter) { 305 | var replacement = match.group(1)!; 306 | str = 307 | str.substring(0, match.start) + replacement + str.substring(match.end); 308 | } 309 | 310 | if (shorten) { 311 | // Shorten the stacktrace up a bit. 312 | str = str.replaceAll(_tabOrSpaceRegex, ' '); 313 | } 314 | 315 | return str; 316 | } 317 | -------------------------------------------------------------------------------- /lib/usage_html.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// In order to use this library import the `usage_html.dart` file and 6 | /// instantiate the [AnalyticsHtml] class. 7 | /// 8 | /// You'll need to provide a Google Analytics tracking ID, the application name, 9 | /// and the application version. 10 | library usage_html; 11 | 12 | export 'src/usage_impl_html.dart' show AnalyticsHtml; 13 | export 'usage.dart'; 14 | -------------------------------------------------------------------------------- /lib/usage_io.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// In order to use this library import the `usage_io.dart` file and 6 | /// instantiate the [AnalyticsIO] class. 7 | /// 8 | /// You'll need to provide a Google Analytics tracking ID, the application name, 9 | /// and the application version. 10 | library usage_io; 11 | 12 | export 'src/usage_impl_io.dart' show AnalyticsIO; 13 | export 'usage.dart'; 14 | -------------------------------------------------------------------------------- /lib/uuid/uuid.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// A UUID generator library. 6 | library uuid; 7 | 8 | import 'dart:math' show Random; 9 | 10 | /// A UUID generator. 11 | /// 12 | /// This will generate unique IDs in the format: 13 | /// 14 | /// f47ac10b-58cc-4372-a567-0e02b2c3d479 15 | /// 16 | /// The generated uuids are 128 bit numbers encoded in a specific string format. 17 | /// For more information, see 18 | /// [en.wikipedia.org/wiki/Universally_unique_identifier](http://en.wikipedia.org/wiki/Universally_unique_identifier). 19 | class Uuid { 20 | final Random _random = Random(); 21 | 22 | /// Generate a version 4 (random) uuid. This is a uuid scheme that only uses 23 | /// random numbers as the source of the generated uuid. 24 | String generateV4() { 25 | // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12. 26 | var special = 8 + _random.nextInt(4); 27 | 28 | return '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-' 29 | '${_bitsDigits(16, 4)}-' 30 | '4${_bitsDigits(12, 3)}-' 31 | '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-' 32 | '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}'; 33 | } 34 | 35 | String _bitsDigits(int bitCount, int digitCount) => 36 | _printDigits(_generateBits(bitCount), digitCount); 37 | 38 | int _generateBits(int bitCount) => _random.nextInt(1 << bitCount); 39 | 40 | String _printDigits(int value, int count) => 41 | value.toRadixString(16).padLeft(count, '0'); 42 | } 43 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | # for details. All rights reserved. Use of this source code is governed by a 3 | # BSD-style license that can be found in the LICENSE file. 4 | 5 | name: usage 6 | version: 4.1.1 7 | description: A Google Analytics wrapper for command-line, web, and Flutter apps. 8 | repository: https://github.com/dart-lang/usage 9 | 10 | environment: 11 | sdk: '>=2.19.0 <3.0.0' 12 | 13 | dependencies: 14 | meta: ^1.7.0 15 | path: ^1.8.0 16 | 17 | dev_dependencies: 18 | dart_flutter_team_lints: ^1.0.0 19 | test: ^1.16.0 20 | -------------------------------------------------------------------------------- /test/all.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'hit_types_test.dart' as hit_types_test; 6 | import 'usage_impl_io_test.dart' as usage_impl_io_test; 7 | import 'usage_impl_test.dart' as usage_impl_test; 8 | import 'usage_test.dart' as usage_test; 9 | import 'uuid_test.dart' as uuid_test; 10 | 11 | void main() { 12 | hit_types_test.defineTests(); 13 | usage_impl_io_test.defineTests(); 14 | usage_impl_test.defineTests(); 15 | usage_test.defineTests(); 16 | uuid_test.defineTests(); 17 | } 18 | -------------------------------------------------------------------------------- /test/hit_types_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | import 'package:test/test.dart'; 9 | 10 | import 'src/common.dart'; 11 | 12 | void main() => defineTests(); 13 | 14 | void defineTests() { 15 | group('screenView', () { 16 | test('simple', () async { 17 | var mock = createMock(); 18 | await mock.sendScreenView('main'); 19 | expect(mock.mockProperties['clientId'], isNotNull); 20 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 21 | }); 22 | test('with parameters', () async { 23 | var mock = createMock(); 24 | await mock.sendScreenView('withParams', parameters: {'cd1': 'foo'}); 25 | expect(mock.mockProperties['clientId'], isNotNull); 26 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 27 | has(mock.last, 'cd1'); 28 | }); 29 | }); 30 | 31 | group('event', () { 32 | test('simple', () async { 33 | var mock = createMock(); 34 | await mock.sendEvent('files', 'save'); 35 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 36 | was(mock.last, 'event'); 37 | has(mock.last, 'ec'); 38 | has(mock.last, 'ea'); 39 | }); 40 | 41 | test('with parameters', () async { 42 | var mock = createMock(); 43 | await mock.sendEvent('withParams', 'save', parameters: {'cd1': 'foo'}); 44 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 45 | was(mock.last, 'event'); 46 | has(mock.last, 'ec'); 47 | has(mock.last, 'ea'); 48 | has(mock.last, 'cd1'); 49 | }); 50 | 51 | test('optional args', () async { 52 | var mock = createMock(); 53 | await mock.sendEvent('files', 'save', label: 'File Save', value: 23); 54 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 55 | was(mock.last, 'event'); 56 | has(mock.last, 'ec'); 57 | has(mock.last, 'ea'); 58 | has(mock.last, 'el'); 59 | has(mock.last, 'ev'); 60 | }); 61 | }); 62 | 63 | group('social', () { 64 | test('simple', () async { 65 | var mock = createMock(); 66 | await mock.sendSocial('g+', 'plus', 'userid'); 67 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 68 | was(mock.last, 'social'); 69 | has(mock.last, 'sn'); 70 | has(mock.last, 'st'); 71 | has(mock.last, 'sa'); 72 | }); 73 | }); 74 | 75 | group('timing', () { 76 | test('simple', () async { 77 | var mock = createMock(); 78 | await mock.sendTiming('compile', 123); 79 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 80 | was(mock.last, 'timing'); 81 | has(mock.last, 'utv'); 82 | has(mock.last, 'utt'); 83 | }); 84 | 85 | test('optional args', () async { 86 | var mock = createMock(); 87 | await mock.sendTiming( 88 | 'compile', 89 | 123, 90 | category: 'Build', 91 | label: 'Compile', 92 | ); 93 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 94 | was(mock.last, 'timing'); 95 | has(mock.last, 'utv'); 96 | has(mock.last, 'utt'); 97 | has(mock.last, 'utc'); 98 | has(mock.last, 'utl'); 99 | }); 100 | 101 | test('timer', () async { 102 | var mock = createMock(); 103 | var timer = 104 | mock.startTimer('compile', category: 'Build', label: 'Compile'); 105 | 106 | await Future.delayed(Duration(milliseconds: 20)); 107 | 108 | await timer.finish(); 109 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 110 | was(mock.last, 'timing'); 111 | has(mock.last, 'utv'); 112 | has(mock.last, 'utt'); 113 | has(mock.last, 'utc'); 114 | has(mock.last, 'utl'); 115 | var time = timer.currentElapsedMillis; 116 | expect(time, greaterThan(10)); 117 | 118 | await Future.delayed(Duration(milliseconds: 10)); 119 | expect(timer.currentElapsedMillis, time); 120 | }); 121 | }); 122 | 123 | group('exception', () { 124 | test('simple', () async { 125 | var mock = createMock(); 126 | await mock.sendException('FooException'); 127 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 128 | was(mock.last, 'exception'); 129 | has(mock.last, 'exd'); 130 | }); 131 | 132 | test('optional args', () async { 133 | var mock = createMock(); 134 | await mock.sendException('FooException', fatal: true); 135 | expect(mock.mockPostHandler.sentValues, isNot(isEmpty)); 136 | was(mock.last, 'exception'); 137 | has(mock.last, 'exd'); 138 | has(mock.last, 'exf'); 139 | }); 140 | 141 | test('exception file paths', () async { 142 | var mock = createMock(); 143 | await mock 144 | .sendException('foo bar (file:///Users/foobar/tmp/error.dart:3:13)'); 145 | expect( 146 | (jsonDecode(mock.last) as Map)['exd'], 'foo bar ('); 147 | }); 148 | }); 149 | } 150 | -------------------------------------------------------------------------------- /test/src/common.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | import 'package:test/test.dart'; 9 | import 'package:usage/src/usage_impl.dart'; 10 | 11 | AnalyticsImplMock createMock({Map? props}) => 12 | AnalyticsImplMock('UA-0', props: props); 13 | 14 | void was(String m, String type) => 15 | expect((jsonDecode(m) as Map)['t'], type); 16 | void has(String m, String key) => 17 | expect((jsonDecode(m) as Map)[key], isNotNull); 18 | void hasnt(String m, String key) => 19 | expect((jsonDecode(m) as Map)[key], isNull); 20 | 21 | class AnalyticsImplMock extends AnalyticsImpl { 22 | MockProperties get mockProperties => properties as MockProperties; 23 | MockPostHandler get mockPostHandler => postHandler as MockPostHandler; 24 | 25 | AnalyticsImplMock(String trackingId, {Map? props}) 26 | : super(trackingId, MockProperties(props), MockPostHandler(), 27 | applicationName: 'Test App', applicationVersion: '0.1'); 28 | 29 | String get last => mockPostHandler.last; 30 | } 31 | 32 | class StallingAnalyticsImplMock extends AnalyticsImpl { 33 | StallingAnalyticsImplMock(String trackingId, {Map? props}) 34 | : super(trackingId, MockProperties(props), StallingPostHandler(), 35 | applicationName: 'Test App', applicationVersion: '0.1'); 36 | } 37 | 38 | class StallingPostHandler extends PostHandler { 39 | @override 40 | void close() {} 41 | 42 | @override 43 | String encodeHit(Map hit) => jsonEncode(hit); 44 | 45 | @override 46 | Future sendPost(String url, List batch) => 47 | Completer().future; 48 | } 49 | 50 | class MockProperties extends PersistentProperties { 51 | Map props = {}; 52 | 53 | MockProperties([Map? props]) : super('mock') { 54 | if (props != null) this.props.addAll(props); 55 | } 56 | 57 | @override 58 | dynamic operator [](String key) => props[key]; 59 | 60 | @override 61 | void operator []=(String key, dynamic value) { 62 | props[key] = value; 63 | } 64 | 65 | @override 66 | void syncSettings() {} 67 | } 68 | 69 | class MockPostHandler extends PostHandler { 70 | List sentValues = []; 71 | 72 | @override 73 | Future sendPost(String url, List batch) { 74 | sentValues.addAll(batch); 75 | 76 | return Future.value(); 77 | } 78 | 79 | String get last => sentValues.last; 80 | 81 | @override 82 | void close() {} 83 | 84 | @override 85 | String encodeHit(Map hit) => jsonEncode(hit); 86 | } 87 | -------------------------------------------------------------------------------- /test/usage_impl_io_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @TestOn('!browser') 6 | library usage.usage_impl_io_test; 7 | 8 | import 'dart:async'; 9 | import 'dart:io'; 10 | 11 | import 'package:test/test.dart'; 12 | import 'package:usage/src/usage_impl_io.dart'; 13 | 14 | void main() => defineTests(); 15 | 16 | void defineTests() { 17 | group('IOPostHandler', () { 18 | test('sendPost', () async { 19 | var mockClient = MockHttpClient(); 20 | 21 | var postHandler = IOPostHandler(client: mockClient); 22 | var args = [ 23 | {'utv': 'varName', 'utt': '123'}, 24 | ]; 25 | await postHandler.sendPost( 26 | 'http://www.google.com', args.map(postHandler.encodeHit).toList()); 27 | expect(mockClient.requests.single.buffer.toString(), ''' 28 | Request to http://www.google.com with ${createUserAgent()} 29 | utv=varName&utt=123'''); 30 | expect(mockClient.requests.single.response.drained, isTrue); 31 | }); 32 | }); 33 | 34 | group('IOPersistentProperties', () { 35 | test('add', () { 36 | var props = IOPersistentProperties('foo_props'); 37 | props['foo'] = 'bar'; 38 | expect(props['foo'], 'bar'); 39 | }); 40 | 41 | test('remove', () { 42 | var props = IOPersistentProperties('foo_props'); 43 | props['foo'] = 'bar'; 44 | expect(props['foo'], 'bar'); 45 | props['foo'] = null; 46 | expect(props['foo'], null); 47 | }); 48 | }); 49 | 50 | group('usage_impl_io', () { 51 | test('getDartVersion', () { 52 | expect(getDartVersion(), isNotNull); 53 | }); 54 | 55 | test('getPlatformLocale', () { 56 | expect(getPlatformLocale(), isNotNull); 57 | }); 58 | }); 59 | 60 | group('batching', () { 61 | test('with 0 batch-delay hits from the same sync span are batched together', 62 | () async { 63 | var mockClient = MockHttpClient(); 64 | 65 | final analytics = AnalyticsIO(' requests = []; 136 | @override 137 | String? userAgent; 138 | bool closed = false; 139 | 140 | MockHttpClient(); 141 | 142 | @override 143 | Future postUrl(Uri uri) async { 144 | if (closed) throw StateError('Posting after close'); 145 | final request = MockHttpClientRequest(); 146 | request.buffer.writeln('Request to $uri with $userAgent'); 147 | requests.add(request); 148 | return request; 149 | } 150 | 151 | @override 152 | void close({bool force = false}) { 153 | if (closed) throw StateError('Double close'); 154 | closed = true; 155 | } 156 | 157 | @override 158 | dynamic noSuchMethod(Invocation invocation) { 159 | throw UnimplementedError('Unexpected call'); 160 | } 161 | } 162 | 163 | class MockHttpClientRequest implements HttpClientRequest { 164 | final buffer = StringBuffer(); 165 | final MockHttpClientResponse response = MockHttpClientResponse(); 166 | bool closed = false; 167 | 168 | MockHttpClientRequest(); 169 | 170 | @override 171 | void write(Object? o) { 172 | buffer.write(o); 173 | } 174 | 175 | @override 176 | Future close() async { 177 | if (closed) throw StateError('Double close'); 178 | closed = true; 179 | return response; 180 | } 181 | 182 | @override 183 | dynamic noSuchMethod(Invocation invocation) { 184 | throw UnimplementedError('Unexpected call'); 185 | } 186 | } 187 | 188 | class MockHttpClientResponse implements HttpClientResponse { 189 | bool drained = false; 190 | MockHttpClientResponse(); 191 | 192 | @override 193 | Future drain([E? futureValue]) async { 194 | drained = true; 195 | return futureValue as E; 196 | } 197 | 198 | @override 199 | dynamic noSuchMethod(Invocation invocation) { 200 | throw UnimplementedError('Unexpected call'); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /test/usage_impl_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:test/test.dart'; 6 | import 'package:usage/src/usage_impl.dart'; 7 | 8 | import 'src/common.dart'; 9 | 10 | void main() => defineTests(); 11 | 12 | void defineTests() { 13 | group('ThrottlingBucket', () { 14 | test('can send', () { 15 | var bucket = ThrottlingBucket(20); 16 | expect(bucket.removeDrop(), true); 17 | }); 18 | 19 | test('doesn\'t send too many', () { 20 | var bucket = ThrottlingBucket(20); 21 | for (var i = 0; i < 20; i++) { 22 | expect(bucket.removeDrop(), true); 23 | } 24 | expect(bucket.removeDrop(), false); 25 | }); 26 | 27 | test('does re-send after throttling', () async { 28 | var bucket = ThrottlingBucket(20); 29 | for (var i = 0; i < 20; i++) { 30 | expect(bucket.removeDrop(), true); 31 | } 32 | expect(bucket.removeDrop(), false); 33 | 34 | // TODO: Re-write to use package:fake_async. 35 | await Future.delayed(Duration(milliseconds: 1500)); 36 | expect(bucket.removeDrop(), true); 37 | }); 38 | }); 39 | 40 | group('AnalyticsImpl', () { 41 | test('trackingId', () { 42 | var mock = createMock(); 43 | expect(mock.trackingId, isNotNull); 44 | }); 45 | 46 | test('applicationName', () { 47 | var mock = createMock(); 48 | expect(mock.applicationName, isNotNull); 49 | }); 50 | 51 | test('applicationVersion', () { 52 | var mock = createMock(); 53 | expect(mock.applicationVersion, isNotNull); 54 | }); 55 | 56 | test('respects disabled', () { 57 | var mock = createMock(); 58 | mock.enabled = false; 59 | mock.sendException('FooBar exception'); 60 | expect(mock.enabled, false); 61 | expect(mock.mockPostHandler.sentValues, isEmpty); 62 | }); 63 | 64 | test('firstRun', () { 65 | var mock = createMock(); 66 | expect(mock.firstRun, true); 67 | mock = createMock(props: {'firstRun': false}); 68 | expect(mock.firstRun, false); 69 | }); 70 | 71 | test('setSessionValue', () async { 72 | var mock = createMock(); 73 | await mock.sendScreenView('foo'); 74 | hasnt(mock.last, 'val'); 75 | mock.setSessionValue('val', 'ue'); 76 | await mock.sendScreenView('bar'); 77 | has(mock.last, 'val'); 78 | mock.setSessionValue('val', null); 79 | await mock.sendScreenView('baz'); 80 | hasnt(mock.last, 'val'); 81 | }); 82 | 83 | test('waitForLastPing', () { 84 | var mock = createMock(); 85 | mock.sendScreenView('foo'); 86 | mock.sendScreenView('bar'); 87 | mock.sendScreenView('baz'); 88 | return mock.waitForLastPing(timeout: Duration(milliseconds: 100)); 89 | }); 90 | 91 | test('waitForLastPing times out', () async { 92 | var mock = StallingAnalyticsImplMock('blahID'); 93 | // ignore: unawaited_futures 94 | mock.sendScreenView('foo'); 95 | await mock.waitForLastPing(timeout: Duration(milliseconds: 100)); 96 | }); 97 | 98 | group('clientId', () { 99 | test('is available immediately', () { 100 | var mock = createMock(); 101 | expect(mock.clientId, isNotEmpty); 102 | }); 103 | 104 | test('is memoized', () { 105 | var mock = createMock(); 106 | final value1 = mock.clientId; 107 | final value2 = mock.clientId; 108 | expect(value1, isNotEmpty); 109 | expect(value1, value2); 110 | }); 111 | 112 | test('is stored in properties', () { 113 | var mock = createMock(); 114 | expect(mock.properties['clientId'], isNull); 115 | final value = mock.clientId; 116 | expect(mock.properties['clientId'], value); 117 | }); 118 | }); 119 | }); 120 | 121 | group('postEncode', () { 122 | test('simple', () { 123 | var map = {'foo': 'bar', 'baz': 'qux norf'}; 124 | expect(postEncode(map), 'foo=bar&baz=qux%20norf'); 125 | }); 126 | }); 127 | } 128 | -------------------------------------------------------------------------------- /test/usage_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:test/test.dart'; 6 | import 'package:usage/usage.dart'; 7 | 8 | void main() => defineTests(); 9 | 10 | void defineTests() { 11 | group('AnalyticsMock', () { 12 | test('simple', () { 13 | var mock = AnalyticsMock(); 14 | mock.sendScreenView('main'); 15 | mock.sendScreenView('withParameters', parameters: {'cd1': 'custom'}); 16 | mock.sendEvent('files', 'save'); 17 | mock.sendEvent('eventWithParameters', 'save', 18 | parameters: {'cd1': 'custom'}); 19 | mock.sendSocial('g+', 'plus', 'userid'); 20 | mock.sendTiming('compile', 123); 21 | mock.startTimer('compile').finish(); 22 | mock.sendException('FooException'); 23 | mock.setSessionValue('val', 'ue'); 24 | return mock.waitForLastPing(); 25 | }); 26 | }); 27 | 28 | group('sanitizeStacktrace', () { 29 | test('replace file', () { 30 | expect( 31 | sanitizeStacktrace('(file:///Users/foo/tmp/error.dart:3:13)', 32 | shorten: false), 33 | '(error.dart:3:13)'); 34 | }); 35 | 36 | test('replace files', () { 37 | expect( 38 | sanitizeStacktrace( 39 | 'foo (file:///Users/foo/tmp/error.dart:3:13)\n' 40 | 'bar (file:///Users/foo/tmp/error.dart:3:13)', 41 | shorten: false), 42 | 'foo (error.dart:3:13)\nbar (error.dart:3:13)'); 43 | }); 44 | 45 | test('shorten 1', () { 46 | expect(sanitizeStacktrace('(file:///Users/foo/tmp/error.dart:3:13)'), 47 | '(error.dart:3:13)'); 48 | }); 49 | 50 | test('shorten 2', () { 51 | expect( 52 | sanitizeStacktrace('foo (file:///Users/foo/tmp/error.dart:3:13)\n' 53 | 'bar (file:///Users/foo/tmp/error.dart:3:13)'), 54 | 'foo (error.dart:3:13)\nbar (error.dart:3:13)'); 55 | }); 56 | 57 | test('shorten 3', () { 58 | expect( 59 | sanitizeStacktrace('foo (package:foo/foo.dart:3:13)\n' 60 | 'bar (dart:async/schedule_microtask.dart:41)'), 61 | 'foo (package:foo/foo.dart:3:13)\nbar (dart:async/schedule_microtask.dart:41)'); 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/uuid_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:test/test.dart'; 6 | import 'package:usage/uuid/uuid.dart'; 7 | 8 | void main() => defineTests(); 9 | 10 | void defineTests() { 11 | group('uuid', () { 12 | // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx 13 | test('simple', () { 14 | var uuid = Uuid(); 15 | var result = uuid.generateV4(); 16 | expect(result.length, 36); 17 | expect(result[8], '-'); 18 | expect(result[13], '-'); 19 | expect(result[18], '-'); 20 | expect(result[23], '-'); 21 | }); 22 | 23 | test('can parse', () { 24 | var uuid = Uuid(); 25 | var result = uuid.generateV4(); 26 | expect(int.parse(result.substring(0, 8), radix: 16), isNotNull); 27 | expect(int.parse(result.substring(9, 13), radix: 16), isNotNull); 28 | expect(int.parse(result.substring(14, 18), radix: 16), isNotNull); 29 | expect(int.parse(result.substring(19, 23), radix: 16), isNotNull); 30 | expect(int.parse(result.substring(24, 36), radix: 16), isNotNull); 31 | }); 32 | 33 | test('special bits', () { 34 | var uuid = Uuid(); 35 | var result = uuid.generateV4(); 36 | expect(result[14], '4'); 37 | expect(result[19].toLowerCase(), isIn('89ab')); 38 | 39 | result = uuid.generateV4(); 40 | expect(result[19].toLowerCase(), isIn('89ab')); 41 | 42 | result = uuid.generateV4(); 43 | expect(result[19].toLowerCase(), isIn('89ab')); 44 | }); 45 | 46 | test('is pretty random', () { 47 | var set = {}; 48 | 49 | var uuid = Uuid(); 50 | for (var i = 0; i < 64; i++) { 51 | var val = uuid.generateV4(); 52 | expect(set, isNot(contains(val))); 53 | set.add(val); 54 | } 55 | 56 | uuid = Uuid(); 57 | for (var i = 0; i < 64; i++) { 58 | var val = uuid.generateV4(); 59 | expect(set, isNot(contains(val))); 60 | set.add(val); 61 | } 62 | 63 | uuid = Uuid(); 64 | for (var i = 0; i < 64; i++) { 65 | var val = uuid.generateV4(); 66 | expect(set, isNot(contains(val))); 67 | set.add(val); 68 | } 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /test/web.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | Usage Tests 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/web_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @TestOn('browser') 6 | library usage.web_test; 7 | 8 | import 'dart:async'; 9 | import 'dart:html'; 10 | 11 | import 'package:test/test.dart'; 12 | import 'package:usage/src/usage_impl_html.dart'; 13 | 14 | import 'hit_types_test.dart' as hit_types_test; 15 | import 'usage_impl_test.dart' as usage_impl_test; 16 | import 'usage_test.dart' as usage_test; 17 | import 'uuid_test.dart' as uuid_test; 18 | 19 | void main() { 20 | // Define the tests. 21 | hit_types_test.defineTests(); 22 | usage_test.defineTests(); 23 | usage_impl_test.defineTests(); 24 | uuid_test.defineTests(); 25 | 26 | // Define some web specific tests. 27 | defineWebTests(); 28 | } 29 | 30 | void defineWebTests() { 31 | group('HtmlPostHandler', () { 32 | test('sendPost', () async { 33 | var client = MockRequestor(); 34 | var postHandler = HtmlPostHandler(mockRequestor: client.request); 35 | var args = [ 36 | {'utv': 'varName', 'utt': '123'}, 37 | ]; 38 | 39 | await postHandler.sendPost( 40 | 'http://www.google.com', args.map(postHandler.encodeHit).toList()); 41 | expect(client.sendCount, 1); 42 | }); 43 | }); 44 | 45 | group('HtmlPersistentProperties', () { 46 | test('add', () { 47 | var props = HtmlPersistentProperties('foo_props'); 48 | props['foo'] = 'bar'; 49 | expect(props['foo'], 'bar'); 50 | }); 51 | 52 | test('remove', () { 53 | var props = HtmlPersistentProperties('foo_props'); 54 | props['foo'] = 'bar'; 55 | expect(props['foo'], 'bar'); 56 | props['foo'] = null; 57 | expect(props['foo'], null); 58 | }); 59 | }); 60 | } 61 | 62 | class MockRequestor { 63 | int sendCount = 0; 64 | 65 | Future request(String url, {String? method, Object? sendData}) { 66 | expect(url, isNotEmpty); 67 | expect(method, isNotEmpty); 68 | expect(sendData, isNotEmpty); 69 | 70 | sendCount++; 71 | return Future.value(HttpRequest()); 72 | } 73 | } 74 | --------------------------------------------------------------------------------