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); }); });