diff --git a/README.md b/README.md index 8e6d446..defac19 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Clickstream Flutter SDK can help you easily collect and report events from your The SDK relies on the [Clickstream Android SDK](https://github.com/awslabs/clickstream-android) and [Clickstream Swift SDK](https://github.com/awslabs/clickstream-swift). Therefore, flutter SDK also supports automatically collect common user events and attributes (e.g., session start, first open). In addition, we've added easy-to-use APIs to simplify data collection in Flutter apps. -Visit our [Documentation site](https://awslabs.github.io/clickstream-analytics-on-aws/en/sdk-manual/flutter/) to learn more about Clickstream Flutter SDK. +Visit our [Documentation site](https://awslabs.github.io/clickstream-analytics-on-aws/en/latest/sdk-manual/flutter/) to learn more about Clickstream Flutter SDK. ## Integrate SDK @@ -100,6 +100,33 @@ analytics.deleteGlobalAttributes(["level"]); It is recommended to set global attributes after each SDK initialization, global attributes will be included in all events that occur after it is set. +#### Record event with items + +You can add the following code to log an event with an item. you can add custom item attribute in `attributes` object. + +**Note: Only pipelines from version 1.1+ can handle items with custom attribute.** + +```dart +var itemBook = ClickstreamItem( + id: "123", + name: "Nature", + category: "book", + price: 99, + attributes: { + "book_publisher": "Nature Research" + } +); + +analytics.record( + name: "view_item", + attributes: { + "currency": 'USD', + "event_category": 'recommended' + }, + items: [itemBook] +); +``` + #### Other configurations In addition to the required `appId` and `endpoint`, you can configure other information to get more customized usage: diff --git a/android/build.gradle b/android/build.gradle index 9a51158..77d4bb3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -52,7 +52,7 @@ android { dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.0.0' - implementation 'software.aws.solution:clickstream:0.9.0' + implementation 'software.aws.solution:clickstream:0.10.0' implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.10")) } diff --git a/android/src/main/kotlin/software/aws/solution/clickstream_analytics/ClickstreamFlutterPlugin.kt b/android/src/main/kotlin/software/aws/solution/clickstream_analytics/ClickstreamFlutterPlugin.kt index 9f21d6e..647a2c9 100644 --- a/android/src/main/kotlin/software/aws/solution/clickstream_analytics/ClickstreamFlutterPlugin.kt +++ b/android/src/main/kotlin/software/aws/solution/clickstream_analytics/ClickstreamFlutterPlugin.kt @@ -32,6 +32,7 @@ import software.aws.solution.clickstream.AWSClickstreamPlugin import software.aws.solution.clickstream.ClickstreamAnalytics import software.aws.solution.clickstream.ClickstreamAttribute import software.aws.solution.clickstream.ClickstreamEvent +import software.aws.solution.clickstream.ClickstreamItem import software.aws.solution.clickstream.ClickstreamUserAttribute import software.aws.solution.clickstream.client.util.ThreadUtil import java.util.Objects @@ -159,6 +160,7 @@ class ClickstreamFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware arguments?.let { val eventName = it["eventName"] as String val attributes = it["attributes"] as HashMap<*, *> + val items = it["items"] as ArrayList<*> val eventBuilder = ClickstreamEvent.builder().name(eventName) for ((key, value) in attributes) { if (value is String) { @@ -173,6 +175,27 @@ class ClickstreamFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware eventBuilder.add(key.toString(), value) } } + if (items.size > 0) { + val clickstreamItems = arrayOfNulls(items.size) + for (index in 0 until items.size) { + val builder = ClickstreamItem.builder() + for ((key, value) in (items[index] as HashMap<*, *>)) { + if (value is String) { + builder.add(key.toString(), value) + } else if (value is Double) { + builder.add(key.toString(), value) + } else if (value is Boolean) { + builder.add(key.toString(), value) + } else if (value is Int) { + builder.add(key.toString(), value) + } else if (value is Long) { + builder.add(key.toString(), value) + } + } + clickstreamItems[index] = builder.build() + } + eventBuilder.setItems(clickstreamItems) + } ClickstreamAnalytics.recordEvent(eventBuilder.build()) } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 18fcae0..809788b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import 'package:clickstream_analytics/clickstream_analytics.dart'; +import 'package:clickstream_analytics/clickstream_analytics_item.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -54,10 +55,18 @@ class _MyAppState extends State { minLeadingWidth: 0, ), ListTile( - leading: const Icon(Icons.touch_app), - title: const Text('recordEvent'), + leading: const Icon(Icons.circle), + title: const Text('recordEventWithName'), onTap: () async { analytics.record(name: "testEventWithName"); + log("recorded testEvent with testEventWithName"); + }, + minLeadingWidth: 0, + ), + ListTile( + leading: const Icon(Icons.touch_app), + title: const Text('recordEventWithAttributes'), + onTap: () async { analytics.record(name: "testEvent", attributes: { "category": 'shoes', "currency": 'CNY', @@ -67,7 +76,47 @@ class _MyAppState extends State { "boolValue": true, "value": 279.9 }); - log("recorded testEvent and testEventWithName"); + log("recorded testEvent and attributes"); + }, + minLeadingWidth: 0, + ), + ListTile( + leading: const Icon(Icons.touch_app_outlined), + title: const Text('recordEventWithItem'), + onTap: () async { + var testItem1 = ClickstreamItem( + id: "1", + name: "testName1", + brand: "Google", + currency: "CNY", + category: "book", + locationId: "1", + attributes: { + "intValue": 21, + "longValue": 888888888813991919, + "doubleValue": 11.1234567890121213, + "boolValue": true, + "value": 279.9 + }); + var testItem2 = ClickstreamItem( + id: "2", + name: "testName2", + brand: "Sumsang", + currency: "USD", + category: "shoes", + locationId: "2", + attributes: { + "intValue": 13, + "longValue": 9999999913991919, + "doubleValue": 22.1234567890121213, + "boolValue": true, + "value": 379.9 + }); + analytics.record( + name: "testRecordItem", + attributes: {"testKey": "testValue"}, + items: [testItem1, testItem2]); + log("recorded testEvent with item"); }, minLeadingWidth: 0, ), @@ -76,6 +125,14 @@ class _MyAppState extends State { title: const Text('setUserId'), onTap: () async { analytics.setUserId("12345"); + log("setUserId"); + }, + minLeadingWidth: 0, + ), + ListTile( + leading: const Icon(Icons.no_accounts), + title: const Text('setUserIdToNull'), + onTap: () async { analytics.setUserId(null); log("setUserId"); }, diff --git a/ios/Classes/ClickstreamFlutterPlugin.swift b/ios/Classes/ClickstreamFlutterPlugin.swift index 03ad5ce..6d63560 100644 --- a/ios/Classes/ClickstreamFlutterPlugin.swift +++ b/ios/Classes/ClickstreamFlutterPlugin.swift @@ -76,8 +76,17 @@ public class ClickstreamFlutterPlugin: NSObject, FlutterPlugin { func recordEvent(_ arguments: [String: Any]) { let eventName = arguments["eventName"] as! String let attributes = arguments["attributes"] as! [String: Any] + let items = arguments["items"] as! [[String: Any]] if attributes.count > 0 { - ClickstreamAnalytics.recordEvent(eventName, getClickstreamAttributes(attributes)) + if items.count > 0 { + var clickstreamItems: [ClickstreamAttribute] = [] + for itemObject in items { + clickstreamItems.append(getClickstreamAttributes(itemObject)) + } + ClickstreamAnalytics.recordEvent(eventName, getClickstreamAttributes(attributes), clickstreamItems) + } else { + ClickstreamAnalytics.recordEvent(eventName, getClickstreamAttributes(attributes)) + } } else { ClickstreamAnalytics.recordEvent(eventName) } diff --git a/lib/clickstream_analytics.dart b/lib/clickstream_analytics.dart index 76165da..fb1c7f4 100644 --- a/lib/clickstream_analytics.dart +++ b/lib/clickstream_analytics.dart @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import 'clickstream_analytics_item.dart'; import 'clickstream_analytics_platform_interface.dart'; class ClickstreamAnalytics { @@ -32,9 +33,20 @@ class ClickstreamAnalytics { } Future record( - {required String name, Map? attributes}) { - return ClickstreamInterface.instance - .record({"eventName": name, "attributes": attributes ?? {}}); + {required String name, + Map? attributes, + List? items}) { + var itemArray = []; + if (items != null) { + for (ClickstreamItem item in items) { + itemArray.add(item.toMap()); + } + } + return ClickstreamInterface.instance.record({ + "eventName": name, + "attributes": attributes ?? {}, + "items": itemArray + }); } Future setUserId(String? userId) { diff --git a/lib/clickstream_analytics_item.dart b/lib/clickstream_analytics_item.dart new file mode 100644 index 0000000..f612e9e --- /dev/null +++ b/lib/clickstream_analytics_item.dart @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +class ClickstreamItem { + ClickstreamItem({ + this.id, + this.name, + this.locationId, + this.brand, + this.currency, + this.price, + this.quantity, + this.creativeName, + this.creativeSlot, + this.category, + this.category2, + this.category3, + this.category4, + this.category5, + this.attributes, + }); + + final String? id; + final String? name; + final String? locationId; + final String? brand; + final String? currency; + final num? price; + final int? quantity; + final String? creativeName; + final String? creativeSlot; + final String? category; + final String? category2; + final String? category3; + final String? category4; + final String? category5; + // used to add custom item attribute. + final Map? attributes; + + Map toMap() { + return { + if (id != null) 'id': id, + if (name != null) 'name': name, + if (locationId != null) 'location_id': locationId, + if (brand != null) 'brand': brand, + if (currency != null) 'currency': currency, + if (price != null) 'price': price, + if (quantity != null) 'quantity': quantity, + if (creativeName != null) 'creative_name': creativeName, + if (creativeSlot != null) 'creative_slot': creativeSlot, + if (category != null) 'category': category, + if (category2 != null) 'category2': category2, + if (category3 != null) 'category3': category3, + if (category4 != null) 'category4': category4, + if (category5 != null) 'category5': category5, + if (attributes != null) ...attributes!, + }; + } +} diff --git a/test/clickstream_flutter_test.dart b/test/clickstream_flutter_test.dart index e387cfc..01a75a9 100644 --- a/test/clickstream_flutter_test.dart +++ b/test/clickstream_flutter_test.dart @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import 'package:clickstream_analytics/clickstream_analytics.dart'; +import 'package:clickstream_analytics/clickstream_analytics_item.dart'; import 'package:clickstream_analytics/clickstream_analytics_method_channel.dart'; import 'package:clickstream_analytics/clickstream_analytics_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -77,6 +78,29 @@ void main() { attributes: {"category": "shoes", "currency": "CNY", "value": 279.9}); expect(result, isNotNull); }); + + test('record event with item', () async { + var itemBook = ClickstreamItem( + id: "123", + name: "Nature", + category: "book", + currency: "CNY", + price: 99, + attributes: {"book_publisher": "Nature Research"}); + var itemShoes = ClickstreamItem( + id: "124", + name: "Nike", + category: "shoes", + 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]); + expect(result, isNotNull); + }); + test('setUserId', () async { var result = analytics.setUserId("11234"); expect(result, isNotNull);