Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: push notification flush events #5215

Merged
merged 17 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
tyllark marked this conversation as resolved.
Show resolved Hide resolved
campaign('campaign'),
journey('journey');

@Deprecated('this enum will be private in the next major version')
const PinpointEventSource(this.name);

final String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions packages/aws_common/lib/aws_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
tyllark marked this conversation as resolved.
Show resolved Hide resolved
/// {@macro amplify_analytics_pinpoint_dart.stoppable_timer}
///
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -104,6 +117,10 @@ class PinpointProvider implements ServiceProviderClient {
}
}

Future<void> _flushEvents() {
return _analyticsClient.eventClient.flushEvents();
}

@override
Future<void> identifyUser({
required String userId,
Expand Down Expand Up @@ -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 = <String, String>{};
var journey = <String, String>{};
var pinpointData = <Object?, Object?>{};

// 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'] =
Expand All @@ -239,15 +256,15 @@ 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<String, String>.from(
pinpointData['campaign'] as Map<Object?, Object?>,
);
}

// 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<String, String>.from(
pinpointData['journey'] as Map<Object?, Object?>,
);
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
Expand All @@ -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));
});
});

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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'),
),
);
Expand Down
Loading