diff --git a/CHANGELOG.md b/CHANGELOG.md index f44df44..41eb4a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.2 + +* Added feature to customize HTTP requests + ## 1.0.1 * PianoConsents has been moved to the piano_consents package * Added feature to specify property type via forceType argument diff --git a/README.md b/README.md index aa84c04..5c692c9 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,9 @@ Initialize PianoAnalytics with your site and collect domain in your application storageLifetimeVisitor: 395, visitorStorageMode: VisitorStorageMode.fixed, ignoreLimitedAdvertisingTracking: true, - visitorId: "WEB-192203AJ" + visitorId: "WEB-192203AJ", + headers: {"X-Request-Id": "123456789"}, + query: {"request_id": "123456789"} ); @override @@ -59,6 +61,18 @@ Initialize PianoAnalytics with your site and collect domain in your application } ``` +### Set HTTP parameters + +Headers: +```dart +await _pianoAnalytics.setHeader(key: "X-User-Id", value: "WEB-192203AJ") +``` + +Query string parameters: +```dart +await _pianoAnalytics.setQuery(key: "user_id", value: "WEB-192203AJ") +``` + ### Send events ```dart await _pianoAnalytics.sendEvents(events: [ diff --git a/android/.gitignore b/android/.gitignore index f31728a..99cf51b 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -8,3 +8,5 @@ gradle /build /captures .cxx + +property* \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 96af0ea..6ba3d6f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,15 +1,15 @@ group = "io.piano.flutter.piano_analytics" -version = "1.0.1" +version = "1.0.2" buildscript { - ext.kotlin_version = "1.9.0" + ext.kotlin_version = "2.1.0" repositories { google() mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.3.0") + classpath("com.android.tools.build:gradle:8.7.3") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") } } @@ -25,7 +25,7 @@ apply plugin: "com.android.library" apply plugin: "kotlin-android" dependencies { - api("io.piano.android:analytics:3.4.1") + api("io.piano.android:analytics:3.5.0") } android { @@ -36,12 +36,12 @@ android { compileSdk = 34 compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_11 } sourceSets { diff --git a/android/src/main/kotlin/io/piano/flutter/piano_analytics/PianoAnalyticsPlugin.kt b/android/src/main/kotlin/io/piano/flutter/piano_analytics/PianoAnalyticsPlugin.kt index 8418e88..389de58 100644 --- a/android/src/main/kotlin/io/piano/flutter/piano_analytics/PianoAnalyticsPlugin.kt +++ b/android/src/main/kotlin/io/piano/flutter/piano_analytics/PianoAnalyticsPlugin.kt @@ -11,6 +11,7 @@ import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.common.StandardMethodCodec import io.piano.android.analytics.Configuration +import io.piano.android.analytics.CustomHttpDataProvider import io.piano.android.analytics.PianoAnalytics import io.piano.android.analytics.model.Event import io.piano.android.analytics.model.PrivacyMode @@ -37,7 +38,18 @@ private class Codec : StandardMessageCodec() { } } -class PianoAnalyticsPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { +class HttpDataProvider : CustomHttpDataProvider { + + val headers = mutableMapOf() + var parameters = mutableMapOf() + + override fun headers() = headers + override fun parameters() = parameters +} + +class PianoAnalyticsPlugin( + private val httpDataProvider: HttpDataProvider = HttpDataProvider() +) : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel: MethodChannel private lateinit var visitorIDType: VisitorIDType @@ -78,6 +90,8 @@ class PianoAnalyticsPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val methodResult: Any? = when (call.method) { // Main "init" -> handleInit(call) + "setHeader" -> handleSetHeader(call) + "setQuery" -> handleSetQuery(call) "send" -> handleSend(call) // User "getUser" -> handleGetUser() @@ -103,6 +117,12 @@ class PianoAnalyticsPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private fun handleInit(call: MethodCall) { visitorIDType = getVisitorIDType(call.arg("visitorIDType")) + call.argument>("headers")?.let { + httpDataProvider.headers.putAll(it) + } + call.argument>("query")?.let { + httpDataProvider.parameters.putAll(it) + } val pianoAnalytics = PianoAnalytics.init( context = context.get() ?: error("Activity not attached"), configuration = Configuration.Builder( @@ -113,9 +133,10 @@ class PianoAnalyticsPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { ?: Configuration.DEFAULT_VISITOR_STORAGE_LIFETIME, visitorStorageMode = getVisitorStorageMode(call.argument("visitorStorageMode")), ignoreLimitedAdTracking = call.argument("ignoreLimitedAdvertisingTracking") - ?: false + ?: false, ).build(), - pianoConsents = getPianoConsents() + pianoConsents = getPianoConsents(), + customHttpDataProvider = httpDataProvider ) if (visitorIDType == VisitorIDType.CUSTOM) { @@ -125,6 +146,24 @@ class PianoAnalyticsPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } + private fun handleSetHeader(call: MethodCall) { + val value = call.argument("value") + if (value != null) { + httpDataProvider.headers[call.arg("key")] = value + } else { + httpDataProvider.headers.remove(call.arg("key")) + } + } + + private fun handleSetQuery(call: MethodCall) { + val value = call.argument("value") + if (value != null) { + httpDataProvider.parameters[call.arg("key")] = value + } else { + httpDataProvider.parameters.remove(call.arg("key")) + } + } + private fun handleSend(call: MethodCall) { val events = call.arg>>("events").map { val name = it["name"] as? String ?: error("Undefined event name") diff --git a/android/src/test/kotlin/io/piano/flutter/piano_analytics/PianoAnalyticsPluginTest.kt b/android/src/test/kotlin/io/piano/flutter/piano_analytics/PianoAnalyticsPluginTest.kt index 810bfba..5a59fbe 100644 --- a/android/src/test/kotlin/io/piano/flutter/piano_analytics/PianoAnalyticsPluginTest.kt +++ b/android/src/test/kotlin/io/piano/flutter/piano_analytics/PianoAnalyticsPluginTest.kt @@ -20,6 +20,7 @@ import java.util.Date import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNull internal class PianoAnalyticsPluginTest : BasePluginTest() { @@ -31,25 +32,30 @@ internal class PianoAnalyticsPluginTest : BasePluginTest() { @Test fun `Check init`() { val pianoAnalytics: PianoAnalytics = mockk() - every { PianoAnalytics.Companion.init(any(), any(), any(), any()) } returns pianoAnalytics + every { PianoAnalytics.Companion.init(any(), any(), any(), any(), any()) } returns pianoAnalytics val customVisitorIdSlot = slot() every { pianoAnalytics.customVisitorId = capture(customVisitorIdSlot) } answers {} + val httpDataProvider = HttpDataProvider() call( - "init", mapOf( + "init", + mapOf( "site" to 123456789, "collectDomain" to "xxxxxxx.pa-cd.com", "visitorIDType" to "CUSTOM", "visitorStorageLifetime" to 395, "visitorStorageMode" to "fixed", "ignoreLimitedAdvertisingTracking" to true, - "visitorId" to "WEB-192203AJ" - ) - ) + "visitorId" to "WEB-192203AJ", + "headers" to mapOf("X-Request-ID" to "123456789"), + "query" to mapOf("request_id" to "123456789") + ), + null + ) { PianoAnalyticsPlugin(httpDataProvider) } val slot = slot() - verify { PianoAnalytics.init(any(), capture(slot), any(), any()) } + verify { PianoAnalytics.init(any(), capture(slot), any(), any(), any()) } val configuration = slot.captured assertEquals(123456789, configuration.site) @@ -58,7 +64,8 @@ internal class PianoAnalyticsPluginTest : BasePluginTest() { assertEquals(395, configuration.visitorStorageLifetime) assertEquals(VisitorStorageMode.FIXED, configuration.visitorStorageMode) assertEquals(true, configuration.ignoreLimitedAdTracking) - assertEquals("WEB-192203AJ", customVisitorIdSlot.captured) + assertEquals("123456789", httpDataProvider.headers["X-Request-ID"]) + assertEquals("123456789", httpDataProvider.parameters["request_id"]) } @Test @@ -155,6 +162,56 @@ internal class PianoAnalyticsPluginTest : BasePluginTest() { ) } + @Test + fun `Check setHeader`() { + val httpDataProvider = HttpDataProvider() + call( + "setHeader", + mapOf( + "key" to "X-User-Id", + "value" to "WEB-192203AJ" + ), + null + ) { PianoAnalyticsPlugin(httpDataProvider) } + + assertEquals("WEB-192203AJ", httpDataProvider.headers["X-User-Id"]) + + call( + "setHeader", + mapOf( + "key" to "X-User-Id" + ), + null + ) { PianoAnalyticsPlugin(httpDataProvider) } + + assertNull(httpDataProvider.headers["X-User-Id"]) + } + + @Test + fun `Check setQuery`() { + val httpDataProvider = HttpDataProvider() + call( + "setQuery", + mapOf( + "key" to "user_id", + "value" to "WEB-192203AJ" + ), + null + ) { PianoAnalyticsPlugin(httpDataProvider) } + + assertEquals("WEB-192203AJ", httpDataProvider.parameters["user_id"]) + + call( + "setQuery", + mapOf( + "key" to "user_id" + ), + null + ) { PianoAnalyticsPlugin(httpDataProvider) } + + assertNull(httpDataProvider.parameters["user_id"]) + } + @Test fun `Check getUser`() { val pianoAnalytics: PianoAnalytics = mockk() @@ -210,7 +267,7 @@ internal class PianoAnalyticsPluginTest : BasePluginTest() { @Test fun `Check getVisitorId`() { - every { PianoAnalytics.Companion.init(any(), any(), any(), any()) } returns mockk() + every { PianoAnalytics.Companion.init(any(), any(), any(), any(), any()) } returns mockk() val pianoAnalytics: PianoAnalytics = mockk() every { PianoAnalytics.Companion.getInstance() } returns pianoAnalytics diff --git a/example/.gitignore b/example/.gitignore index 7aa453c..661005f 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/example/android/.gitignore b/example/android/.gitignore index aaf553c..82fe2cd 100644 --- a/example/android/.gitignore +++ b/example/android/.gitignore @@ -12,3 +12,6 @@ GeneratedPluginRegistrant.java key.properties **/*.keystore **/*.jks + +app/property* +app/.cxx \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 77a55bd..bbad59d 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,11 +26,15 @@ if (flutterVersionName == null) { android { namespace = "io.piano.piano_analytics_example" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 } defaultConfig { @@ -55,4 +59,4 @@ android { flutter { source = "../.." -} +} \ No newline at end of file diff --git a/example/android/build.gradle b/example/android/build.gradle index 24e24ae..844ff35 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -7,7 +7,7 @@ allprojects { rootProject.layout.buildDirectory = "../build" subprojects { - project.layout.buildDirectory = "${rootProject.buildDir}/${project.name}" + project.layout.buildDirectory = "${rootProject.layout.buildDirectory.get()}/${project.name}" } subprojects { project.evaluationDependsOn(":app") diff --git a/example/android/settings.gradle b/example/android/settings.gradle index c7d430c..951934b 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '7.4.2' apply false - id "org.jetbrains.kotlin.android" version "1.9.0" apply false + id "com.android.application" version '8.7.3' apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5d..15cada4 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/example/lib/main.dart b/example/lib/main.dart index 5287207..bc67b8b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -22,7 +22,9 @@ class _MyAppState extends State { storageLifetimeVisitor: 395, visitorStorageMode: VisitorStorageMode.fixed, ignoreLimitedAdvertisingTracking: true, - visitorId: "WEB-192203AJ"); + visitorId: "WEB-192203AJ", + headers: {"X-Request-Id": "123456789"}, + query: {"request_id": "123456789"}); @override void initState() { @@ -42,8 +44,37 @@ class _MyAppState extends State { title: const Text('Piano analytics'), ), body: Center( - child: Column( + child: SingleChildScrollView( + child: Column( children: [ + // Set header + FilledButton( + onPressed: () async { + await _pianoAnalytics.setHeader( + key: "X-User-Id", + value: "WEB-192203AJ"); + }, + child: const Text("Set header")), + // Remove header + FilledButton( + onPressed: () async { + await _pianoAnalytics.setHeader(key: "X-User-Id", value: null); + }, + child: const Text("Remove header")), + // Set query + FilledButton( + onPressed: () async { + await _pianoAnalytics.setQuery( + key: "user_id", + value: "WEB-192203AJ"); + }, + child: const Text("Set query")), + // Remove query + FilledButton( + onPressed: () async { + await _pianoAnalytics.setQuery(key: "user_id", value: null); + }, + child: const Text("Remove query")), // Send events FilledButton( onPressed: () async { @@ -209,6 +240,7 @@ class _MyAppState extends State { child: const Text("Exclude events (privcy)")) ], ), + ) ), ), ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a9471c6..42f5acd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -40,7 +40,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^3.0.0 + flutter_lints: ^5.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/ios/Classes/PianoAnalyticsPlugin.swift b/ios/Classes/PianoAnalyticsPlugin.swift index 97a8036..a29d872 100644 --- a/ios/Classes/PianoAnalyticsPlugin.swift +++ b/ios/Classes/PianoAnalyticsPlugin.swift @@ -27,6 +27,11 @@ fileprivate class ReaderWriter: FlutterStandardReaderWriter { } } +fileprivate class HTTPProvider: CustomHTTPProvider { + var headers: [String : String] = [:] + var query: [String : String] = [:] +} + public class PianoAnalyticsPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { @@ -37,6 +42,16 @@ public class PianoAnalyticsPlugin: NSObject, FlutterPlugin { let instance = PianoAnalyticsPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } + + //private var pianoAnalytics: PianoAnalytics + + private var httpProvider = HTTPProvider() + + public override init() { + var extendedConfiguration = PA.ExtendedConfiguration() + extendedConfiguration.httpProvider = httpProvider + _ = PianoAnalytics.sharedWithExtendedConfiguration(extendedConfiguration) + } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { do { @@ -45,6 +60,10 @@ public class PianoAnalyticsPlugin: NSObject, FlutterPlugin { // Main case "init": try handleInit(call) + case "setHeader": + try handleSetHeader(call) + case "setQuery": + try handleSetQuery(call) case "send": try handleSend(call) // User @@ -111,12 +130,44 @@ public class PianoAnalyticsPlugin: NSObject, FlutterPlugin { _ = configurationBuilder.enableIgnoreLimitedAdTracking(ignoreLimitedAdvertisingTracking) } + if let headers = arguments["headers"] as? [String:String] { + httpProvider.headers.merge(headers) { $1 } + } + + if let query = arguments["query"] as? [String:String] { + httpProvider.query.merge(query) { $1 } + } + PianoAnalytics.shared.setConfiguration(configurationBuilder.build()) if let visitorId = arguments["visitorId"] as? String, visitorIdType == .Custom { PianoAnalytics.shared.setVisitorId(visitorId) } } + + private func handleSetHeader(_ call: FlutterMethodCall) throws { + let arguments = try getArguments(call) + let key: String = try getArgument(call, arguments, "key") + let value: String? = arguments["value"] as? String + + if let value { + httpProvider.headers[key] = value + } else { + httpProvider.headers.removeValue(forKey: key) + } + } + + private func handleSetQuery(_ call: FlutterMethodCall) throws { + let arguments = try getArguments(call) + let key: String = try getArgument(call, arguments, "key") + let value: String? = arguments["value"] as? String + + if let value { + httpProvider.query[key] = value + } else { + httpProvider.query.removeValue(forKey: key) + } + } private func handleSend(_ call: FlutterMethodCall) throws { let arguments = try getArguments(call) diff --git a/ios/piano_analytics.podspec b/ios/piano_analytics.podspec index a97d2b9..36343c2 100644 --- a/ios/piano_analytics.podspec +++ b/ios/piano_analytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'piano_analytics' - s.version = '1.0.1' + s.version = '1.0.2' s.summary = 'Piano Analytics SDK Flutter' s.homepage = 'https://piano.io/product/analytics/' s.license = { :file => '../LICENSE' } diff --git a/lib/piano_analytics.dart b/lib/piano_analytics.dart index c71d0d9..3d72d77 100644 --- a/lib/piano_analytics.dart +++ b/lib/piano_analytics.dart @@ -107,6 +107,8 @@ class PianoAnalytics { VisitorStorageMode? visitorStorageMode, bool? ignoreLimitedAdvertisingTracking, String? visitorId, + Map? headers, + Map? query, MethodChannel? channel}) : _parameters = { "site": site, @@ -115,7 +117,9 @@ class PianoAnalytics { "storageLifetimeVisitor": storageLifetimeVisitor, "visitorStorageMode": visitorStorageMode?.value, "ignoreLimitedAdvertisingTracking": ignoreLimitedAdvertisingTracking, - "visitorId": visitorId + "visitorId": visitorId, + "headers": headers, + "query": query }, _channel = channel ?? _pianoAnalyticsChannel; @@ -124,6 +128,18 @@ class PianoAnalytics { _initialized = true; } + Future setHeader( + {required String key, required String? value}) async { + await _channel.invokeMethod("setHeader", + {"key": key, "value": value}); + } + + Future setQuery( + {required String key, required String? value}) async { + await _channel.invokeMethod("setQuery", + {"key": key, "value": value}); + } + Future sendEvents({required List events}) async { _checkInit(); await _channel.invokeMethod( diff --git a/pubspec.yaml b/pubspec.yaml index f2242e0..8346908 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: piano_analytics description: "Piano Analytics SDK Flutter" -version: 1.0.1 +version: 1.0.2 homepage: https://piano.io/product/analytics/ repository: https://github.com/at-internet/piano-analytics-flutter @@ -16,7 +16,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_lints: ^5.0.0 flutter: plugin: diff --git a/test/piano_analytics_test.dart b/test/piano_analytics_test.dart index 7f3ae85..ad7fae2 100644 --- a/test/piano_analytics_test.dart +++ b/test/piano_analytics_test.dart @@ -156,6 +156,44 @@ main() { expect(arguments["visitorId"], testUserId); }); + test("setHeader", () async { + var pianoAnalytics = await getPianoAnalytics(); + pianoAnalytics.setHeader(key: "X-User-Id", value: testUserId); + + expect(call?.method, "setHeader"); + + var arguments = call?.arguments as Map; + expect(arguments["key"], "X-User-Id"); + expect(arguments["value"], testUserId); + + pianoAnalytics.setHeader(key: "X-User-Id", value: null); + + expect(call?.method, "setHeader"); + + arguments = call?.arguments as Map; + expect(arguments["key"], "X-User-Id"); + expect(arguments["value"], null); + }); + + test("setQuery", () async { + var pianoAnalytics = await getPianoAnalytics(); + pianoAnalytics.setQuery(key: "user_id", value: testUserId); + + expect(call?.method, "setQuery"); + + var arguments = call?.arguments as Map; + expect(arguments["key"], "user_id"); + expect(arguments["value"], testUserId); + + pianoAnalytics.setQuery(key: "user_id", value: null); + + expect(call?.method, "setQuery"); + + arguments = call?.arguments as Map; + expect(arguments["key"], "user_id"); + expect(arguments["value"], null); + }); + test("send", () async { var pianoAnalytics = await getPianoAnalytics(); await pianoAnalytics.sendEvents(events: [