From 8e0891d11d73dd8b111c9de412a0cef9c9e19447 Mon Sep 17 00:00:00 2001 From: Xiaowei Zhu <33129495+zhu-xiaowei@users.noreply.github.com> Date: Thu, 11 Apr 2024 19:07:24 +0800 Subject: [PATCH] feat: add preset traffic source attributes (#10) Co-authored-by: zhu-xiaowei --- CHANGELOG.md | 4 + README.md | 22 +++-- analysis_options.yaml | 3 + example/lib/main.dart | 15 +++- lib/clickstream_analytics.dart | 16 ++++ ...tream_flutter_platform_interface_test.dart | 85 +++++++++++++++++++ test/clickstream_flutter_test.dart | 39 +++++++-- test/mock_method_channel.dart | 63 ++++++++++++++ 8 files changed, 230 insertions(+), 17 deletions(-) create mode 100644 test/clickstream_flutter_platform_interface_test.dart create mode 100644 test/mock_method_channel.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a47353..a187353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0 + +* feat: add preset traffic source attributes + ## 0.3.0 * feat: add record screen view api diff --git a/README.md b/README.md index 838cb37..df94e7a 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ analytics.setUserId(null); ```dart analytics.setUserAttributes({ - "userName":"carl", + "userName": "carl", "userAge": 22 }); ``` @@ -90,14 +90,22 @@ Current login user's attributes will be cached in disk, so the next time app lau #### Add global attribute 1. Add global attributes when initializing the SDK - + + The following example code shows how to add traffic source fields as global attributes when initializing the SDK. ```dart analytics.init({ appId: "your appId", endpoint: "https://example.com/collect", globalAttributes: { - "_traffic_source_medium": "Search engine", - "_traffic_source_name": "Summer promotion", + Attr.TRAFFIC_SOURCE_SOURCE: "amazon", + Attr.TRAFFIC_SOURCE_MEDIUM: "cpc", + Attr.TRAFFIC_SOURCE_CAMPAIGN: "summer_promotion", + Attr.TRAFFIC_SOURCE_CAMPAIGN_ID: "summer_promotion_01", + Attr.TRAFFIC_SOURCE_TERM: "running_shoes", + Attr.TRAFFIC_SOURCE_CONTENT: "banner_ad_1", + Attr.TRAFFIC_SOURCE_CLID: "amazon_ad_123", + Attr.TRAFFIC_SOURCE_CLID_PLATFORM: "amazon_ads", + Attr.APP_INSTALL_CHANNEL: "amazon_store" } }); ``` @@ -105,8 +113,7 @@ Current login user's attributes will be cached in disk, so the next time app lau 2. Add global attributes after initializing the SDK ```dart analytics.addGlobalAttributes({ - "_traffic_source_medium": "Search engine", - "_traffic_source_name": "Summer promotion", + Attr.TRAFFIC_SOURCE_MEDIUM: "Search engine", "level": 10 }); ``` @@ -138,7 +145,8 @@ var itemBook = ClickstreamItem( analytics.record( name: "view_item", attributes: { - "currency": "USD", + Attr.VALUE: 99, + Attr.CURRENCY: "USD" "event_category": "recommended" }, items: [itemBook] diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..342194d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,3 +2,6 @@ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options +linter: + rules: + // ignore: non_constant_identifier_name \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 51d16e5..83d4062 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -42,7 +42,15 @@ class _MyAppState extends State { isCompressEvents: false, sessionTimeoutDuration: 30000, globalAttributes: { - "channel": "Samsung", + Attr.TRAFFIC_SOURCE_SOURCE: "amazon", + Attr.TRAFFIC_SOURCE_MEDIUM: "cpc", + Attr.TRAFFIC_SOURCE_CAMPAIGN: "summer_promotion", + Attr.TRAFFIC_SOURCE_CAMPAIGN_ID: "summer_promotion_01", + Attr.TRAFFIC_SOURCE_TERM: "running_shoes", + Attr.TRAFFIC_SOURCE_CONTENT: "banner_ad_1", + Attr.TRAFFIC_SOURCE_CLID: "amazon_ad_123", + Attr.TRAFFIC_SOURCE_CLID_PLATFORM: "amazon_ads", + Attr.APP_INSTALL_CHANNEL: "amazon_store", "Class": 5, "isTrue": true, "Score": 24.32 @@ -180,7 +188,7 @@ class _MyAppState extends State { title: const Text('addGlobalAttributes'), onTap: () async { analytics.addGlobalAttributes({ - "channel": "Samsung", + Attr.APP_INSTALL_CHANNEL: "amazon_store", "Class": 5, "isTrue": true, "Score": 24.32 @@ -193,7 +201,8 @@ class _MyAppState extends State { leading: const Icon(Icons.delete_rounded), title: const Text('deleteGlobalAttributes'), onTap: () async { - analytics.deleteGlobalAttributes(["Score", "channel"]); + analytics.deleteGlobalAttributes( + ["Score", Attr.APP_INSTALL_CHANNEL]); log("deleteGlobalAttributes Score and channel"); }, minLeadingWidth: 0, diff --git a/lib/clickstream_analytics.dart b/lib/clickstream_analytics.dart index 4c9dfb0..45d69ac 100644 --- a/lib/clickstream_analytics.dart +++ b/lib/clickstream_analytics.dart @@ -119,3 +119,19 @@ class ClickstreamAnalytics { return ClickstreamInterface.instance.enable(); } } + +class Attr { + static const String TRAFFIC_SOURCE_SOURCE = '_traffic_source_source'; + static const String TRAFFIC_SOURCE_MEDIUM = '_traffic_source_medium'; + static const String TRAFFIC_SOURCE_CAMPAIGN = '_traffic_source_campaign'; + static const String TRAFFIC_SOURCE_CAMPAIGN_ID = + '_traffic_source_campaign_id'; + static const String TRAFFIC_SOURCE_TERM = '_traffic_source_term'; + static const String TRAFFIC_SOURCE_CONTENT = '_traffic_source_content'; + static const String TRAFFIC_SOURCE_CLID = '_traffic_source_clid'; + static const String TRAFFIC_SOURCE_CLID_PLATFORM = + '_traffic_source_clid_platform'; + static const String APP_INSTALL_CHANNEL = '_app_install_channel'; + static const String VALUE = '_value'; + static const String CURRENCY = '_currency'; +} diff --git a/test/clickstream_flutter_platform_interface_test.dart b/test/clickstream_flutter_platform_interface_test.dart new file mode 100644 index 0000000..8210d81 --- /dev/null +++ b/test/clickstream_flutter_platform_interface_test.dart @@ -0,0 +1,85 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter_test/flutter_test.dart'; + +import 'mock_method_channel.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + MockMethodChannel platform = MockMethodChannel(); + + test('init method for UnimplementedError', () async { + Map initConfig = { + 'appId': 'testApp', + 'endpoint': "", + }; + expect(() async { + await platform.init(initConfig); + }, throwsA(isInstanceOf())); + }); + + test('record method for UnimplementedError', () async { + Map attributes = { + "category": "shoes", + "currency": "CNY", + "value": 279.9 + }; + expect(platform.record(attributes), + throwsA(isInstanceOf())); + }); + + test('setUserId method for UnimplementedError', () async { + Map attributes = { + "userId": "1234", + }; + expect(platform.setUserId(attributes), + throwsA(isInstanceOf())); + }); + + test('setUserAttributes method for UnimplementedError', () async { + Map attributes = {"_user_age": 21, "_user_name": "carl"}; + expect(platform.setUserAttributes(attributes), + throwsA(isInstanceOf())); + }); + + test('addGlobalAttributes method for UnimplementedError', () async { + Map attributes = { + "channel": "Play Store", + "level": 5.1, + "class": 6 + }; + expect(platform.addGlobalAttributes(attributes), + throwsA(isInstanceOf())); + }); + + test('deleteGlobalAttributes method for UnimplementedError', () async { + Map attributes = { + "attributes": ["attr1", "attr2"], + }; + expect(platform.deleteGlobalAttributes(attributes), + throwsA(isInstanceOf())); + }); + + test('updateConfigure method for UnimplementedError', () async { + Map attributes = { + "appId": "newAppId", + "endpoint": "https://example.com/collect", + }; + expect(platform.updateConfigure(attributes), + throwsA(isInstanceOf())); + }); + + test('flushEvents method for UnimplementedError', () async { + expect(platform.flushEvents(), throwsA(isInstanceOf())); + }); + + test('disable method for UnimplementedError', () async { + expect(platform.disable(), throwsA(isInstanceOf())); + }); + + test('enable method for UnimplementedError', () async { + expect(platform.enable(), throwsA(isInstanceOf())); + }); +} diff --git a/test/clickstream_flutter_test.dart b/test/clickstream_flutter_test.dart index 86cdd1f..4a86da3 100644 --- a/test/clickstream_flutter_test.dart +++ b/test/clickstream_flutter_test.dart @@ -72,7 +72,28 @@ void main() { appId: 'testApp', endpoint: "https://example.com/collect", globalAttributes: { - "channel": "Samsung", + Attr.APP_INSTALL_CHANNEL: "amazon_store", + "Class": 5, + "isTrue": true, + "Score": 24.32 + }); + expect(result, true); + }); + + test('init SDK with traffic source using global attributes', () async { + var result = await analytics.init( + appId: 'testApp', + endpoint: "https://example.com/collect", + globalAttributes: { + Attr.TRAFFIC_SOURCE_SOURCE: "amazon", + Attr.TRAFFIC_SOURCE_MEDIUM: "cpc", + Attr.TRAFFIC_SOURCE_CAMPAIGN: "summer_promotion", + Attr.TRAFFIC_SOURCE_CAMPAIGN_ID: "summer_promotion_01", + Attr.TRAFFIC_SOURCE_TERM: "running_shoes", + Attr.TRAFFIC_SOURCE_CONTENT: "banner_ad_1", + Attr.TRAFFIC_SOURCE_CLID: "amazon_ad_123", + Attr.TRAFFIC_SOURCE_CLID_PLATFORM: "amazon_ads", + Attr.APP_INSTALL_CHANNEL: "amazon_store", "Class": 5, "isTrue": true, "Score": 24.32 @@ -93,7 +114,7 @@ void main() { isTrackUserEngagementEvents: false, isTrackAppExceptionEvents: true, globalAttributes: { - "channel": "Samsung", + Attr.APP_INSTALL_CHANNEL: "amazon_store", }); expect(result, true); }); @@ -125,10 +146,14 @@ void main() { price: 65, currency: "USD", attributes: {"place_of_origin": "USA"}); - var result = analytics.record( - name: "cart_view", - attributes: {"_traffic_source_name": "Summer promotion"}, - items: [itemBook, itemShoes]); + var result = analytics.record(name: "cart_view", attributes: { + Attr.TRAFFIC_SOURCE_CAMPAIGN: "Summer promotion", + Attr.VALUE: 164, + Attr.CURRENCY: "USD" + }, items: [ + itemBook, + itemShoes + ]); expect(result, isNotNull); }); @@ -155,7 +180,7 @@ void main() { test('setGlobalAttributes', () async { var result = analytics.addGlobalAttributes( - {"channel": "Play Store", "level": 5.1, "class": 6}); + {Attr.APP_INSTALL_CHANNEL: "amazon_store", "level": 5.1, "class": 6}); var result1 = analytics.addGlobalAttributes({}); expect(result, isNotNull); expect(result1, isNotNull); diff --git a/test/mock_method_channel.dart b/test/mock_method_channel.dart new file mode 100644 index 0000000..e09c6be --- /dev/null +++ b/test/mock_method_channel.dart @@ -0,0 +1,63 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:clickstream_analytics/clickstream_analytics_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// An implementation of [ClickstreamFlutterPlatform] that uses method channels. +class MockMethodChannel extends ClickstreamInterface { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('clickstream_flutter'); + + @override + Future init(Map configure) async { + return super.init(configure); + } + + @override + Future record(Map attributes) async { + return super.record(attributes); + } + + @override + Future setUserId(Map userId) async { + return super.setUserId(userId); + } + + @override + Future setUserAttributes(Map attributes) async { + return super.setUserAttributes(attributes); + } + + @override + Future addGlobalAttributes(Map attributes) async { + return super.addGlobalAttributes(attributes); + } + + @override + Future deleteGlobalAttributes(Map attributes) async { + return super.deleteGlobalAttributes(attributes); + } + + @override + Future updateConfigure(Map configure) async { + return super.updateConfigure(configure); + } + + @override + Future flushEvents() async { + return super.flushEvents(); + } + + @override + Future disable() async { + return super.disable(); + } + + @override + Future enable() async { + return super.enable(); + } +}