Merge pull request #92 from dart-lang/usage_3.0

Usage 3.0
diff --git a/changelog.md b/changelog.md
index 68d72da..71df07c 100644
--- a/changelog.md
+++ b/changelog.md
@@ -5,6 +5,8 @@
 - removed the Flutter specific entry-point; Flutter apps can now use the
   regular `dart:io` entrypoint (AnalyticsIO)
 - moved the uuid library from `lib/src/` to `lib/uuid/`
+- fixed an issue with reporting the user language for the dart:io provider
+- send additional lines for reported exceptions
 
 ## 2.2.2
 - adjust the Flutter usage client to Flutter API changes
diff --git a/example/ga.dart b/example/ga.dart
index 3dfada1..bf0807e 100644
--- a/example/ga.dart
+++ b/example/ga.dart
@@ -23,7 +23,8 @@
 
   await ga.sendScreenView('home');
   await ga.sendScreenView('files');
-  await ga.sendException('foo exception, line 123:56');
+  await ga
+      .sendException('foo error:\n${sanitizeStacktrace(StackTrace.current)}');
   await ga.sendTiming('writeDuration', 123);
   await ga.sendEvent('create', 'consoleapp', label: 'Console App');
   print('pinged ${ua}');
diff --git a/lib/src/usage_impl.dart b/lib/src/usage_impl.dart
index 5953a74..105fe36 100644
--- a/lib/src/usage_impl.dart
+++ b/lib/src/usage_impl.dart
@@ -8,8 +8,6 @@
 import '../usage.dart';
 import '../uuid/uuid.dart';
 
-final int _MAX_EXCEPTION_LENGTH = 100;
-
 String postEncode(Map<String, dynamic> map) {
   // &foo=bar
   return map.keys.map((key) {
@@ -157,6 +155,10 @@
   }
 
   Future sendException(String description, {bool fatal}) {
+    // We trim exceptions to a max length; google analytics will apply it's own
+    // truncation, likely around 150 chars or so.
+    const int maxExceptionLength = 1000;
+
     // In order to ensure that the client of this API is not sending any PII
     // data, we strip out any stack trace that may reference a path on the
     // user's drive (file:/...).
@@ -164,8 +166,10 @@
       description = description.substring(0, description.indexOf('file:/'));
     }
 
-    if (description != null && description.length > _MAX_EXCEPTION_LENGTH) {
-      description = description.substring(0, _MAX_EXCEPTION_LENGTH);
+    description = description.replaceAll('\n', '; ');
+
+    if (description.length > maxExceptionLength) {
+      description = description.substring(0, maxExceptionLength);
     }
 
     Map<String, dynamic> args = {'exd': description};
diff --git a/lib/src/usage_impl_html.dart b/lib/src/usage_impl_html.dart
index c227906..7752fe5 100644
--- a/lib/src/usage_impl_html.dart
+++ b/lib/src/usage_impl_html.dart
@@ -8,6 +8,10 @@
 
 import 'usage_impl.dart';
 
+/// An interface to a Google Analytics session, suitable for use in web apps.
+///
+/// [analyticsUrl] is an optional replacement for the default Google Analytics
+/// URL (`https://www.google-analytics.com/collect`).
 class AnalyticsHtml extends AnalyticsImpl {
   AnalyticsHtml(
       String trackingId, String applicationName, String applicationVersion,
diff --git a/lib/src/usage_impl_io.dart b/lib/src/usage_impl_io.dart
index c062732..fe5cc44 100644
--- a/lib/src/usage_impl_io.dart
+++ b/lib/src/usage_impl_io.dart
@@ -10,11 +10,14 @@
 
 import 'usage_impl.dart';
 
-/// Create a new Analytics instance.
+/// An interface to a Google Analytics session, suitable for use in command-line
+/// applications.
 ///
 /// `trackingId`, `applicationName`, and `applicationVersion` values should be supplied.
 /// `analyticsUrl` is optional, and lets user's substitute their own analytics URL for
-/// the default. `documentDirectory` is where the analytics settings are stored. It
+/// the default.
+///
+/// `documentDirectory` is where the analytics settings are stored. It
 /// defaults to the user home directory. For regular `dart:io` apps this doesn't need to
 /// be supplied. For Flutter applications, you should pass in a value like
 /// `PathProvider.getApplicationDocumentsDirectory()`.
@@ -29,7 +32,12 @@
             new IOPostHandler(),
             applicationName: applicationName,
             applicationVersion: applicationVersion,
-            analyticsUrl: analyticsUrl);
+            analyticsUrl: analyticsUrl) {
+    final String locale = getPlatformLocale();
+    if (locale != null) {
+      setSessionValue('ul', locale);
+    }
+  }
 }
 
 String _createUserAgent() {
@@ -136,9 +144,8 @@
     int index = locale.indexOf('.');
     if (index != null) locale = locale.substring(0, index);
 
-    // Convert `en_US` to `en`.
-    index = locale.indexOf('_');
-    if (index != null) locale = locale.substring(0, index);
+    // Convert `en_US` to `en-us`.
+    locale = locale.replaceAll('_', '-').toLowerCase();
   }
 
   return locale;
diff --git a/lib/usage.dart b/lib/usage.dart
index 108200f..13f98d6 100644
--- a/lib/usage.dart
+++ b/lib/usage.dart
@@ -29,6 +29,9 @@
 // Matches file:/, non-ws, /, non-ws, .dart
 final RegExp _pathRegex = new RegExp(r'file:/\S+/(\S+\.dart)');
 
+// Match multiple tabs or spaces.
+final RegExp _tabOrSpaceRegex = new RegExp(r'[\t ]+');
+
 /**
  * An interface to a Google Analytics session. [AnalyticsHtml] and [AnalyticsIO]
  * are concrete implementations of this interface. [AnalyticsMock] can be used
@@ -297,10 +300,7 @@
 
   if (shorten) {
     // Shorten the stacktrace up a bit.
-    str = str
-        .replaceAll('(package:', '(')
-        .replaceAll('(dart:', '(')
-        .replaceAll(new RegExp(r'\s+'), ' ');
+    str = str.replaceAll(_tabOrSpaceRegex, ' ');
   }
 
   return str;
diff --git a/lib/usage_html.dart b/lib/usage_html.dart
index 80247b1..569c1eb 100644
--- a/lib/usage_html.dart
+++ b/lib/usage_html.dart
@@ -2,42 +2,12 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-/**
- * In order to use this library import the `usage_html.dart` file and
- * instantiate the [AnalyticsHtml] class.
- *
- * You'll need to provide a Google Analytics tracking ID, the application name,
- * and the application version.
- */
+/// In order to use this library import the `usage_html.dart` file and
+/// instantiate the [AnalyticsHtml] class.
+///
+/// You'll need to provide a Google Analytics tracking ID, the application name,
+/// and the application version.
 library usage_html;
 
-import 'dart:html';
-
-import 'src/usage_impl.dart';
-import 'src/usage_impl_html.dart';
-
+export 'src/usage_impl_html.dart' show AnalyticsHtml;
 export 'usage.dart';
-
-/**
- * An interface to a Google Analytics session, suitable for use in web apps.
- *
- * [analyticsUrl] is an optional replacement for the default Google Analytics
- * URL (`https://www.google-analytics.com/collect`).
- */
-class AnalyticsHtml extends AnalyticsImpl {
-  AnalyticsHtml(
-      String trackingId, String applicationName, String applicationVersion,
-      {String analyticsUrl})
-      : super(trackingId, new HtmlPersistentProperties(applicationName),
-            new HtmlPostHandler(),
-            applicationName: applicationName,
-            applicationVersion: applicationVersion,
-            analyticsUrl: analyticsUrl) {
-    int screenWidth = window.screen.width;
-    int screenHeight = window.screen.height;
-
-    setSessionValue('sr', '${screenWidth}x$screenHeight');
-    setSessionValue('sd', '${window.screen.pixelDepth}-bits');
-    setSessionValue('ul', window.navigator.language);
-  }
-}
diff --git a/lib/usage_io.dart b/lib/usage_io.dart
index 6a015e5..5e35e94 100644
--- a/lib/usage_io.dart
+++ b/lib/usage_io.dart
@@ -2,34 +2,12 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-/**
- * In order to use this library import the `usage_io.dart` file and
- * instantiate the [AnalyticsIO] class.
- *
- * You'll need to provide a Google Analytics tracking ID, the application name,
- * and the application version.
- */
+/// In order to use this library import the `usage_io.dart` file and
+/// instantiate the [AnalyticsIO] class.
+///
+/// You'll need to provide a Google Analytics tracking ID, the application name,
+/// and the application version.
 library usage_io;
 
-import 'src/usage_impl.dart';
-import 'src/usage_impl_io.dart';
-
+export 'src/usage_impl_io.dart' show AnalyticsIO;
 export 'usage.dart';
-
-/**
- * An interface to a Google Analytics session, suitable for use in command-line
- * applications.
- *
- * [analyticsUrl] is an optional replacement for the default Google Analytics
- * URL (`https://www.google-analytics.com/collect`).
- */
-class AnalyticsIO extends AnalyticsImpl {
-  AnalyticsIO(
-      String trackingId, String applicationName, String applicationVersion,
-      {String analyticsUrl})
-      : super(trackingId, new IOPersistentProperties(applicationName),
-            new IOPostHandler(),
-            applicationName: applicationName,
-            applicationVersion: applicationVersion,
-            analyticsUrl: analyticsUrl);
-}
diff --git a/lib/uuid/uuid.dart b/lib/uuid/uuid.dart
index 5261c8f..eaafeb2 100644
--- a/lib/uuid/uuid.dart
+++ b/lib/uuid/uuid.dart
@@ -2,31 +2,25 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-/**
- * A UUID generator library.
- */
+/// A UUID generator library.
 library uuid;
 
 import 'dart:math' show Random;
 
-/**
- * A UUID generator.
- *
- * This will generate unique IDs in the format:
- *
- *     f47ac10b-58cc-4372-a567-0e02b2c3d479
- *
- * The generated uuids are 128 bit numbers encoded in a specific string format.
- * For more information, see
- * [en.wikipedia.org/wiki/Universally_unique_identifier](http://en.wikipedia.org/wiki/Universally_unique_identifier).
- */
+/// A UUID generator.
+///
+/// This will generate unique IDs in the format:
+///
+///     f47ac10b-58cc-4372-a567-0e02b2c3d479
+///
+/// The generated uuids are 128 bit numbers encoded in a specific string format.
+/// For more information, see
+/// [en.wikipedia.org/wiki/Universally_unique_identifier](http://en.wikipedia.org/wiki/Universally_unique_identifier).
 class Uuid {
-  Random _random = new Random();
+  final Random _random = new Random();
 
-  /**
-   * Generate a version 4 (random) uuid. This is a uuid scheme that only uses
-   * random numbers as the source of the generated uuid.
-   */
+  /// Generate a version 4 (random) uuid. This is a uuid scheme that only uses
+  /// random numbers as the source of the generated uuid.
   String generateV4() {
     // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12.
     int special = 8 + _random.nextInt(4);
diff --git a/test/hit_types_test.dart b/test/hit_types_test.dart
index ab78e38..7aab2d3 100644
--- a/test/hit_types_test.dart
+++ b/test/hit_types_test.dart
@@ -123,12 +123,5 @@
       mock.sendException('foo bar (file:///Users/foobar/tmp/error.dart:3:13)');
       expect(mock.last['exd'], 'foo bar (');
     });
-
-    test('long description trimmed', () {
-      String str = '0123456789abcdefghijklmnopqrstuvwxyz';
-      AnalyticsImplMock mock = createMock();
-      mock.sendException(str + str + str + str + str);
-      expect(mock.last['exd'].length, 100);
-    });
   });
 }
diff --git a/test/usage_test.dart b/test/usage_test.dart
index c0e5664..8c4cedb 100644
--- a/test/usage_test.dart
+++ b/test/usage_test.dart
@@ -50,14 +50,14 @@
       expect(
           sanitizeStacktrace('foo (file:///Users/foo/tmp/error.dart:3:13)\n'
               'bar (file:///Users/foo/tmp/error.dart:3:13)'),
-          'foo (error.dart:3:13) bar (error.dart:3:13)');
+          'foo (error.dart:3:13)\nbar (error.dart:3:13)');
     });
 
     test('shorten 3', () {
       expect(
           sanitizeStacktrace('foo (package:foo/foo.dart:3:13)\n'
               'bar (dart:async/schedule_microtask.dart:41)'),
-          'foo (foo/foo.dart:3:13) bar (async/schedule_microtask.dart:41)');
+          'foo (package:foo/foo.dart:3:13)\nbar (dart:async/schedule_microtask.dart:41)');
     });
   });
 }