Handle batching implicit waiting for a 'batchDelay' future
diff --git a/lib/src/usage_impl.dart b/lib/src/usage_impl.dart
index 8adf980..83c8798 100644
--- a/lib/src/usage_impl.dart
+++ b/lib/src/usage_impl.dart
@@ -3,8 +3,11 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
+import 'dart:collection';
import 'dart:math' as math;
+import 'package:pedantic/pedantic.dart';
+
import '../usage.dart';
import '../uuid/uuid.dart';
@@ -78,6 +81,10 @@
@override
AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut;
+ late Future<void>? Function() _batchingDelay;
+ final Queue<String> _batchedEvents = Queue<String>();
+ bool _isAwaitingSending = false;
+
late final String _url;
late final String _batchingUrl;
@@ -92,12 +99,14 @@
this.applicationVersion,
String? analyticsUrl,
String? analyticsBatchingUrl,
+ Future<void>? Function()? batchingDelay,
}) {
if (applicationName != null) setSessionValue('an', applicationName);
if (applicationVersion != null) setSessionValue('av', applicationVersion);
_url = analyticsUrl ?? _defaultAnalyticsUrl;
_batchingUrl = analyticsBatchingUrl ?? _defaultAnalyticsBatchingUrl;
+ _batchingDelay = batchingDelay ?? () => Future(() {});
}
bool? _firstRun;
@@ -199,24 +208,6 @@
return _enqueuePayload('exception', args);
}
- static const _batchingKey = #_batching;
-
- @override
- Future<T> withBatching<T>(
- FutureOr<T> Function() callback, {
- int maxEventsPerBatch = 20,
- }) async {
- final queue = _BatchingQueue(maxEventsPerBatch: maxEventsPerBatch);
- return await runZoned(() async {
- final result = await callback();
- if (queue.enqueuedEvents.isNotEmpty) {
- // Send any remaining events.
- _trySendBatch(queue.takeBatch());
- }
- return result;
- }, zoneValues: {_batchingKey: queue});
- }
-
@override
dynamic getSessionValue(String param) => _variableMap[param];
@@ -258,14 +249,16 @@
return _enqueuePayload(hitType, args);
}
- /// Puts a single event in the queue. If batching is not enabled it will be
- /// send immediately - otherwise when the batch is full (20 events) or when
- /// the batching callback is over.
+ /// Puts a single hit in the queue. If the queue was empty - start waiting
+ /// for the result of [_batchingDelay] before sending all enqueued events.
+ ///
/// Valid values for [hitType] are: 'pageview', 'screenview', 'event',
/// 'transaction', 'item', 'social', 'exception', and 'timing'.
Future<void> _enqueuePayload(String hitType, Map<String, String> args) async {
if (!enabled) return;
-
+ // TODO(sigurdm): Really all the 'send' methods should not return Futures
+ // there is not much point in waiting for it. Only [waitForLastPing].
+ final completer = Completer<void>();
final eventArgs = <String, String>{
...args,
..._variableMap,
@@ -275,40 +268,51 @@
't': hitType,
};
- final batch = <Map<String, String>>[];
+ _sendController.add(eventArgs);
- // See if we currently are batching events:
- final batchingQueue = Zone.current[_batchingKey];
- if (batchingQueue is _BatchingQueue) {
- // Add the current event to the batch.
- batchingQueue.enqueuedEvents.add(eventArgs);
- if (!batchingQueue.isFull) {
- // Queue not full yet. Do nothing.
- return;
+ _batchedEvents.add(postHandler.encodeHit(eventArgs));
+
+ if (!_isAwaitingSending) {
+ final delay = _batchingDelay();
+ if (delay == null) {
+ _trySendBatches(completer);
+ } else {
+ _isAwaitingSending = true;
+ unawaited(delay.then((value) {
+ _trySendBatches(completer);
+ _isAwaitingSending = false;
+ }));
}
- // We have a full batch, start a new one.
- batch.addAll(batchingQueue.takeBatch());
- } else {
- batch.add(eventArgs);
}
- _trySendBatch(batch);
+ return completer.future;
}
- void _trySendBatch(List<Map<String, String>> batch) {
- if (_bucket.removeDrop()) {
- for (final args in batch) {
- _sendController.add(args);
- }
+ // Send no more than 20 messages per batch.
+ static const _maxHitsPerBatch = 20;
+ // Send no more than 16K per batch.
+ static const _maxBytesPerBatch = 16000;
- // See if we currently are batching events:
- final batchingQueue = Zone.current[_batchingKey];
- if (batchingQueue is _BatchingQueue) {
- _recordFuture(postHandler.sendPost(_batchingUrl, batch));
- } else {
- assert(batch.length == 1);
- _recordFuture(postHandler.sendPost(_url, batch));
+ void _trySendBatches(Completer completer) {
+ final futures = <Future>[];
+ while (_batchedEvents.isNotEmpty) {
+ final batch = <String>[];
+ final totalLength = 0;
+
+ while (true) {
+ if (_batchedEvents.isEmpty) break;
+ if (totalLength + _batchedEvents.first.length > _maxBytesPerBatch) {
+ break;
+ }
+ batch.add(_batchedEvents.removeFirst());
+ if (batch.length == _maxHitsPerBatch) break;
+ }
+ if (_bucket.removeDrop()) {
+ final future = postHandler.sendPost(
+ batch.length == 1 ? _url : _batchingUrl, batch);
+ _recordFuture(future);
}
}
+ Future.wait(futures).then((_) => completer.complete());
}
void _recordFuture(Future f) {
@@ -348,26 +352,9 @@
/// The `Future` from [sendPost] should complete when the operation is finished,
/// but failures to send the information should be silent.
abstract class PostHandler {
- Future sendPost(String url, List<Map<String, dynamic>> batch);
+ Future sendPost(String url, List<String> batch);
+ String encodeHit(Map<String, String> hit);
/// Free any used resources.
void close();
}
-
-class _BatchingQueue {
- List<Map<String, String>> enqueuedEvents = [];
- int maxEventsPerBatch;
-
- _BatchingQueue({required this.maxEventsPerBatch});
-
- bool get isFull => enqueuedEvents.length >= maxEventsPerBatch;
- List<Map<String, String>> takeBatch() {
- // TODO(sigurdm): We should take size limits on batching into account.
- // https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide#batch-limitations
- final result = enqueuedEvents.take(maxEventsPerBatch).toList();
- // If for some reason more events than one batch have queued up simply
- // discard them.
- enqueuedEvents.clear();
- return result;
- }
-}
diff --git a/lib/src/usage_impl_html.dart b/lib/src/usage_impl_html.dart
index 9ee8632..0161b31 100644
--- a/lib/src/usage_impl_html.dart
+++ b/lib/src/usage_impl_html.dart
@@ -12,15 +12,28 @@
///
/// [analyticsUrl] is an optional replacement for the default Google Analytics
/// URL (`https://www.google-analytics.com/collect`).
+///
+/// [batchingDelay] is used to control batching behaviour. It should return a
+/// [Future] that will be awaited before attempting sending the enqueued
+/// messages. If it returns `null` events will not be batched.
+///
+/// Batched messages are sent in batches of up to 20 messages.
class AnalyticsHtml extends AnalyticsImpl {
AnalyticsHtml(
- String trackingId, String applicationName, String applicationVersion,
- {String? analyticsUrl})
- : super(trackingId, HtmlPersistentProperties(applicationName),
- HtmlPostHandler(),
- applicationName: applicationName,
- applicationVersion: applicationVersion,
- analyticsUrl: analyticsUrl) {
+ String trackingId,
+ String applicationName,
+ String applicationVersion, {
+ String? analyticsUrl,
+ Future<void>? Function()? batchingDelay,
+ }) : super(
+ trackingId,
+ HtmlPersistentProperties(applicationName),
+ HtmlPostHandler(),
+ applicationName: applicationName,
+ applicationVersion: applicationVersion,
+ analyticsUrl: analyticsUrl,
+ batchingDelay: batchingDelay,
+ ) {
var screenWidth = window.screen!.width;
var screenHeight = window.screen!.height;
@@ -39,14 +52,15 @@
HtmlPostHandler({this.mockRequestor});
@override
- Future sendPost(String url, List<Map<String, dynamic>> batch) {
+ String encodeHit(Map<String, String> hit) {
var viewportWidth = document.documentElement!.clientWidth;
var viewportHeight = document.documentElement!.clientHeight;
+ return postEncode({...hit, 'vp': '${viewportWidth}x$viewportHeight'});
+ }
- var data = batch
- .map((event) =>
- postEncode({...event, 'vp': '${viewportWidth}x$viewportHeight'}))
- .join('\n');
+ @override
+ Future sendPost(String url, List<String> batch) {
+ var data = batch.join('\n');
Future<HttpRequest> Function(String, {String method, dynamic sendData})
requestor = mockRequestor ?? HttpRequest.request;
return requestor(url, method: 'POST', sendData: data).catchError((e) {
diff --git a/lib/src/usage_impl_io.dart b/lib/src/usage_impl_io.dart
index c696275..fbe1017 100644
--- a/lib/src/usage_impl_io.dart
+++ b/lib/src/usage_impl_io.dart
@@ -14,6 +14,9 @@
/// 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`).
+///
/// `trackingId`, `applicationName`, and `applicationVersion` values should be supplied.
/// `analyticsUrl` is optional, and lets user's substitute their own analytics URL for
/// the default.
@@ -22,18 +25,38 @@
/// 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()`.
+///
+/// [batchingDelay] is used to control batching behaviour. It should return a
+/// [Future] that will be awaited before attempting sending the enqueued
+/// messages. The default [batchingDelay] is `() => Future(() {})`. That implies
+/// messages will be sent whem control returns to the event loop.
+///
+/// If [batchingDelay] returns `null`, events will not be batched.
+///
+/// Batched messages are sent in batches of up to 20 messages. They will be sent
+/// to [analyticsBatchingUrl] defaulting to
+/// `https://www.google-analytics.com/batch`.
class AnalyticsIO extends AnalyticsImpl {
AnalyticsIO(
- String trackingId, String applicationName, String applicationVersion,
- {String? analyticsUrl, Directory? documentDirectory, HttpClient? client})
- : super(
- trackingId,
- IOPersistentProperties(applicationName,
- documentDirPath: documentDirectory?.path),
- IOPostHandler(client: client),
- applicationName: applicationName,
- applicationVersion: applicationVersion,
- analyticsUrl: analyticsUrl) {
+ String trackingId,
+ String applicationName,
+ String applicationVersion, {
+ String? analyticsUrl,
+ String? analyticsBatchingUrl,
+ Directory? documentDirectory,
+ HttpClient? client,
+ Future<void>? Function()? batchingDelay,
+ }) : super(
+ trackingId,
+ IOPersistentProperties(applicationName,
+ documentDirPath: documentDirectory?.path),
+ IOPostHandler(client: client),
+ applicationName: applicationName,
+ applicationVersion: applicationVersion,
+ analyticsUrl: analyticsUrl,
+ analyticsBatchingUrl: analyticsBatchingUrl,
+ batchingDelay: batchingDelay,
+ ) {
final locale = getPlatformLocale();
if (locale != null) {
setSessionValue('ul', locale);
@@ -82,8 +105,13 @@
: _client = (client ?? HttpClient())..userAgent = createUserAgent();
@override
- Future sendPost(String url, List<Map<String, dynamic>> batch) async {
- var data = batch.map(postEncode).join('\n');
+ String encodeHit(Map<String, String> hit) {
+ return postEncode(hit);
+ }
+
+ @override
+ Future sendPost(String url, List<String> batch) async {
+ var data = batch.join('\n');
try {
var req = await _client.postUrl(Uri.parse(url));
req.write(data);
diff --git a/lib/usage.dart b/lib/usage.dart
index 5755b61..cd548fe 100644
--- a/lib/usage.dart
+++ b/lib/usage.dart
@@ -98,15 +98,6 @@
Future sendTiming(String variableName, int time,
{String? category, String? label});
- /// All sendX calls sent from within [callback] will be enqueued and sent
- /// in batches of {maxEventsPerBatch} messages at a time.
- ///
- /// Google analytics by default supports no more than 20 items per batch.
- Future<T> withBatching<T>(
- FutureOr<T> Function() callback, {
- int maxEventsPerBatch = 20,
- });
-
/// Start a timer. The time won't be calculated, and the analytics information
/// sent, until the [AnalyticsTimer.finish] method is called.
AnalyticsTimer startTimer(String variableName,
@@ -292,12 +283,6 @@
return Future.value();
}
-
- @override
- Future<T> withBatching<T>(FutureOr<T> Function() callback,
- {int maxEventsPerBatch = 20}) async {
- return await callback();
- }
}
/// Sanitize a stacktrace. This will shorten file paths in order to remove any
diff --git a/pubspec.yaml b/pubspec.yaml
index 9613c34..6a3bbeb 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -12,7 +12,7 @@
dependencies:
path: ^1.8.0-nullsafety
+ pedantic: ^1.10.0-nullsafety.3
dev_dependencies:
- pedantic: ^1.9.0
test: ^1.16.0-nullsafety
diff --git a/test/hit_types_test.dart b/test/hit_types_test.dart
index 63da854..ecdce39 100644
--- a/test/hit_types_test.dart
+++ b/test/hit_types_test.dart
@@ -5,6 +5,7 @@
library usage.hit_types_test;
import 'dart:async';
+import 'dart:convert';
import 'package:test/test.dart';
@@ -14,15 +15,15 @@
void defineTests() {
group('screenView', () {
- test('simple', () {
+ test('simple', () async {
var mock = createMock();
- mock.sendScreenView('main');
+ await mock.sendScreenView('main');
expect(mock.mockProperties['clientId'], isNotNull);
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
});
- test('with parameters', () {
+ test('with parameters', () async {
var mock = createMock();
- mock.sendScreenView('withParams', parameters: {'cd1': 'foo'});
+ await mock.sendScreenView('withParams', parameters: {'cd1': 'foo'});
expect(mock.mockProperties['clientId'], isNotNull);
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
has(mock.last, 'cd1');
@@ -30,18 +31,18 @@
});
group('event', () {
- test('simple', () {
+ test('simple', () async {
var mock = createMock();
- mock.sendEvent('files', 'save');
+ await mock.sendEvent('files', 'save');
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
was(mock.last, 'event');
has(mock.last, 'ec');
has(mock.last, 'ea');
});
- test('with parameters', () {
+ test('with parameters', () async {
var mock = createMock();
- mock.sendEvent('withParams', 'save', parameters: {'cd1': 'foo'});
+ await mock.sendEvent('withParams', 'save', parameters: {'cd1': 'foo'});
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
was(mock.last, 'event');
has(mock.last, 'ec');
@@ -49,9 +50,9 @@
has(mock.last, 'cd1');
});
- test('optional args', () {
+ test('optional args', () async {
var mock = createMock();
- mock.sendEvent('files', 'save', label: 'File Save', value: 23);
+ await mock.sendEvent('files', 'save', label: 'File Save', value: 23);
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
was(mock.last, 'event');
has(mock.last, 'ec');
@@ -62,9 +63,9 @@
});
group('social', () {
- test('simple', () {
+ test('simple', () async {
var mock = createMock();
- mock.sendSocial('g+', 'plus', 'userid');
+ await mock.sendSocial('g+', 'plus', 'userid');
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
was(mock.last, 'social');
has(mock.last, 'sn');
@@ -74,18 +75,23 @@
});
group('timing', () {
- test('simple', () {
+ test('simple', () async {
var mock = createMock();
- mock.sendTiming('compile', 123);
+ await mock.sendTiming('compile', 123);
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
was(mock.last, 'timing');
has(mock.last, 'utv');
has(mock.last, 'utt');
});
- test('optional args', () {
+ test('optional args', () async {
var mock = createMock();
- mock.sendTiming('compile', 123, category: 'Build', label: 'Compile');
+ await mock.sendTiming(
+ 'compile',
+ 123,
+ category: 'Build',
+ label: 'Compile',
+ );
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
was(mock.last, 'timing');
has(mock.last, 'utv');
@@ -117,27 +123,28 @@
});
group('exception', () {
- test('simple', () {
+ test('simple', () async {
var mock = createMock();
- mock.sendException('FooException');
+ await mock.sendException('FooException');
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
was(mock.last, 'exception');
has(mock.last, 'exd');
});
- test('optional args', () {
+ test('optional args', () async {
var mock = createMock();
- mock.sendException('FooException', fatal: true);
+ await mock.sendException('FooException', fatal: true);
expect(mock.mockPostHandler.sentValues, isNot(isEmpty));
was(mock.last, 'exception');
has(mock.last, 'exd');
has(mock.last, 'exf');
});
- test('exception file paths', () {
+ test('exception file paths', () async {
var mock = createMock();
- mock.sendException('foo bar (file:///Users/foobar/tmp/error.dart:3:13)');
- expect(mock.last['exd'], 'foo bar (');
+ await mock
+ .sendException('foo bar (file:///Users/foobar/tmp/error.dart:3:13)');
+ expect(jsonDecode(mock.last)['exd'], 'foo bar (');
});
});
}
diff --git a/test/src/common.dart b/test/src/common.dart
index ccebb72..e934e71 100644
--- a/test/src/common.dart
+++ b/test/src/common.dart
@@ -5,6 +5,7 @@
library usage.common_test;
import 'dart:async';
+import 'dart:convert';
import 'package:test/test.dart';
import 'package:usage/src/usage_impl.dart';
@@ -12,9 +13,9 @@
AnalyticsImplMock createMock({Map<String, dynamic>? props}) =>
AnalyticsImplMock('UA-0', props: props);
-void was(Map m, String type) => expect(m['t'], type);
-void has(Map m, String key) => expect(m[key], isNotNull);
-void hasnt(Map m, String key) => expect(m[key], isNull);
+void was(String m, String type) => expect(jsonDecode(m)['t'], type);
+void has(String m, String key) => expect(jsonDecode(m)[key], isNotNull);
+void hasnt(String m, String key) => expect(jsonDecode(m)[key], isNull);
class AnalyticsImplMock extends AnalyticsImpl {
MockProperties get mockProperties => properties as MockProperties;
@@ -24,7 +25,7 @@
: super(trackingId, MockProperties(props), MockPostHandler(),
applicationName: 'Test App', applicationVersion: '0.1');
- Map<String, dynamic> get last => mockPostHandler.last;
+ String get last => mockPostHandler.last;
}
class MockProperties extends PersistentProperties {
@@ -47,17 +48,20 @@
}
class MockPostHandler extends PostHandler {
- List<Map<String, dynamic>> sentValues = [];
+ List<String> sentValues = [];
@override
- Future sendPost(String url, List<Map<String, dynamic>> batch) {
+ Future sendPost(String url, List<String> batch) {
sentValues.addAll(batch);
return Future.value();
}
- Map<String, dynamic> get last => sentValues.last;
+ String get last => sentValues.last;
@override
void close() {}
+
+ @override
+ String encodeHit(Map<String, String> hit) => jsonEncode(hit);
}
diff --git a/test/usage_impl_io_test.dart b/test/usage_impl_io_test.dart
index c0a97f0..f8b3145 100644
--- a/test/usage_impl_io_test.dart
+++ b/test/usage_impl_io_test.dart
@@ -8,6 +8,7 @@
import 'dart:async';
import 'dart:io';
+import 'package:pedantic/pedantic.dart';
import 'package:test/test.dart';
import 'package:usage/src/usage_impl_io.dart';
@@ -22,7 +23,8 @@
var args = [
<String, String>{'utv': 'varName', 'utt': '123'},
];
- await postHandler.sendPost('http://www.google.com', args);
+ await postHandler.sendPost(
+ 'http://www.google.com', args.map(postHandler.encodeHit).toList());
expect(mockClient.requests.single.buffer.toString(), '''
Request to http://www.google.com with ${createUserAgent()}
utv=varName&utt=123''');
@@ -57,43 +59,88 @@
});
group('batching', () {
- test('Without batching sends to regular url', () async {
+ test('With batch-delay returning null sends all events individually',
+ () async {
final mockClient = MockHttpClient();
- final analytics = AnalyticsIO(
- '<TRACKING-ID',
- 'usage-test',
- '0.0.1',
- client: mockClient,
- );
- await analytics.sendEvent('my-event', 'something');
- expect(mockClient.requests.single.buffer.toString(), '''
+ final analytics = AnalyticsIO('<TRACKING-ID', 'usage-test', '0.0.1',
+ client: mockClient, batchingDelay: () => null);
+ unawaited(analytics.sendEvent('my-event1', 'something'));
+ unawaited(analytics.sendEvent('my-event2', 'something'));
+ unawaited(analytics.sendEvent('my-event3', 'something'));
+ await analytics.waitForLastPing();
+ expect(mockClient.requests.length, 3);
+ expect(mockClient.requests[0].buffer.toString(), '''
Request to https://www.google-analytics.com/collect with ${createUserAgent()}
-ec=my-event&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event''');
+ec=my-event1&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event''');
+ expect(mockClient.requests[1].buffer.toString(), '''
+Request to https://www.google-analytics.com/collect with ${createUserAgent()}
+ec=my-event2&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event''');
+ expect(mockClient.requests[2].buffer.toString(), '''
+Request to https://www.google-analytics.com/collect with ${createUserAgent()}
+ec=my-event3&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event''');
});
- test('with batching sends to batching url', () async {
+ test(
+ 'with default batch-delay hits from the same sync span are batched together',
+ () async {
var mockClient = MockHttpClient();
final analytics = AnalyticsIO('<TRACKING-ID', 'usage-test', '0.0.1',
client: mockClient);
- await analytics.withBatching(() async {
- await analytics.sendEvent('my-event1', 'something1');
- await analytics.sendEvent('my-event2', 'something2');
- await analytics.sendEvent('my-event3', 'something3');
- await analytics.sendEvent('my-event4', 'something4');
- }, maxEventsPerBatch: 3);
- await analytics.sendEvent('my-event-not-batched', 'something');
+ unawaited(analytics.sendEvent('my-event1', 'something'));
+ unawaited(analytics.sendEvent('my-event2', 'something'));
+ unawaited(analytics.sendEvent('my-event3', 'something'));
+ unawaited(analytics.sendEvent('my-event4', 'something'));
+ unawaited(analytics.sendEvent('my-event5', 'something'));
+ unawaited(analytics.sendEvent('my-event6', 'something'));
+ unawaited(analytics.sendEvent('my-event7', 'something'));
+ unawaited(analytics.sendEvent('my-event8', 'something'));
+ unawaited(analytics.sendEvent('my-event9', 'something'));
+ unawaited(analytics.sendEvent('my-event10', 'something'));
+ unawaited(analytics.sendEvent('my-event11', 'something'));
+ unawaited(analytics.sendEvent('my-event12', 'something'));
+ unawaited(analytics.sendEvent('my-event13', 'something'));
+ unawaited(analytics.sendEvent('my-event14', 'something'));
+ unawaited(analytics.sendEvent('my-event15', 'something'));
+ unawaited(analytics.sendEvent('my-event16', 'something'));
+ unawaited(analytics.sendEvent('my-event17', 'something'));
+ unawaited(analytics.sendEvent('my-event18', 'something'));
+ unawaited(analytics.sendEvent('my-event19', 'something'));
+ unawaited(analytics.sendEvent('my-event20', 'something'));
+ unawaited(analytics.sendEvent('my-event21', 'something'));
+ await Future(() {});
+ expect(mockClient.requests.length, 2);
+ unawaited(analytics.sendEvent('my-event-not-batched', 'something'));
+ await Future(() {});
+ await analytics.waitForLastPing();
expect(mockClient.requests.length, 3);
expect(mockClient.requests[0].buffer.toString(), '''
Request to https://www.google-analytics.com/batch with ${createUserAgent()}
-ec=my-event1&ea=something1&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
-ec=my-event2&ea=something2&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
-ec=my-event3&ea=something3&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event''');
+ec=my-event1&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event2&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event3&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event4&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event5&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event6&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event7&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event8&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event9&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event10&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event11&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event12&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event13&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event14&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event15&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event16&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event17&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event18&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event19&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event
+ec=my-event20&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event''');
expect(mockClient.requests[1].buffer.toString(), '''
-Request to https://www.google-analytics.com/batch with ${createUserAgent()}
-ec=my-event4&ea=something4&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event''');
+Request to https://www.google-analytics.com/collect with ${createUserAgent()}
+ec=my-event21&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event''');
expect(mockClient.requests[2].buffer.toString(), '''
Request to https://www.google-analytics.com/collect with ${createUserAgent()}
ec=my-event-not-batched&ea=something&an=usage-test&av=0.0.1&ul=en-us&v=1&tid=%3CTRACKING-ID&cid=8e3fa343-70bc-4afe-ad81-5fed4256b4e8&t=event''');
diff --git a/test/usage_impl_test.dart b/test/usage_impl_test.dart
index ae71261..0d46125 100644
--- a/test/usage_impl_test.dart
+++ b/test/usage_impl_test.dart
@@ -58,15 +58,15 @@
expect(mock.firstRun, false);
});
- test('setSessionValue', () {
+ test('setSessionValue', () async {
var mock = createMock();
- mock.sendScreenView('foo');
+ await mock.sendScreenView('foo');
hasnt(mock.last, 'val');
mock.setSessionValue('val', 'ue');
- mock.sendScreenView('bar');
+ await mock.sendScreenView('bar');
has(mock.last, 'val');
mock.setSessionValue('val', null);
- mock.sendScreenView('baz');
+ await mock.sendScreenView('baz');
hasnt(mock.last, 'val');
});
diff --git a/test/web_test.dart b/test/web_test.dart
index 2a544ba..5aa87bc 100644
--- a/test/web_test.dart
+++ b/test/web_test.dart
@@ -36,7 +36,8 @@
<String, String>{'utv': 'varName', 'utt': '123'},
];
- await postHandler.sendPost('http://www.google.com', args);
+ await postHandler.sendPost(
+ 'http://www.google.com', args.map(postHandler.encodeHit).toList());
expect(client.sendCount, 1);
});
});