From cf904dc0289f89f973c778bd8f4816f71749e3ca Mon Sep 17 00:00:00 2001 From: Tyler-Larkin Date: Wed, 14 Aug 2024 15:18:59 -0700 Subject: [PATCH] fix: push notification flush events (#5215) fix: push notification flush events (#5215) --- .../push/pinpoint_event_source.dart | 2 ++ .../lib/src/analytics_plugin_impl.dart | 1 - packages/aws_common/lib/aws_common.dart | 1 + .../lib/src/util}/stoppable_timer.dart | 1 + .../lib/src/pinpoint_event_type_source.dart | 26 +++++++++++++++ .../lib/src/pinpoint_provider.dart | 31 ++++++++++++++--- .../test/pinpoint_provider_test.dart | 33 +++++++++++++++++-- 7 files changed, 87 insertions(+), 8 deletions(-) rename packages/{analytics/amplify_analytics_pinpoint_dart/lib/src/impl/analytics_client => aws_common/lib/src/util}/stoppable_timer.dart (99%) create mode 100644 packages/notifications/push/amplify_push_notifications_pinpoint/lib/src/pinpoint_event_type_source.dart diff --git a/packages/amplify_core/lib/src/types/notifications/push/pinpoint_event_source.dart b/packages/amplify_core/lib/src/types/notifications/push/pinpoint_event_source.dart index ab80ac7562..ed60871846 100644 --- a/packages/amplify_core/lib/src/types/notifications/push/pinpoint_event_source.dart +++ b/packages/amplify_core/lib/src/types/notifications/push/pinpoint_event_source.dart @@ -10,10 +10,12 @@ /// [Campaigns](https://docs.aws.amazon.com/pinpoint/latest/userguide/campaigns.html) /// [Journeys](https://docs.aws.amazon.com/pinpoint/latest/userguide/journeys.html) /// {@endtemplate} +@Deprecated('this enum will be private in the next major version') enum PinpointEventSource { campaign('campaign'), journey('journey'); + @Deprecated('this enum will be private in the next major version') const PinpointEventSource(this.name); final String name; diff --git a/packages/analytics/amplify_analytics_pinpoint_dart/lib/src/analytics_plugin_impl.dart b/packages/analytics/amplify_analytics_pinpoint_dart/lib/src/analytics_plugin_impl.dart index f981412729..a7187214a3 100644 --- a/packages/analytics/amplify_analytics_pinpoint_dart/lib/src/analytics_plugin_impl.dart +++ b/packages/analytics/amplify_analytics_pinpoint_dart/lib/src/analytics_plugin_impl.dart @@ -9,7 +9,6 @@ import 'package:amplify_analytics_pinpoint_dart/src/impl/analytics_client/endpoi import 'package:amplify_analytics_pinpoint_dart/src/impl/analytics_client/event_client/event_client.dart'; import 'package:amplify_analytics_pinpoint_dart/src/impl/analytics_client/event_client/queued_item_store/dart_queued_item_store.dart'; import 'package:amplify_analytics_pinpoint_dart/src/impl/analytics_client/session_manager.dart'; -import 'package:amplify_analytics_pinpoint_dart/src/impl/analytics_client/stoppable_timer.dart'; import 'package:amplify_analytics_pinpoint_dart/src/impl/flutter_provider_interfaces/app_lifecycle_provider.dart'; import 'package:amplify_analytics_pinpoint_dart/src/impl/flutter_provider_interfaces/cached_events_path_provider.dart'; import 'package:amplify_analytics_pinpoint_dart/src/impl/flutter_provider_interfaces/device_context_info_provider.dart'; diff --git a/packages/aws_common/lib/aws_common.dart b/packages/aws_common/lib/aws_common.dart index 899c1e6dc4..8d7555d1a1 100644 --- a/packages/aws_common/lib/aws_common.dart +++ b/packages/aws_common/lib/aws_common.dart @@ -52,5 +52,6 @@ export 'src/util/json.dart'; export 'src/util/print.dart'; export 'src/util/recase.dart'; export 'src/util/serializable.dart'; +export 'src/util/stoppable_timer.dart'; export 'src/util/stream.dart'; export 'src/util/uuid.dart'; diff --git a/packages/analytics/amplify_analytics_pinpoint_dart/lib/src/impl/analytics_client/stoppable_timer.dart b/packages/aws_common/lib/src/util/stoppable_timer.dart similarity index 99% rename from packages/analytics/amplify_analytics_pinpoint_dart/lib/src/impl/analytics_client/stoppable_timer.dart rename to packages/aws_common/lib/src/util/stoppable_timer.dart index 4de7560050..8e57cf9f49 100644 --- a/packages/analytics/amplify_analytics_pinpoint_dart/lib/src/impl/analytics_client/stoppable_timer.dart +++ b/packages/aws_common/lib/src/util/stoppable_timer.dart @@ -8,6 +8,7 @@ import 'package:meta/meta.dart'; /// {@template amplify_analytics_pinpoint_dart.stoppable_timer} /// A Timer that can be stopped and started again. /// {@endtemplate} +@protected class StoppableTimer { /// {@macro amplify_analytics_pinpoint_dart.stoppable_timer} /// diff --git a/packages/notifications/push/amplify_push_notifications_pinpoint/lib/src/pinpoint_event_type_source.dart b/packages/notifications/push/amplify_push_notifications_pinpoint/lib/src/pinpoint_event_type_source.dart new file mode 100644 index 0000000000..95667966b5 --- /dev/null +++ b/packages/notifications/push/amplify_push_notifications_pinpoint/lib/src/pinpoint_event_type_source.dart @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/// {@template amplify_core.push.pinpoint_event_source} +/// The source of a push notification. +/// +/// Pinpoint offers two ways of sending push notifications to users campaigns and journeys. +/// +/// See also: +/// [Campaigns](https://docs.aws.amazon.com/pinpoint/latest/userguide/campaigns.html) +/// [Journeys](https://docs.aws.amazon.com/pinpoint/latest/userguide/journeys.html) +/// {@endtemplate} +enum PinpointEventTypeSource { + /// [campaign] represents a push notification originating from a campaign + /// [Campaign Events](https://docs.aws.amazon.com/pinpoint/latest/developerguide/event-streams-data-campaign.html) + campaign('_campaign'), + + /// [journey] represents a push notification originating from a journey + /// [Journey Events](https://docs.aws.amazon.com/pinpoint/latest/developerguide/event-streams-data-journey.html) + journey('_journey'); + + const PinpointEventTypeSource(this.name); + + /// [name] contains the source prefix for event_type attributes + final String name; +} diff --git a/packages/notifications/push/amplify_push_notifications_pinpoint/lib/src/pinpoint_provider.dart b/packages/notifications/push/amplify_push_notifications_pinpoint/lib/src/pinpoint_provider.dart index 6cf94d6b6f..081f213417 100644 --- a/packages/notifications/push/amplify_push_notifications_pinpoint/lib/src/pinpoint_provider.dart +++ b/packages/notifications/push/amplify_push_notifications_pinpoint/lib/src/pinpoint_provider.dart @@ -15,6 +15,7 @@ import 'package:amplify_core/amplify_core.dart'; // ignore: implementation_imports import 'package:amplify_core/src/config/amplify_outputs/notifications/notifications_outputs.dart'; import 'package:amplify_push_notifications_pinpoint/src/event_info_type.dart'; +import 'package:amplify_push_notifications_pinpoint/src/pinpoint_event_type_source.dart'; import 'package:amplify_secure_storage/amplify_secure_storage.dart'; import 'package:flutter/widgets.dart'; @@ -27,12 +28,18 @@ final AmplifyLogger _logger = AmplifyLogger.category(Category.pushNotifications) /// [init] method has to be called before other methods can be used. /// Once initialized, it can [registerDevice], [recordNotificationEvent] /// & [identifyUser] with Pinpoint. +/// +/// To release any initialized resources [dispose] should be called. /// {@endtemplate} class PinpointProvider implements ServiceProviderClient { /// {@macro amplify_push_notifications_pinpoint.pinpoint_provider} late AnalyticsClient _analyticsClient; + /// Periodic timer for flushing events made public for testing + @visibleForTesting + late final StoppableTimer autoEventSubmitter; + static const _androidCampaignIdKey = 'pinpoint.campaign.campaign_id'; static const _androidCampaignActivityIdKey = 'pinpoint.campaign.campaign_activity_id'; @@ -92,6 +99,12 @@ class PinpointProvider implements ServiceProviderClient { authProvider: authProvider, ); + autoEventSubmitter = StoppableTimer( + duration: const Duration(seconds: 10), + callback: _flushEvents, + onError: (e) => _logger.warn('Exception in events auto flush', e), + ); + _isInitialized = true; } } on Exception catch (e) { @@ -104,6 +117,10 @@ class PinpointProvider implements ServiceProviderClient { } } + Future _flushEvents() { + return _analyticsClient.eventClient.flushEvents(); + } + @override Future identifyUser({ required String userId, @@ -209,14 +226,14 @@ class PinpointProvider implements ServiceProviderClient { }) { final data = notification.data; final analyticsProperties = CustomProperties(); - var source = PinpointEventSource.campaign.name; + var source = PinpointEventTypeSource.campaign.name; var campaign = {}; var journey = {}; var pinpointData = {}; // Android payload contain pinpoint.campaign.* format if (data.containsKey(_androidCampaignIdKey)) { - source = PinpointEventSource.campaign.name; + source = PinpointEventTypeSource.campaign.name; campaign['campaign_id'] = data[_androidCampaignIdKey] as String; if (data.containsKey(_androidCampaignActivityIdKey)) { campaign['campaign_activity_id'] = @@ -239,7 +256,7 @@ class PinpointProvider implements ServiceProviderClient { // iOS payload conatin a nested map of pinpoint, campaign, * format if (pinpointData.containsKey('campaign')) { - source = PinpointEventSource.campaign.name; + source = PinpointEventTypeSource.campaign.name; campaign = Map.from( pinpointData['campaign'] as Map, ); @@ -247,7 +264,7 @@ class PinpointProvider implements ServiceProviderClient { // Common way of represting journeys both on Android and iOS payloads if (pinpointData.containsKey('journey')) { - source = PinpointEventSource.journey.name; + source = PinpointEventTypeSource.journey.name; journey = Map.from( pinpointData['journey'] as Map, ); @@ -274,4 +291,10 @@ class PinpointProvider implements ServiceProviderClient { return ChannelType.apns; } } + + /// Cleans up and releases resources retained by this object. + /// This includes but is not limited to periodic timers for flushing events. + void dispose() { + autoEventSubmitter.stop(); + } } diff --git a/packages/notifications/push/amplify_push_notifications_pinpoint/test/pinpoint_provider_test.dart b/packages/notifications/push/amplify_push_notifications_pinpoint/test/pinpoint_provider_test.dart index ff72d427ee..1cf3ce0eb0 100644 --- a/packages/notifications/push/amplify_push_notifications_pinpoint/test/pinpoint_provider_test.dart +++ b/packages/notifications/push/amplify_push_notifications_pinpoint/test/pinpoint_provider_test.dart @@ -8,6 +8,7 @@ import 'package:amplify_analytics_pinpoint_dart/src/impl/analytics_client/event_ import 'package:amplify_core/src/config/amplify_outputs/notifications/amazon_pinpoint_channel.dart'; import 'package:amplify_core/src/config/amplify_outputs/notifications/notifications_outputs.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_push_notifications_pinpoint/src/pinpoint_event_type_source.dart'; import 'package:amplify_push_notifications_pinpoint/src/pinpoint_provider.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -142,7 +143,7 @@ void main() { final properties = res.properties; final source = res.source; expect(properties.attributes.containsKey('journey_id'), isTrue); - expect(source, equals(PinpointEventSource.journey.name)); + expect(source, equals(PinpointEventTypeSource.journey.name)); }); test( @@ -154,7 +155,7 @@ void main() { final properties = res.properties; final source = res.source; expect(properties.attributes.containsKey('campaign_id'), isTrue); - expect(source, equals(PinpointEventSource.campaign.name)); + expect(source, equals(PinpointEventTypeSource.campaign.name)); }); }); @@ -197,6 +198,32 @@ void main() { ); }); + test('flush events timer initialized', () async { + when( + () => mockAmplifyAuthProviderRepository.getAuthProvider( + APIAuthorizationType.iam.authProviderToken, + ), + ).thenReturn(awsIamAmplifyAuthProvider); + when( + () => mockAnalyticsClient.init( + pinpointAppId: any(named: 'pinpointAppId'), + region: any(named: 'region'), + authProvider: any(named: 'authProvider'), + ), + ).thenAnswer((realInvocation) async {}); + + await pinpointProvider.init( + config: notificationsPinpointConfig, + authProviderRepo: mockAmplifyAuthProviderRepository, + analyticsClient: mockAnalyticsClient, + ); + + expect( + pinpointProvider.autoEventSubmitter.duration, + const Duration(seconds: 10), + ); + }); + test('identifyUser should run successfully', () async { when( () => mockAmplifyAuthProviderRepository.getAuthProvider( @@ -408,7 +435,7 @@ void main() { verify( () => mockEventClient.recordEvent( eventType: - '${PinpointEventSource.campaign.name}.${PinpointEventType.foregroundMessageReceived.name}', + '${PinpointEventTypeSource.campaign.name}.${PinpointEventType.foregroundMessageReceived.name}', properties: any(named: 'properties'), ), );