diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 96cc43e..61a9130 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,22 +1,6 @@ - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 3097f31..57f05c9 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,7 +1,7 @@ - \ No newline at end of file diff --git a/README.MD b/README.MD index 3ff164e..69252d2 100644 --- a/README.MD +++ b/README.MD @@ -15,12 +15,17 @@ Add the following code block to in your app/build.gradle. ```groovy buildscript { dependencies { - classpath 'com.orhanobut.tracklytics:tracklytics-plugin:2.1.0' + classpath 'com.orhanobut.tracklytics:tracklytics-plugin:2.2.1' } } apply plugin: 'com.android.application' -apply plugin: 'com.orhanobut.tracklytics' // Must be added after com.android.application + +android { +... +} + +apply plugin: 'com.orhanobut.tracklytics' // Must be added at end of file ``` @@ -249,6 +254,26 @@ Use [Bee](https://github.com/orhanobut/bee) to monitor your events ### How it works +### ProGuard + +Depending on your ProGuard (DexGuard) config and usage, you may need to include the following lines in your proguard +``` +-keep class com.orhanobut.tracklytics.** { *; } +-keepclassmembers class com.orhanobut.tracklytics.** { *; } +-keep interface com.orhanobut.tracklytics.** { *; } +-keep @interface com.orhanobut.tracklytics.** { *; } +-keep enum com.orhanobut.tracklytics.** { *; } +-keepattributes Attribute,FixedAttribute,FixedAttributes,RemoveSuperAttribute,TrackableAttribute,TrackEvent,TrackSuperAttribute,TransformAttribute,TransformAttributeMap +-keepattributes *Annotation* + +-keepclasseswithmembers class * { +@com.orhanobut.tracklytics.TrackEvent *; +} + +-dontwarn com.orhanobut.tracklytics.* + +``` + ### Licence
 Copyright 2017 Orhan Obut
diff --git a/build.gradle b/build.gradle
index a68e1c9..504c993 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,81 +1,85 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 
 buildscript {
-  repositories {
-    google()
-    jcenter()
-  }
-  dependencies {
-    classpath 'com.android.tools.build:gradle:3.3.0'
-    classpath 'org.gradle.api.plugins:gradle-nexus-plugin:0.7'
-    classpath 'org.aspectj:aspectjtools:1.8.10'
-    classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0'
+    ext {
+        kotlin_version = '1.4.30'
+    }
+    repositories {
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.1.2'
+        classpath 'org.gradle.api.plugins:gradle-nexus-plugin:0.7'
+        classpath 'org.aspectj:aspectjtools:1.8.10'
+        classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 
-    // NOTE: Do not place your application dependencies here; they belong
-    // in the individual module build.gradle files
-  }
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
 }
 
 subprojects { project ->
-  group = GROUP
-  version = VERSION_NAME
-
-  apply plugin: 'checkstyle'
-  apply plugin: 'maven'
+    group = GROUP
+    version = VERSION_NAME
 
-  task checkstyle(type: Checkstyle) {
-    configFile rootProject.file('checkstyle.xml')
-    source 'src/main/java'
-    ignoreFailures false
-    showViolations true
-    include '**/*.java'
+    apply plugin: 'checkstyle'
+    apply plugin: 'maven'
 
-    classpath = files()
-  }
+    task checkstyle(type: Checkstyle) {
+        configFile rootProject.file('checkstyle.xml')
+        source 'src/main/java'
+        ignoreFailures false
+        showViolations true
+        include '**/*.java'
 
-  afterEvaluate {
-    if (project.tasks.findByName('check')) {
-      check.dependsOn('checkstyle')
+        classpath = files()
     }
-  }
 
-  buildscript {
-    repositories {
-      google()
-      jcenter()
+    afterEvaluate {
+        if (project.tasks.findByName('check')) {
+            check.dependsOn('checkstyle')
+        }
     }
-    dependencies {
-      classpath 'com.android.tools.build:gradle:3.3.0'
+
+    buildscript {
+        repositories {
+            google()
+            jcenter()
+        }
+        dependencies {
+            classpath 'com.android.tools.build:gradle:4.1.2'
+        }
     }
-  }
 
-  repositories {
-    google()
-    jcenter()
-  }
+    repositories {
+        google()
+        jcenter()
+    }
 }
 
 task clean(type: Delete) {
-  delete rootProject.buildDir
+    delete rootProject.buildDir
 }
 
 ext {
-  minSdkVersion = 10
-  targetSdkVersion = 26
-  compileSdkVersion = 26
-  buildToolsVersion = "28.0.3"
-  sourceCompatibilityVersion = JavaVersion.VERSION_1_7
-  targetCompatibilityVersion = JavaVersion.VERSION_1_7
+    minSdkVersion = 10
+    targetSdkVersion = 26
+    compileSdkVersion = 26
+    buildToolsVersion = "28.0.3"
+    sourceCompatibilityVersion = JavaVersion.VERSION_1_7
+    targetCompatibilityVersion = JavaVersion.VERSION_1_7
 }
 
 ext.deps = [
 
-    // AspectJ
-    aspectjRuntime: "org.aspectj:aspectjrt:1.8.10",
-    aspectjTools  : "org.aspectj:aspectjtools:1.8.10",
+        // AspectJ
+        aspectjRuntime: "org.aspectj:aspectjrt:1.8.10",
+        aspectjTools  : "org.aspectj:aspectjtools:1.8.10",
 
-    // Test dependencies
-    junit         : 'junit:junit:4.12',
-    truth         : 'com.google.truth:truth:0.28',
-    mockito       : "org.mockito:mockito-core:1.10.19"
+        // Test dependencies
+        junit         : 'junit:junit:4.12',
+        truth         : 'com.google.truth:truth:0.28',
+        mockito       : "org.mockito:mockito-core:1.10.19"
 ]
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 1231919..fd2d896 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-VERSION_NAME=2.1.0
+VERSION_NAME=2.2.1
 GROUP=com.orhanobut.tracklytics
 
 POM_DESCRIPTION=Android analytics tracklytics
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 895c38f..6a70e27 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue Jan 22 16:58:26 MSK 2019
+#Thu Feb 25 17:33:23 MSK 2021
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
diff --git a/library_module/.gitignore b/library_module/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/library_module/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/library_module/build.gradle b/library_module/build.gradle
new file mode 100644
index 0000000..1797a73
--- /dev/null
+++ b/library_module/build.gradle
@@ -0,0 +1,55 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'com.orhanobut.tracklytics'
+
+buildscript {
+    repositories {
+        mavenCentral()
+        mavenLocal()
+    }
+    dependencies {
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+        classpath 'com.orhanobut.tracklytics:tracklytics-plugin:2.2.1'
+    }
+}
+
+repositories {
+//  mavenCentral()
+
+  // NOTE: This is only needed when developing the plugin!
+  mavenLocal()
+}
+
+android {
+  compileSdkVersion 29
+  buildToolsVersion "28.0.3"
+
+  compileOptions {
+    compileSdkVersion rootProject.ext.compileSdkVersion
+    buildToolsVersion rootProject.ext.buildToolsVersion
+  }
+
+    defaultConfig {
+        minSdkVersion 16
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+
+}
\ No newline at end of file
diff --git a/library_module/consumer-rules.pro b/library_module/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/library_module/proguard-rules.pro b/library_module/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/library_module/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/library_module/src/androidTest/java/com/github/library_view/ExampleInstrumentedTest.kt b/library_module/src/androidTest/java/com/github/library_view/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..a3714f6
--- /dev/null
+++ b/library_module/src/androidTest/java/com/github/library_view/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.github.library_view
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.runner.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("com.github.library_view.test", appContext.packageName)
+    }
+}
\ No newline at end of file
diff --git a/library_module/src/main/AndroidManifest.xml b/library_module/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c9eb6ea
--- /dev/null
+++ b/library_module/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+    /
+
\ No newline at end of file
diff --git a/library_module/src/main/java/com/github/library_view/LibraryModuleJavaObject.java b/library_module/src/main/java/com/github/library_view/LibraryModuleJavaObject.java
new file mode 100644
index 0000000..beee11d
--- /dev/null
+++ b/library_module/src/main/java/com/github/library_view/LibraryModuleJavaObject.java
@@ -0,0 +1,10 @@
+package com.github.library_view;
+
+import com.orhanobut.tracklytics.TrackEvent;
+
+public class LibraryModuleJavaObject {
+
+    @TrackEvent("library_module_event_java")
+    public void track() {
+    }
+}
diff --git a/library_module/src/main/java/com/github/library_view/LibraryModuleKotlinObject.kt b/library_module/src/main/java/com/github/library_view/LibraryModuleKotlinObject.kt
new file mode 100644
index 0000000..b34cc61
--- /dev/null
+++ b/library_module/src/main/java/com/github/library_view/LibraryModuleKotlinObject.kt
@@ -0,0 +1,9 @@
+package com.github.library_view
+
+import com.orhanobut.tracklytics.TrackEvent
+
+class LibraryModuleKotlinObject {
+    @TrackEvent("library_module_event_kotlin")
+    fun track() {
+    }
+}
\ No newline at end of file
diff --git a/library_module/src/test/java/com/github/library_view/ExampleUnitTest.kt b/library_module/src/test/java/com/github/library_view/ExampleUnitTest.kt
new file mode 100644
index 0000000..4aaf358
--- /dev/null
+++ b/library_module/src/test/java/com/github/library_view/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.github.library_view
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+    @Test
+    fun addition_isCorrect() {
+        assertEquals(4, 2 + 2)
+    }
+}
\ No newline at end of file
diff --git a/sample/build.gradle b/sample/build.gradle
index f4cca54..75c9f09 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -1,53 +1,83 @@
 apply plugin: 'com.android.application'
 apply plugin: 'kotlin-android'
 apply plugin: 'kotlin-android-extensions'
-apply plugin: 'com.orhanobut.tracklytics'
 
 buildscript {
-  repositories {
-    mavenCentral()
-    mavenLocal()
-  }
-  dependencies {
-    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.20"
-    classpath 'com.orhanobut.tracklytics:tracklytics-plugin:2.1.0'
-  }
+    repositories {
+        mavenCentral()
+        mavenLocal()
+    }
+    dependencies {
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+        classpath 'com.orhanobut.tracklytics:tracklytics-plugin:2.2.1'
+    }
 }
 
 repositories {
-  mavenCentral()
+//  mavenCentral()
 
-  // NOTE: This is only needed when developing the plugin!
-  mavenLocal()
+    // NOTE: This is only needed when developing the plugin!
+    mavenLocal()
 }
 
 android {
-  compileSdkVersion 26
-  buildToolsVersion "28.0.3"
-
-  compileOptions {
-    compileSdkVersion rootProject.ext.compileSdkVersion
-    buildToolsVersion rootProject.ext.buildToolsVersion
-  }
-
-  defaultConfig {
-    applicationId "com.orhanobut.sample"
-    minSdkVersion 16
-    targetSdkVersion 26
-    versionCode 1
-    versionName "1.0"
-  }
-
-  sourceSets {
-    test.java.srcDirs += 'src/test/kotlin'
-
-    main.java.srcDirs += 'src/main/kotlin'
-  }
+    compileSdkVersion 29
+    buildToolsVersion "28.0.3"
+
+    compileOptions {
+        compileSdkVersion rootProject.ext.compileSdkVersion
+        buildToolsVersion rootProject.ext.buildToolsVersion
+    }
+
+    defaultConfig {
+        applicationId "com.orhanobut.sample"
+        minSdkVersion 16
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+    }
+
+    sourceSets {
+        test.java.srcDirs += 'src/test/kotlin'
+
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled true
+            debuggable false
+        }
+        stage {
+            versionNameSuffix "-stage"
+        }
+        debug {
+            versionNameSuffix '-debug'
+            debuggable true
+            minifyEnabled false
+        }
+    }
+
+    flavorDimensions "develop"
+
+    productFlavors {
+        production {
+        }
+        payment {
+        }
+        label {
+        }
+        develop {
+        }
+    }
 }
 
 dependencies {
-  testImplementation 'junit:junit:4.12'
-  testImplementation 'com.google.truth:truth:0.28'
+    testImplementation 'junit:junit:4.13.1'
+    testImplementation 'com.google.truth:truth:0.28'
 
-  implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.20"
-}
\ No newline at end of file
+    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+    implementation project(":library_module")
+}
+
+apply plugin: 'com.orhanobut.tracklytics'
diff --git a/sample/src/main/java/com/orhanobut/sample/Foo.java b/sample/src/main/java/com/orhanobut/sample/Foo.java
deleted file mode 100644
index 774fd94..0000000
--- a/sample/src/main/java/com/orhanobut/sample/Foo.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.orhanobut.sample;
-
-import com.orhanobut.tracklytics.TrackEvent;
-
-public class Foo {
-
-  @TrackEvent("event_java")
-  public void trackFoo() {
-
-  }
-}
diff --git a/sample/src/main/java/com/orhanobut/sample/JavaObject.java b/sample/src/main/java/com/orhanobut/sample/JavaObject.java
new file mode 100644
index 0000000..3c44b22
--- /dev/null
+++ b/sample/src/main/java/com/orhanobut/sample/JavaObject.java
@@ -0,0 +1,11 @@
+package com.orhanobut.sample;
+
+import com.orhanobut.tracklytics.FixedAttribute;
+import com.orhanobut.tracklytics.TrackEvent;
+
+@FixedAttribute(key="screen_name", value = "FooKotlin")
+class JavaObject {
+    @TrackEvent("event_java_object")
+    void track() {
+    }
+}
diff --git a/sample/src/main/java/com/orhanobut/sample/KotlinObject.kt b/sample/src/main/java/com/orhanobut/sample/KotlinObject.kt
new file mode 100644
index 0000000..92246df
--- /dev/null
+++ b/sample/src/main/java/com/orhanobut/sample/KotlinObject.kt
@@ -0,0 +1,9 @@
+package com.orhanobut.sample
+
+import com.orhanobut.tracklytics.TrackEvent
+
+class KotlinObject {
+    @TrackEvent("event_kotlin_object")
+    fun track() {
+    }
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/orhanobut/sample/MainActivity.java b/sample/src/main/java/com/orhanobut/sample/MainActivity.java
index 3f4fe1a..0caf7ef 100644
--- a/sample/src/main/java/com/orhanobut/sample/MainActivity.java
+++ b/sample/src/main/java/com/orhanobut/sample/MainActivity.java
@@ -4,7 +4,8 @@
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
-
+import com.github.library_view.LibraryModuleJavaObject;
+import com.github.library_view.LibraryModuleKotlinObject;
 import com.orhanobut.tracklytics.Attribute;
 import com.orhanobut.tracklytics.Event;
 import com.orhanobut.tracklytics.EventLogListener;
@@ -44,7 +45,13 @@ public class MainActivity extends Activity implements Trackable {
       @TrackEvent("button_click")
       @FixedAttribute(key = "button_name", value = "Login")
       @Override public void onClick(View v) {
-
+        onItemSelected(2);
+        String id = userId();
+        onLoggedIn(new User(id,"e@e.com"),id);
+        new JavaObject().track();
+        new KotlinObject().track();
+        new LibraryModuleJavaObject().track();
+        new LibraryModuleKotlinObject().track();
       }
     });
   }
diff --git a/sample/src/main/kotlin/com/orhanobut/sample/FooKotlin.kt b/sample/src/main/kotlin/com/orhanobut/sample/FooKotlin.kt
index a287e6e..66c24ac 100644
--- a/sample/src/main/kotlin/com/orhanobut/sample/FooKotlin.kt
+++ b/sample/src/main/kotlin/com/orhanobut/sample/FooKotlin.kt
@@ -2,11 +2,12 @@ package com.orhanobut.sample
 
 import com.orhanobut.tracklytics.FixedAttribute
 import com.orhanobut.tracklytics.TrackEvent
+import com.orhanobut.tracklytics.TransformAttribute
 
 @FixedAttribute(key="screen_name", value = "FooKotlin")
 open class FooKotlin {
 
   @TrackEvent("event_kotlin")
-  open fun trackFoo() {
+  open fun trackFoo(@TransformAttribute("fun_attribute") attribute: String?) {
   }
 }
\ No newline at end of file
diff --git a/sample/src/test/java/com/orhanobut/sample/TrackingTest.java b/sample/src/test/java/com/orhanobut/sample/TrackingTest.java
index 19f43cb..6290c05 100644
--- a/sample/src/test/java/com/orhanobut/sample/TrackingTest.java
+++ b/sample/src/test/java/com/orhanobut/sample/TrackingTest.java
@@ -27,14 +27,26 @@ public class TrackingTest {
   }
 
   @Test public void confirmKotlinAspects() {
-    new FooKotlin().trackFoo();
+    new FooKotlin().trackFoo("any_value");
 
     assertThat(triggeredEvents).containsKey("event_kotlin");
+    Event event = triggeredEvents.get("event_kotlin");
+    Map attributes = event.attributes;
+    assertThat(attributes).containsKey("screen_name");
+    assertThat(attributes).containsKey("fun_attribute");
+    assertThat(attributes.get("fun_attribute")).isEqualTo("any_value");
+  }
+  @Test public void confirmJavaObjectAspects() {
+    new JavaObject().track();
+
+    assertThat(triggeredEvents).containsKey("event_java_object");
+    Event event = triggeredEvents.get("event_java_object");
+    assertThat(event.attributes).containsKey("screen_name");
   }
 
   @Test public void confirmJavaAspects() {
-    new Foo().trackFoo();
+    new KotlinObject().track();
 
-    assertThat(triggeredEvents).containsKey("event_java");
+    assertThat(triggeredEvents).containsKey("event_kotlin_object");
   }
 }
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 2ce0d77..0a13918 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,4 @@
+include ':library_module'
 include ':tracklytics-plugin'
 include ':sample'
 include ':tracklytics-runtime'
diff --git a/tracklytics-plugin/build.gradle b/tracklytics-plugin/build.gradle
index 3881760..eb31774 100644
--- a/tracklytics-plugin/build.gradle
+++ b/tracklytics-plugin/build.gradle
@@ -6,9 +6,9 @@ dependencies {
   implementation localGroovy()
   implementation 'org.aspectj:aspectjtools:1.8.10'
   implementation 'org.aspectj:aspectjrt:1.8.10'
-  implementation 'com.android.tools.build:gradle:3.3.0'
+  implementation 'com.android.tools.build:gradle:4.0.0'
 
-  testImplementation 'junit:junit:4.12'
+  testImplementation 'junit:junit:4.13.1'
 }
 
 modifyPom {
diff --git a/tracklytics-plugin/src/main/groovy/tracklytics/weaving/plugin/TracklyticsPlugin.groovy b/tracklytics-plugin/src/main/groovy/tracklytics/weaving/plugin/TracklyticsPlugin.groovy
index 4f50b4d..8c208cc 100644
--- a/tracklytics-plugin/src/main/groovy/tracklytics/weaving/plugin/TracklyticsPlugin.groovy
+++ b/tracklytics-plugin/src/main/groovy/tracklytics/weaving/plugin/TracklyticsPlugin.groovy
@@ -8,77 +8,159 @@ import org.gradle.api.tasks.compile.JavaCompile
 
 class TracklyticsPlugin implements Plugin {
 
-  @Override
-  void apply(Project project) {
+    @Override
+    void apply(Project project) {
+
+        println("----------------------------------------------")
+        println("--------------Tracklytics Weave---------------")
+
+        project.dependencies {
+            println("-----------add dependencies------------------------")
+
+            implementation 'org.aspectj:aspectjrt:1.8.10'
+            implementation 'com.orhanobut.tracklytics:tracklytics-runtime:2.2.1'
+            compileOnly "org.aspectj:aspectjrt:1.8.10"
+        }
+
+        if (!project.hasProperty("android")) {
+            throw RuntimeException("\'apply plugin: \'com.orhanobut.tracklytics\'\' should be added after " +
+                    "\'apply plugin: \'com.android.application' or \'apply plugin: \'com.android.library\'\'")
+        }
+
+        if (project.android.hasProperty("libraryVariants")) {
+            /*if the plugin is added to android library module*/
+            println("-----------libraryVariants------------------------")
+            project.android.libraryVariants.all { variant ->
+                JavaCompile javaCompile
+                if (variant.hasProperty('javaCompileProvider')) {
+                    // Android 3.3.0+
+                    javaCompile = variant.javaCompileProvider.get()
+                } else {
+                    javaCompile = variant.javaCompile
+                }
+
+                javaCompile.doLast {
+                    def dirName = variant.name
+                    runWithArgs(javaCompile, project, dirName, "libraryVariants javaCompile")
+                }
+            }
+        } else if (project.android.hasProperty("applicationVariants")) {
+            /*if the plugin is added to android application module*/
+            println("-----------applicationVariants------------------------")
+
+            if (project.android.productFlavors.size() == 0) {
+                /*if android application hasn't flavors*/
+                println("-----------hasn't flavors------------------------")
+                project.android.applicationVariants.all { variant ->
+                    JavaCompile javaCompile
+                    if (variant.hasProperty('javaCompileProvider')) {
+                        // Android 3.3.0+
+                        javaCompile = variant.javaCompileProvider.get()
+                    } else {
+                        javaCompile = variant.javaCompile
+                    }
+
+                    // Gets the variant name and capitalize the first character
+                    def taskPartName = variant.name[0].toUpperCase() + variant.name[1..-1].toLowerCase()
+
+                    // Weave the binary for the actual code
+                    // CompileSources task is invoked after java and kotlin compilers and copy kotlin classes
+                    // That's the moment we have the finalized byte code and we can weave the aspects
+                    project.tasks.findByName('compile' + taskPartName + 'Sources')?.doLast {
+                        def dirName = variant.name
+                        runWithArgs(javaCompile, project, dirName, "applicationVariants " + 'compile' + taskPartName + 'Sources')
+                    }
+
+                    // Weave the binary for unit tests
+                    // compile unit tests task is invoked after the byte code is finalized
+                    // This is the time that we can weave the aspects onto byte code
+                    project.tasks.findByName('compile' + taskPartName + 'UnitTestSources')?.doLast {
+                        def dirName = variant.name
+                        runWithArgs(javaCompile, project, dirName, "applicationVariants " + 'compile' + taskPartName + 'UnitTestSources')
+                    }
+                }
+            } else {
+                /*if android application has flavors*/
+                println("-----------has flavors ${project.android.productFlavors.size()}------------------------")
+                project.android.applicationVariants.all { variant ->
+                    JavaCompile javaCompile
+                    if (variant.hasProperty('javaCompileProvider')) {
+                        // Android 3.3.0+
+                        javaCompile = variant.javaCompileProvider.get()
+                    } else {
+                        javaCompile = variant.javaCompile
+                    }
+
+                    def variantName = variant.name
+                    println("variantName: ${variantName}")
+
+                    project.android.productFlavors.all { flavor ->
+                        def flavorName = flavor.name
+                        if (variantName.toLowerCase().contains(flavorName.toLowerCase())) {
+                            println("flavor: ${flavorName}")
+                            project.android.buildTypes.all { buildType ->
+                                def buildTypeName = buildType.name
+                                if (variantName.toLowerCase().contains(buildTypeName.toLowerCase())) {
+                                    println("buildType: ${buildTypeName}")
+                                    def taskPartName = flavorName[0].toUpperCase() + flavorName[1..-1].toLowerCase() + buildTypeName[0].toUpperCase() + buildTypeName[1..-1].toLowerCase()
+                                    project.tasks.findByName('compile' + taskPartName + 'Sources')?.doLast {
+                                        def dirName = flavorName + buildTypeName[0].toUpperCase() + buildTypeName[1..-1].toLowerCase()
+                                        runWithArgs(javaCompile, project, dirName, "applicationVariants " + 'compile' + taskPartName + 'Sources')
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            throw RuntimeException("\'apply plugin: \'com.orhanobut.tracklytics\'\' should be added after " +
+                    "\'apply plugin: \'com.android.application' or \'apply plugin: \'com.android.library\'\'")
+        }
 
-    project.dependencies {
-      implementation 'org.aspectj:aspectjrt:1.8.10'
-      implementation 'com.orhanobut.tracklytics:tracklytics-runtime:2.1.0'
     }
 
-    project.android.applicationVariants.all { variant ->
-      JavaCompile javaCompile
-      if (variant.hasProperty('javaCompileProvider')) {
-        // Android 3.3.0+
-        javaCompile = variant.javaCompileProvider.get()
-      } else {
-        javaCompile = variant.javaCompile
-      }
-
-      // Gets the variant name and capitalize the first character
-      def variantName = variant.name[0].toUpperCase() + variant.name[1..-1].toLowerCase()
-
-      // Weave the binary for the actual code
-      // CompileSources task is invoked after java and kotlin compilers and copy kotlin classes
-      // That's the moment we have the finalized byte code and we can weave the aspects
-      project.tasks.findByName('compile' + variantName + 'Sources')?.doLast {
+    private static void runWithArgs(JavaCompile javaCompile, Project project, dirName, type) {
         def destinationDir = javaCompile.destinationDir.toString()
-        def classPath = javaCompile.classpath.asPath
         def bootClassPath = project.android.bootClasspath.join(File.pathSeparator)
-        String[] args = [
+        def classPath = javaCompile.classpath.asPath
+        def aspectPath = javaCompile.classpath.asFileTree.filter {
+            it.canonicalPath.contains("tracklytics-runtime")
+        }.asPath
+        String[] javaArgs = [
                 "-showWeaveInfo",
                 "-1.7",
                 "-inpath", destinationDir,
-                "-aspectpath", classPath,
+                "-aspectpath", aspectPath,
                 "-d", destinationDir,
                 "-classpath", classPath,
                 "-bootclasspath", bootClassPath
         ]
-        new Main().run(args, new MessageHandler(true));
-        println("----------------------------------------------")
-        println("--------------Tracklytics Weave---------------")
-        println("----------------------------------------------")
-        println("destinationDir: $destinationDir")
-        println("classPath: $classPath")
-        println("bootClassPath: $bootClassPath")
-        println("----------------------------------------------")
-      }
-
-      // Weave the binary for unit tests
-      // compile unit tests task is invoked after the byte code is finalized
-      // This is the time that we can weave the aspects onto byte code
-      project.tasks.findByName('compile' + variantName + 'UnitTestSources')?.doLast {
-        def destinationDir = javaCompile.destinationDir.toString()
-        def classPath = javaCompile.classpath.asPath
-        def bootClassPath = project.android.bootClasspath.join(File.pathSeparator)
-        String[] args = [
+        String[] kotlinArgs = [
                 "-showWeaveInfo",
-                "-1.7",
-                "-inpath", destinationDir,
-                "-aspectpath", classPath,
-                "-d", destinationDir,
+                "-1.8",
+                "-inpath", project.buildDir.path + "/tmp/kotlin-classes/" + dirName,
+                "-aspectpath", aspectPath,
+                "-d", project.buildDir.path + "/tmp/kotlin-classes/" + dirName,
                 "-classpath", classPath,
                 "-bootclasspath", bootClassPath
         ]
-        new Main().run(args, new MessageHandler(true));
+
+        MessageHandler handler = new MessageHandler(true)
+        new Main().run(javaArgs, handler)
+        new Main().run(kotlinArgs, handler)
+
         println("----------------------------------------------")
-        println("--------------Tracklytics Weave---------------")
+        println("--------------Tracklytics Weave ($type)---------------")
         println("----------------------------------------------")
+        println("dirName: $dirName")
         println("destinationDir: $destinationDir")
         println("classPath: $classPath")
+        println "aspectpath: $aspectPath"
         println("bootClassPath: $bootClassPath")
+        println("javaArgs: $javaArgs")
+        println("kotlinArgs: $kotlinArgs")
         println("----------------------------------------------")
-      }
     }
-  }
+
 }
\ No newline at end of file
diff --git a/tracklytics-runtime/build.gradle b/tracklytics-runtime/build.gradle
index 1bbfabf..5bfc972 100644
--- a/tracklytics-runtime/build.gradle
+++ b/tracklytics-runtime/build.gradle
@@ -2,72 +2,88 @@ import org.aspectj.bridge.MessageHandler
 import org.aspectj.tools.ajc.Main
 
 buildscript {
-  repositories {
-    jcenter()
-  }
-  dependencies {
-    classpath 'org.aspectj:aspectjtools:1.8.10'
-  }
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath 'org.aspectj:aspectjtools:1.8.10'
+    }
 }
 
 apply plugin: 'com.android.library'
 apply plugin: 'com.github.dcendents.android-maven'
 
 android {
-  compileSdkVersion rootProject.ext.compileSdkVersion
-  buildToolsVersion rootProject.ext.buildToolsVersion
+    compileSdkVersion rootProject.ext.compileSdkVersion
+    buildToolsVersion rootProject.ext.buildToolsVersion
 
-  defaultConfig {
-    minSdkVersion rootProject.ext.minSdkVersion
-    consumerProguardFiles 'consumer-proguard-rules.pro'
-  }
+    defaultConfig {
+        minSdkVersion rootProject.ext.minSdkVersion
+        consumerProguardFiles 'consumer-proguard-rules.pro'
+    }
 
-  lintOptions {
-    textReport true
-    textOutput 'stdout'
-  }
-  buildToolsVersion "28.0.3"
+    lintOptions {
+        textReport true
+        textOutput 'stdout'
+    }
+    buildToolsVersion "28.0.3"
 }
 
 dependencies {
-  compileOnly "org.aspectj:aspectjrt:1.8.10"
+    compileOnly "org.aspectj:aspectjrt:1.8.10"
+    testImplementation "org.aspectj:aspectjrt:1.8.10"
 
-  testImplementation 'junit:junit:4.12'
-  testImplementation 'com.google.truth:truth:0.28'
-  testImplementation "org.mockito:mockito-core:1.10.19"
+    testImplementation 'junit:junit:4.13.1'
+    testImplementation 'com.google.truth:truth:0.28'
+    testImplementation "org.mockito:mockito-core:3.7.7"
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 }
 
 android.libraryVariants.all { variant ->
-  JavaCompile javaCompile
-  if (variant.hasProperty('javaCompileProvider')) {
-    // Android 3.3.0+
-    javaCompile = variant.javaCompileProvider.get()
-  } else {
-    javaCompile = variant.javaCompile
-  }
-  javaCompile.doLast {
-    def destinationDir = javaCompile.destinationDir.toString()
-    def classPath = javaCompile.classpath.asPath
-    def bootClassPath = project.android.bootClasspath.join(File.pathSeparator)
-    String[] args = [
-            "-showWeaveInfo",
-            "-1.7",
-            "-inpath", destinationDir,
-            "-aspectpath", classPath,
-            "-d", destinationDir,
-            "-classpath", classPath,
-            "-bootclasspath", bootClassPath
-    ]
+    JavaCompile javaCompile
+    if (variant.hasProperty('javaCompileProvider')) {
+        // Android 3.3.0+
+        javaCompile = variant.javaCompileProvider.get()
+    } else {
+        javaCompile = variant.javaCompile
+    }
+    javaCompile.doLast {
+        def destinationDir = javaCompile.destinationDir.toString()
+        def bootClassPath = project.android.bootClasspath.join(File.pathSeparator)
+        String[] javaArgs = [
+                "-showWeaveInfo",
+                "-1.7",
+                "-inpath", destinationDir,
+                "-aspectpath", javaCompile.classpath.asPath/*javaCompile.classpath.asFileTree.filter {!it.canonicalPath.contains("transforms")}.asPath*/,
+                "-d", destinationDir,
+                "-classpath", javaCompile.classpath.asPath,
+                "-bootclasspath", bootClassPath
+        ]
 
-    new Main().run(args, new MessageHandler(true));
-    println("----------------------------------------------")
-    println("---------Tracklytics-runtime Weave------------")
-    println("----------------------------------------------")
-    println("destinationDir: $destinationDir")
-    println("classPath: $classPath")
-    println("bootClassPath: $bootClassPath")
-    println("----------------------------------------------")
-  }
+        String[] kotlinArgs = [
+                "-showWeaveInfo",
+                "-1.8",
+                "-inpath", project.buildDir.path + "/tmp/kotlin-classes/" + variant,
+                "-aspectpath", javaCompile.classpath.asPath/*javaCompile.classpath.asFileTree.filter {!it.canonicalPath.contains("transforms")}.asPath*/,
+                "-d", project.buildDir.path + "/tmp/kotlin-classes/" + variant,
+                "-classpath", javaCompile.classpath.asPath,
+                "-bootclasspath", bootClassPath
+        ]
+
+        MessageHandler handler = new MessageHandler(true)
+        new Main().run(javaArgs, handler)
+        new Main().run(kotlinArgs, handler)
+        println("----------------------------------------------")
+        println("---------Tracklytics-runtime Weave------------")
+        println("----------------------------------------------")
+        println("variant: $variant")
+        println("destinationDir: $destinationDir")
+        println("classPath: ${javaCompile.classpath.asPath}")
+        println("bootClassPath: $bootClassPath")
+        println("javaArgs: $javaArgs")
+        println("kotlinArgs: $kotlinArgs")
+        println("----------------------------------------------")
+    }
 }
 
 apply from: rootProject.file('gradle/maven_push.gradle')
diff --git a/tracklytics-runtime/src/main/java/com/orhanobut/tracklytics/Trackable.java b/tracklytics-runtime/src/main/java/com/orhanobut/tracklytics/Trackable.java
index b616036..385b87d 100644
--- a/tracklytics-runtime/src/main/java/com/orhanobut/tracklytics/Trackable.java
+++ b/tracklytics-runtime/src/main/java/com/orhanobut/tracklytics/Trackable.java
@@ -1,5 +1,7 @@
 package com.orhanobut.tracklytics;
 
+import org.jetbrains.annotations.Nullable;
+
 import java.util.Map;
 
 /**
@@ -9,5 +11,6 @@
 @SuppressWarnings("WeakerAccess")
 public interface Trackable {
 
+  @Nullable
   Map getTrackableAttributes();
 }
diff --git a/tracklytics-runtime/src/test/java/com/orhanobut/tracklytics/TracklyticsAspectTest.java b/tracklytics-runtime/src/test/java/com/orhanobut/tracklytics/TracklyticsAspectTest.java
index 7a400f8..282645a 100644
--- a/tracklytics-runtime/src/test/java/com/orhanobut/tracklytics/TracklyticsAspectTest.java
+++ b/tracklytics-runtime/src/test/java/com/orhanobut/tracklytics/TracklyticsAspectTest.java
@@ -19,675 +19,729 @@
 @SuppressWarnings("ALL")
 public class TracklyticsAspectTest {
 
-  @Mock ProceedingJoinPoint joinPoint;
-  @Mock MethodSignature methodSignature;
-
-  private final Map superAttributes = new HashMap<>();
-
-  private TracklyticsAspect aspect;
-  private TrackEvent trackEvent;
-  private Map attributes;
-  private AspectListener aspectListener;
-
-  @Before public void setup() throws Exception {
-    initMocks(this);
-
-    aspectListener = new AspectListener() {
-      @Override public void onAspectEventTriggered(TrackEvent trackEvent, Map attributes) {
-        TracklyticsAspectTest.this.trackEvent = trackEvent;
-        TracklyticsAspectTest.this.attributes = attributes;
-      }
-
-      @Override public void onAspectSuperAttributeAdded(String key, Object value) {
-        superAttributes.put(key, value);
-      }
-
-      @Override public void onAspectSuperAttributeRemoved(String key) {
-        superAttributes.remove(key);
-      }
-    };
-
-    aspect = new TracklyticsAspect();
-    aspect.subscribe(aspectListener);
-
-    when(joinPoint.getSignature()).thenReturn(methodSignature);
-  }
-
-  private Method invokeMethod(Class klass, String methodName, Class... parameterTypes) throws Throwable {
-    Method method = initMethod(klass, methodName, parameterTypes);
-    Object instance = new Object();
-    when(joinPoint.getThis()).thenReturn(instance);
-
-    aspect.weaveJoinPointTrackEvent(joinPoint);
-    return method;
-  }
-
-  private Method initMethod(Class klass, String name, Class... parameterTypes) throws Throwable {
-    Method method = klass.getMethod(name, parameterTypes);
-    when(methodSignature.getMethod()).thenReturn(method);
-    return method;
-  }
-
-  @Test public void trackEventWithoutAttributes() throws Throwable {
-    class Foo {
-      @TrackEvent("title") public void foo() {
-      }
-    }
-    invokeMethod(Foo.class, "foo");
-
-    ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class);
-
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .noAttributes();
-  }
-
-  @Test public void useReturnValueAsAttribute() throws Throwable {
-    class Foo {
-      @TrackEvent("title") @Attribute("key") public String foo() {
-        return "test";
-      }
-    }
-
-    when(joinPoint.proceed()).thenReturn("test");
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("title")
-        .noTags()
-        .noFilters()
-        .attribute("key", "test");
-  }
-
-  @Test public void useReturnValueAndParametersAsAttributes() throws Throwable {
-    class Foo {
-      @TrackEvent("title") @Attribute("key1") public String foo(@Attribute("key2") String param) {
-        return "test";
-      }
-    }
-
-    when(joinPoint.proceed()).thenReturn("test");
-    when(joinPoint.getArgs()).thenReturn(new Object[]{"param"});
-    invokeMethod(Foo.class, "foo", String.class);
-
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "test")
-        .attribute("key2", "param");
-  }
-
-  @Test public void useDefaultValueWhenThereIsNoReturnValue() throws Throwable {
-    class Foo {
-      @TrackEvent("title")
-      @Attribute(value = "key1", defaultValue = "defaultValue") public void foo() {
-      }
-    }
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "defaultValue");
-  }
-
-  @Test public void useReturnValueWhenItIsNotNull() throws Throwable {
-    class Foo {
-      @TrackEvent("title")
-      @Attribute(value = "key1", defaultValue = "defaulValue") public String foo() {
-        return "returnValue";
-      }
-    }
-    when(joinPoint.proceed()).thenReturn("returnValue");
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "returnValue");
-  }
-
-  @Test public void useDefaultValueWhenParameterValueIsNull() throws Throwable {
-    class Foo {
-      @TrackEvent("title") public void foo(@Attribute(value = "key1", defaultValue = "default") String val) {
-      }
-    }
-
-    when(joinPoint.getArgs()).thenReturn(new Object[]{null});
-    invokeMethod(Foo.class, "foo", String.class);
-
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "default");
-  }
-
-  @Test public void fixedAttributeOnMethodScope() throws Throwable {
-    class Foo {
-      @TrackEvent("title")
-      @FixedAttribute(key = "key1", value = "value") public String foo() {
-        return "returnValue";
-      }
-    }
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "value");
-  }
-
-  @Test public void fixedAttributeOnClassScope() throws Throwable {
-    @FixedAttributes({
-        @FixedAttribute(key = "key1", value = "value1"),
-        @FixedAttribute(key = "key2", value = "value2")
-    })
-    @FixedAttribute(key = "key3", value = "value3")
-    class Foo {
-      @TrackEvent("title")
-      @FixedAttribute(key = "key4", value = "value4")
-      public void foo() {
-      }
-    }
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "value1")
-        .attribute("key2", "value2")
-        .attribute("key3", "value3")
-        .attribute("key4", "value4");
-  }
-
-  @Test public void fixedAttributeAndAttributeAtSameTime() throws Throwable {
-    class Foo {
-      @TrackEvent("title")
-      @Attribute("key1")
-      @FixedAttribute(key = "key2", value = "value2")
-      public String foo() {
-        return "value1";
-      }
-    }
-
-    when(joinPoint.proceed()).thenReturn("value1");
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "value1")
-        .attribute("key2", "value2");
-  }
-
-  @Test public void fixedAttributes() throws Throwable {
-    class Foo {
-      @TrackEvent("title")
-      @FixedAttributes({
-          @FixedAttribute(key = "key1", value = "value1"),
-          @FixedAttribute(key = "key2", value = "value2")
-      })
-      @FixedAttribute(key = "key3", value = "value3")
-      public void foo() {
-      }
-    }
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "value1")
-        .attribute("key2", "value2")
-        .attribute("key3", "value3");
-  }
-
-  @Test public void superAttribute() throws Throwable {
-    class Foo {
-      @TrackEvent("title")
-      @Attribute(value = "key1", isSuper = true)
-      public String foo(@Attribute(value = "key2", isSuper = true) String value) {
-        return "value1";
-      }
-    }
-
-    when(joinPoint.proceed()).thenReturn("value1");
-    when(joinPoint.getArgs()).thenReturn(new Object[]{"value2"});
-
-    invokeMethod(Foo.class, "foo", String.class);
-
-    assertThat(superAttributes).containsExactly("key1", "value1", "key2", "value2");
-  }
-
-  @Test public void superFixedAttribute() throws Throwable {
-    class Foo {
-      @TrackEvent("title")
-      @FixedAttributes({
-          @FixedAttribute(key = "key1", value = "value1"),
-          @FixedAttribute(key = "key2", value = "value2", isSuper = true)
-      })
-      @FixedAttribute(key = "key3", value = "value3", isSuper = true)
-      public String foo() {
-        return "returnValue";
-      }
-    }
-
-    when(joinPoint.proceed()).thenReturn("value1");
-    invokeMethod(Foo.class, "foo");
-
-    assertThat(superAttributes).containsExactly("key2", "value2", "key3", "value3");
-  }
+    @Mock
+    ProceedingJoinPoint joinPoint;
+    @Mock
+    MethodSignature methodSignature;
 
-  @Test public void superTransformAttribute() throws Throwable {
-    class Foo {
-      @TrackEvent("event")
-      @TransformAttributeMap(
-          keys = {0, 1},
-          values = {"value1", "value2"}
-      )
-      @TransformAttribute(value = "key1", isSuper = true)
-      public int foo(@TransformAttribute(value = "key2", isSuper = true) Integer val) {
-        return 0;
-      }
+    private final Map superAttributes = new HashMap<>();
+
+    private TracklyticsAspect aspect;
+    private TrackEvent trackEvent;
+    private Map attributes;
+    private AspectListener aspectListener;
+
+    @Before
+    public void setup() throws Exception {
+        initMocks(this);
+
+        aspectListener = new AspectListener() {
+            @Override
+            public void onAspectEventTriggered(TrackEvent trackEvent, Map attributes) {
+                TracklyticsAspectTest.this.trackEvent = trackEvent;
+                TracklyticsAspectTest.this.attributes = attributes;
+            }
+
+            @Override
+            public void onAspectSuperAttributeAdded(String key, Object value) {
+                superAttributes.put(key, value);
+            }
+
+            @Override
+            public void onAspectSuperAttributeRemoved(String key) {
+                superAttributes.remove(key);
+            }
+        };
+
+        aspect = new TracklyticsAspect();
+        aspect.subscribe(aspectListener);
+
+        when(joinPoint.getSignature()).thenReturn(methodSignature);
     }
 
-    when(joinPoint.proceed()).thenReturn(0);
-    when(joinPoint.getArgs()).thenReturn(new Object[]{1});
-    invokeMethod(Foo.class, "foo", Integer.class);
+    private Method invokeMethod(Class klass, String methodName, Class... parameterTypes) throws Throwable {
+        Method method = initMethod(klass, methodName, parameterTypes);
+        Object instance = new Object();
+        when(joinPoint.getThis()).thenReturn(instance);
 
-    assertThat(superAttributes).containsExactly("key1", "value1", "key2", "value2");
-  }
+        aspect.weaveJoinPointTrackEvent(joinPoint);
+        return method;
+    }
+
+    private Method initMethod(Class klass, String name, Class... parameterTypes) throws Throwable {
+        Method method = klass.getMethod(name, parameterTypes);
+        when(methodSignature.getMethod()).thenReturn(method);
+        return method;
+    }
 
-  @Test public void trackable() throws Throwable {
-    class Bar implements Trackable {
+    @Test
+    public void trackEventWithoutAttributes() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            public void foo() {
+            }
+        }
+        invokeMethod(Foo.class, "foo");
+
+        ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class);
 
-      @Override public Map getTrackableAttributes() {
-        Map values = new HashMap<>();
-        values.put("key1", "value1");
-        values.put("key2", "value2");
-        return values;
-      }
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .noAttributes();
     }
 
-    class Foo {
-      @TrackEvent("title") public void foo(@TrackableAttribute Bar bar) {
-      }
+    @Test
+    public void useReturnValueAsAttribute() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute("key")
+            public String foo() {
+                return "test";
+            }
+        }
+
+        when(joinPoint.proceed()).thenReturn("test");
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("title")
+                .noTags()
+                .noFilters()
+                .attribute("key", "test");
     }
 
-    when(joinPoint.getArgs()).thenReturn(new Object[]{new Bar()});
+    @Test
+    public void useReturnValueAndParametersAsAttributes() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute("key1")
+            public String foo(@Attribute("key2") String param) {
+                return "test";
+            }
+        }
+
+        when(joinPoint.proceed()).thenReturn("test");
+        when(joinPoint.getArgs()).thenReturn(new Object[]{"param"});
+        invokeMethod(Foo.class, "foo", String.class);
+
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "test")
+                .attribute("key2", "param");
+    }
+
+    @Test
+    public void useDefaultValueWhenThereIsNoReturnValue() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute(value = "key1", defaultValue = "defaultValue")
+            public void foo() {
+            }
+        }
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "defaultValue");
+    }
+
+    @Test
+    public void useReturnValueWhenItIsNotNull() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute(value = "key1", defaultValue = "defaulValue")
+            public String foo() {
+                return "returnValue";
+            }
+        }
+        when(joinPoint.proceed()).thenReturn("returnValue");
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "returnValue");
+    }
+
+    @Test
+    public void useDefaultValueWhenParameterValueIsNull() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            public void foo(@Attribute(value = "key1", defaultValue = "default") String val) {
+            }
+        }
 
-    invokeMethod(Foo.class, "foo", Bar.class);
+        when(joinPoint.getArgs()).thenReturn(new Object[]{null});
+        invokeMethod(Foo.class, "foo", String.class);
 
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "value1")
-        .attribute("key2", "value2");
-  }
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "default");
+    }
 
-  @Test public void ignoreNullValuesOnTrackable() throws Throwable {
-    class Bar implements Trackable {
+    @Test
+    public void fixedAttributeOnMethodScope() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            @FixedAttribute(key = "key1", value = "value")
+            public String foo() {
+                return "returnValue";
+            }
+        }
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "value");
+    }
+
+    @Test
+    public void fixedAttributeOnClassScope() throws Throwable {
+        @FixedAttributes({
+                @FixedAttribute(key = "key1", value = "value1"),
+                @FixedAttribute(key = "key2", value = "value2")
+        })
+        @FixedAttribute(key = "key3", value = "value3")
+        class Foo {
+            @TrackEvent("title")
+            @FixedAttribute(key = "key4", value = "value4")
+            public void foo() {
+            }
+        }
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "value1")
+                .attribute("key2", "value2")
+                .attribute("key3", "value3")
+                .attribute("key4", "value4");
+    }
+
+    @Test
+    public void fixedAttributeAndAttributeAtSameTime() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute("key1")
+            @FixedAttribute(key = "key2", value = "value2")
+            public String foo() {
+                return "value1";
+            }
+        }
 
-      @Override public Map getTrackableAttributes() {
-        return null;
-      }
+        when(joinPoint.proceed()).thenReturn("value1");
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "value1")
+                .attribute("key2", "value2");
+    }
+
+    @Test
+    public void fixedAttributes() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            @FixedAttributes({
+                    @FixedAttribute(key = "key1", value = "value1"),
+                    @FixedAttribute(key = "key2", value = "value2")
+            })
+            @FixedAttribute(key = "key3", value = "value3")
+            public void foo() {
+            }
+        }
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "value1")
+                .attribute("key2", "value2")
+                .attribute("key3", "value3");
+    }
+
+    @Test
+    public void superAttribute() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute(value = "key1", isSuper = true)
+            public String foo(@Attribute(value = "key2", isSuper = true) String value) {
+                return "value1";
+            }
+        }
+
+        when(joinPoint.proceed()).thenReturn("value1");
+        when(joinPoint.getArgs()).thenReturn(new Object[]{"value2"});
+
+        invokeMethod(Foo.class, "foo", String.class);
+
+        assertThat(superAttributes).containsExactly("key1", "value1", "key2", "value2");
+    }
+
+    @Test
+    public void superFixedAttribute() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            @FixedAttributes({
+                    @FixedAttribute(key = "key1", value = "value1"),
+                    @FixedAttribute(key = "key2", value = "value2", isSuper = true)
+            })
+            @FixedAttribute(key = "key3", value = "value3", isSuper = true)
+            public String foo() {
+                return "returnValue";
+            }
+        }
+
+        when(joinPoint.proceed()).thenReturn("value1");
+        invokeMethod(Foo.class, "foo");
+
+        assertThat(superAttributes).containsExactly("key2", "value2", "key3", "value3");
+    }
+
+    @Test
+    public void superTransformAttribute() throws Throwable {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(
+                    keys = {0, 1},
+                    values = {"value1", "value2"}
+            )
+            @TransformAttribute(value = "key1", isSuper = true)
+            public int foo(@TransformAttribute(value = "key2", isSuper = true) Integer val) {
+                return 0;
+            }
+        }
+
+        when(joinPoint.proceed()).thenReturn(0);
+        when(joinPoint.getArgs()).thenReturn(new Object[]{1});
+        invokeMethod(Foo.class, "foo", Integer.class);
+
+        assertThat(superAttributes).containsExactly("key1", "value1", "key2", "value2");
     }
 
-    class Foo {
-      @TrackEvent("title") public void foo(@TrackableAttribute Bar bar) {
-      }
+    @Test
+    public void trackable() throws Throwable {
+        class Bar implements Trackable {
+
+            @Override
+            public Map getTrackableAttributes() {
+                Map values = new HashMap<>();
+                values.put("key1", "value1");
+                values.put("key2", "value2");
+                return values;
+            }
+        }
+
+        class Foo {
+            @TrackEvent("title")
+            public void foo(@TrackableAttribute Bar bar) {
+            }
+        }
+
+        when(joinPoint.getArgs()).thenReturn(new Object[]{new Bar()});
+
+        invokeMethod(Foo.class, "foo", Bar.class);
+
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "value1")
+                .attribute("key2", "value2");
     }
 
-    when(joinPoint.getArgs()).thenReturn(new Object[]{new Bar()});
+    @Test
+    public void ignoreNullValuesOnTrackable() throws Throwable {
+        class Bar implements Trackable {
 
-    invokeMethod(Foo.class, "foo", Bar.class);
+            @Override
+            public Map getTrackableAttributes() {
+                return null;
+            }
+        }
+
+        class Foo {
+            @TrackEvent("title")
+            public void foo(@TrackableAttribute Bar bar) {
+            }
+        }
 
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .noAttributes();
-  }
+        when(joinPoint.getArgs()).thenReturn(new Object[]{new Bar()});
 
-  @Test public void throwExceptionWhenTrackableAnnotationNotMatchWithValue() throws Throwable {
+        invokeMethod(Foo.class, "foo", Bar.class);
 
-    class Foo {
-      @TrackEvent("title") public void foo(@TrackableAttribute String bar) {
-      }
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .noAttributes();
     }
 
-    when(joinPoint.getArgs()).thenReturn(new Object[]{"sdfsd"});
+    @Test
+    public void throwExceptionWhenTrackableAnnotationNotMatchWithValue() throws Throwable {
 
-    try {
-      invokeMethod(Foo.class, "foo", String.class);
+        class Foo {
+            @TrackEvent("title")
+            public void foo(@TrackableAttribute String bar) {
+            }
+        }
+
+        when(joinPoint.getArgs()).thenReturn(new Object[]{"sdfsd"});
 
-      fail("Should throw exception");
-    } catch (Exception e) {
-      assertThat(e).hasMessage("Trackable interface must be implemented for the parameter type");
+        try {
+            invokeMethod(Foo.class, "foo", String.class);
+
+            fail("Should throw exception");
+        } catch (Exception e) {
+            assertThat(e).hasMessage("Trackable interface must be implemented for the parameter type");
+        }
     }
-  }
 
-  @Test public void methodParameterWithoutAnnotation() throws Throwable {
-    class Foo {
-      @TrackEvent("title") public void foo(@Attribute("Key") String bar, String param2) {
-      }
+    @Test
+    public void methodParameterWithoutAnnotation() throws Throwable {
+        class Foo {
+            @TrackEvent("title")
+            public void foo(@Attribute("Key") String bar, String param2) {
+            }
+        }
+
+        when(joinPoint.getArgs()).thenReturn(new Object[]{"sdfsd"});
+
+        invokeMethod(Foo.class, "foo", String.class, String.class);
+
+        try {
+            aspect.weaveJoinPointTrackEvent(joinPoint);
+        } catch (Exception e) {
+            fail("Method parameters without annotation should be accepted");
+        }
     }
 
-    when(joinPoint.getArgs()).thenReturn(new Object[]{"sdfsd"});
+    @Test
+    public void classWideAttributeInAnonymousClass() throws Throwable {
+        @FixedAttribute(key = "key1", value = "value1")
+        class Foo {
+
+            @FixedAttribute(key = "key2", value = "value2")
+            class Inner {
 
-    invokeMethod(Foo.class, "foo", String.class, String.class);
+                @TrackEvent("title")
+                public void bar() {
+                }
+            }
+        }
+
+        invokeMethod(Foo.Inner.class, "bar");
+
+        assertTrack()
+                .event("title")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "value1")
+                .attribute("key2", "value2");
+    }
+
+    @Test
+    public void transformAttributeForParameters() throws Throwable {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(
+                    keys = {0, 1},
+                    values = {"value1", "value2"}
+            )
+            public void foo(@TransformAttribute("key1") Integer type) {
+            }
+        }
 
-    try {
-      aspect.weaveJoinPointTrackEvent(joinPoint);
-    } catch (Exception e) {
-      fail("Method parameters without annotation should be accepted");
+        when(joinPoint.getArgs()).thenReturn(new Object[]{0});
+        invokeMethod(Foo.class, "foo", Integer.class);
+
+        assertTrack()
+                .event("event")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "value1");
+    }
+
+    @Test
+    public void transformAttributeMapInvalidState() throws Throwable {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(
+                    keys = {0, 1},
+                    values = {"value1"}
+            )
+            public void foo(@TransformAttribute("key1") Integer type) {
+            }
+        }
+
+        when(joinPoint.getArgs()).thenReturn(new Object[]{0});
+
+        try {
+            invokeMethod(Foo.class, "foo", Integer.class);
+        } catch (Exception e) {
+            assertThat(e).hasMessage("TransformAttributeMap keys and values must have same length");
+        }
     }
-  }
 
-  @Test public void classWideAttributeInAnonymousClass() throws Throwable {
-    @FixedAttribute(key = "key1", value = "value1")
-    class Foo {
+    @Test
+    public void transformAttributeWithoutTransformAttributeMap() throws Throwable {
+        class Foo {
+            @TrackEvent("event")
+            public void foo(@TransformAttribute("key1") Integer type) {
+            }
+        }
 
-      @FixedAttribute(key = "key2", value = "value2")
-      class Inner {
+        when(joinPoint.getArgs()).thenReturn(new Object[]{0});
 
-        @TrackEvent("title")
-        public void bar() {
+        try {
+            invokeMethod(Foo.class, "foo", Integer.class);
+        } catch (Exception e) {
+            assertThat(e).hasMessage("Method must have TransformAttributeMap when TransformAttribute is used");
         }
-      }
     }
 
-    invokeMethod(Foo.Inner.class, "bar");
+    @Test
+    public void transformAttributeForReturnValue() throws Throwable {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(
+                    keys = {0, 1},
+                    values = {"value1", "value2"}
+            )
+            @TransformAttribute("key1")
+            public int foo() {
+                return 1;
+            }
+        }
+
+        when(joinPoint.proceed()).thenReturn(1);
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("event")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "value2");
+    }
+
+    @Test
+    public void transformAttributeDefaultValue() throws Throwable {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(
+                    keys = {0, 1},
+                    values = {"value1", "value2"}
+            )
+            @TransformAttribute(value = "key1", defaultValue = "default1")
+            public String foo(@TransformAttribute(value = "key2", defaultValue = "default2") Integer val) {
+                return null;
+            }
+        }
 
-    assertTrack()
-        .event("title")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "value1")
-        .attribute("key2", "value2");
-  }
+        when(joinPoint.getArgs()).thenReturn(new Object[]{null});
+        invokeMethod(Foo.class, "foo", Integer.class);
 
-  @Test public void transformAttributeForParameters() throws Throwable {
-    class Foo {
-      @TrackEvent("event")
-      @TransformAttributeMap(
-          keys = {0, 1},
-          values = {"value1", "value2"}
-      )
-      public void foo(@TransformAttribute("key1") Integer type) {
-      }
+        assertTrack()
+                .event("event")
+                .noFilters()
+                .noTags()
+                .attribute("key1", "default1")
+                .attribute("key2", "default2");
     }
 
-    when(joinPoint.getArgs()).thenReturn(new Object[]{0});
-    invokeMethod(Foo.class, "foo", Integer.class);
+    @Test
+    public void trackableAttributeForCurrentClass() throws Throwable {
+        class Foo implements Trackable {
+
+            @Override
+            public Map getTrackableAttributes() {
+                Map map = new HashMap<>();
+                map.put("key", "value");
+                return map;
+            }
 
-    assertTrack()
-        .event("event")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "value1");
-  }
+            @TrackEvent("event")
+            @TrackableAttribute
+            public void foo() {
+            }
+        }
+
+        initMethod(Foo.class, "foo");
+        when(joinPoint.getThis()).thenReturn(new Foo());
+        aspect.weaveJoinPointTrackEvent(joinPoint);
 
-  @Test public void transformAttributeMapInvalidState() throws Throwable {
-    class Foo {
-      @TrackEvent("event")
-      @TransformAttributeMap(
-          keys = {0, 1},
-          values = {"value1"}
-      )
-      public void foo(@TransformAttribute("key1") Integer type) {
-      }
+        assertTrack()
+                .event("event")
+                .noFilters()
+                .noTags()
+                .attribute("key", "value");
     }
 
-    when(joinPoint.getArgs()).thenReturn(new Object[]{0});
+    @Test
+    public void doNotUseTrackableAttributesWhenTrackableAttributeNotExists() throws Throwable {
+        class Foo implements Trackable {
+
+            @Override
+            public Map getTrackableAttributes() {
+                Map map = new HashMap<>();
+                map.put("key", "value");
+                return map;
+            }
 
-    try {
-      invokeMethod(Foo.class, "foo", Integer.class);
-    } catch (Exception e) {
-      assertThat(e).hasMessage("TransformAttributeMap keys and values must have same length");
+            @TrackEvent("event")
+            public void foo() {
+            }
+        }
+
+        when(joinPoint.getThis()).thenReturn(new Foo());
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("event")
+                .noFilters()
+                .noTags()
+                .noAttributes();
+    }
+
+    @Test
+    public void ignoreNullValueOnTrackableAttributeForCurrentClass() throws Throwable {
+        class Foo implements Trackable {
+
+            @Override
+            public Map getTrackableAttributes() {
+                return null;
+            }
+
+            @TrackEvent("event")
+            @TrackableAttribute
+            public void foo() {
+            }
+        }
+
+        initMethod(Foo.class, "foo");
+        when(joinPoint.getThis()).thenReturn(new Foo());
+        aspect.weaveJoinPointTrackEvent(joinPoint);
+
+        assertTrack()
+                .event("event")
+                .noFilters()
+                .noTags()
+                .noAttributes();
+    }
+
+    @Test
+    public void overrideClassWideAttributeOnMethodWhenAttributesAreSame() throws Throwable {
+        @FixedAttribute(key = "key", value = "class")
+        @FixedAttributes(
+                @FixedAttribute(key = "key1", value = "class1")
+        )
+        class Foo {
+
+            @TrackEvent("event")
+            @FixedAttribute(key = "key", value = "method")
+            @FixedAttributes(
+                    @FixedAttribute(key = "key1", value = "method1")
+            )
+            public void foo() {
+            }
+        }
+
+        invokeMethod(Foo.class, "foo");
+
+        assertTrack()
+                .event("event")
+                .noFilters()
+                .noTags()
+                .attribute("key", "method")
+                .attribute("key1", "method1");
     }
-  }
 
-  @Test public void transformAttributeWithoutTransformAttributeMap() throws Throwable {
-    class Foo {
-      @TrackEvent("event")
-      public void foo(@TransformAttribute("key1") Integer type) {
-      }
+    @Test
+    public void useThisClassWhenCalledFromSuperClass() throws Throwable {
+        @FixedAttribute(key = "key0", value = "value0")
+        class Base {
+
+            @TrackEvent("event")
+            public void base() {
+            }
+        }
+
+        @FixedAttribute(key = "key", value = "value")
+        @FixedAttributes(
+                @FixedAttribute(key = "key2", value = "value2")
+        )
+        class Foo extends Base {
+        }
+
+        initMethod(Foo.class, "base");
+        when(joinPoint.getThis()).thenReturn(new Foo());
+        aspect.weaveJoinPointTrackEvent(joinPoint);
+
+        assertTrack()
+                .event("event")
+                .noFilters()
+                .noTags()
+                .attribute("key0", "value0")
+                .attribute("key", "value")
+                .attribute("key2", "value2");
+    }
+
+    @Test
+    public void filters() throws Throwable {
+        class Foo {
+            @TrackEvent(value = "event", filters = {100, 200})
+            public void foo() {
+            }
+        }
+
+        invokeMethod(Foo.class, "foo");
+
+        int[] tags = {100, 200};
+
+        assertTrack()
+                .event("event")
+                .noTags()
+                .filters(100, 200)
+                .noAttributes();
     }
 
-    when(joinPoint.getArgs()).thenReturn(new Object[]{0});
+    @Test
+    public void tags() throws Throwable {
+        class Foo {
+            @TrackEvent(value = "event", tags = {"abc", "123"})
+            public void foo() {
+            }
+        }
+
+        invokeMethod(Foo.class, "foo");
 
-    try {
-      invokeMethod(Foo.class, "foo", Integer.class);
-    } catch (Exception e) {
-      assertThat(e).hasMessage("Method must have TransformAttributeMap when TransformAttribute is used");
-    }
-  }
+        int[] tags = {100, 200};
 
-  @Test public void transformAttributeForReturnValue() throws Throwable {
-    class Foo {
-      @TrackEvent("event")
-      @TransformAttributeMap(
-          keys = {0, 1},
-          values = {"value1", "value2"}
-      )
-      @TransformAttribute("key1")
-      public int foo() {
-        return 1;
-      }
-    }
-
-    when(joinPoint.proceed()).thenReturn(1);
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("event")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "value2");
-  }
-
-  @Test public void transformAttributeDefaultValue() throws Throwable {
-    class Foo {
-      @TrackEvent("event")
-      @TransformAttributeMap(
-          keys = {0, 1},
-          values = {"value1", "value2"}
-      )
-      @TransformAttribute(value = "key1", defaultValue = "default1")
-      public String foo(@TransformAttribute(value = "key2", defaultValue = "default2") Integer val) {
-        return null;
-      }
-    }
-
-    when(joinPoint.getArgs()).thenReturn(new Object[]{null});
-    invokeMethod(Foo.class, "foo", Integer.class);
-
-    assertTrack()
-        .event("event")
-        .noFilters()
-        .noTags()
-        .attribute("key1", "default1")
-        .attribute("key2", "default2");
-  }
-
-  @Test public void trackableAttributeForCurrentClass() throws Throwable {
-    class Foo implements Trackable {
-
-      @Override public Map getTrackableAttributes() {
-        Map map = new HashMap<>();
-        map.put("key", "value");
-        return map;
-      }
-
-      @TrackEvent("event")
-      @TrackableAttribute
-      public void foo() {
-      }
-    }
-
-    initMethod(Foo.class, "foo");
-    when(joinPoint.getThis()).thenReturn(new Foo());
-    aspect.weaveJoinPointTrackEvent(joinPoint);
-
-    assertTrack()
-        .event("event")
-        .noFilters()
-        .noTags()
-        .attribute("key", "value");
-  }
-
-  @Test public void doNotUseTrackableAttributesWhenTrackableAttributeNotExists() throws Throwable {
-    class Foo implements Trackable {
-
-      @Override public Map getTrackableAttributes() {
-        Map map = new HashMap<>();
-        map.put("key", "value");
-        return map;
-      }
-
-      @TrackEvent("event")
-      public void foo() {
-      }
-    }
-
-    when(joinPoint.getThis()).thenReturn(new Foo());
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("event")
-        .noFilters()
-        .noTags()
-        .noAttributes();
-  }
-
-  @Test public void ignoreNullValueOnTrackableAttributeForCurrentClass() throws Throwable {
-    class Foo implements Trackable {
-
-      @Override public Map getTrackableAttributes() {
-        return null;
-      }
-
-      @TrackEvent("event")
-      @TrackableAttribute
-      public void foo() {
-      }
-    }
-
-    initMethod(Foo.class, "foo");
-    when(joinPoint.getThis()).thenReturn(new Foo());
-    aspect.weaveJoinPointTrackEvent(joinPoint);
-
-    assertTrack()
-        .event("event")
-        .noFilters()
-        .noTags()
-        .noAttributes();
-  }
-
-  @Test public void overrideClassWideAttributeOnMethodWhenAttributesAreSame() throws Throwable {
-    @FixedAttribute(key = "key", value = "class")
-    @FixedAttributes(
-        @FixedAttribute(key = "key1", value = "class1")
-    )
-    class Foo {
-
-      @TrackEvent("event")
-      @FixedAttribute(key = "key", value = "method")
-      @FixedAttributes(
-          @FixedAttribute(key = "key1", value = "method1")
-      )
-      public void foo() {
-      }
-    }
-
-    invokeMethod(Foo.class, "foo");
-
-    assertTrack()
-        .event("event")
-        .noFilters()
-        .noTags()
-        .attribute("key", "method")
-        .attribute("key1", "method1");
-  }
-
-  @Test public void useThisClassWhenCalledFromSuperClass() throws Throwable {
-    @FixedAttribute(key = "key0", value = "value0")
-    class Base {
-
-      @TrackEvent("event")
-      public void base() {
-      }
-    }
-
-    @FixedAttribute(key = "key", value = "value")
-    @FixedAttributes(
-        @FixedAttribute(key = "key2", value = "value2")
-    )
-    class Foo extends Base {
-    }
-
-    initMethod(Foo.class, "base");
-    when(joinPoint.getThis()).thenReturn(new Foo());
-    aspect.weaveJoinPointTrackEvent(joinPoint);
-
-    assertTrack()
-        .event("event")
-        .noFilters()
-        .noTags()
-        .attribute("key0", "value0")
-        .attribute("key", "value")
-        .attribute("key2", "value2");
-  }
-
-  @Test public void filters() throws Throwable {
-    class Foo {
-      @TrackEvent(value = "event", filters = {100, 200})
-      public void foo() {
-      }
+        assertTrack()
+                .event("event")
+                .noFilters()
+                .tags("abc", "123")
+                .noAttributes();
     }
 
-    invokeMethod(Foo.class, "foo");
-
-    int[] tags = {100, 200};
-
-    assertTrack()
-        .event("event")
-        .noTags()
-        .filters(100, 200)
-        .noAttributes();
-  }
-
-  @Test public void tags() throws Throwable {
-    class Foo {
-      @TrackEvent(value = "event", tags = {"abc", "123"})
-      public void foo() {
-      }
-    }
-
-    invokeMethod(Foo.class, "foo");
-
-    int[] tags = {100, 200};
-
-    assertTrack()
-        .event("event")
-        .noFilters()
-        .tags("abc", "123")
-        .noAttributes();
-  }
-
-  AssertTracker assertTrack() {
-    return new AssertTracker(trackEvent, attributes);
-  }
+    AssertTracker assertTrack() {
+        return new AssertTracker(trackEvent, attributes);
+    }
 
 }
diff --git a/tracklytics-runtime/src/test/java/com/orhanobut/tracklytics/TracklyticsAttrTest.kt b/tracklytics-runtime/src/test/java/com/orhanobut/tracklytics/TracklyticsAttrTest.kt
new file mode 100644
index 0000000..85624b2
--- /dev/null
+++ b/tracklytics-runtime/src/test/java/com/orhanobut/tracklytics/TracklyticsAttrTest.kt
@@ -0,0 +1,661 @@
+package com.orhanobut.tracklytics
+
+import com.google.common.truth.Truth
+import junit.framework.Assert
+import org.aspectj.lang.ProceedingJoinPoint
+import org.aspectj.lang.reflect.MethodSignature
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import java.lang.reflect.Method
+import java.util.HashMap
+
+class TracklyticsAttrTest {
+    @Mock
+    lateinit var joinPoint: ProceedingJoinPoint
+
+    @Mock
+    lateinit var methodSignature: MethodSignature
+
+    private val superAttributes: MutableMap = HashMap()
+    lateinit var aspect: TracklyticsAspect
+    private var trackEvent: TrackEvent? = null
+    private var attributes: Map? = null
+    private var aspectListener: AspectListener? = null
+
+    @Before
+    @Throws(Exception::class)
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        aspectListener = object : AspectListener {
+            override fun onAspectEventTriggered(trackEvent: TrackEvent, attributes: Map) {
+                this@TracklyticsAttrTest.trackEvent = trackEvent
+                this@TracklyticsAttrTest.attributes = attributes
+            }
+
+            override fun onAspectSuperAttributeAdded(key: String, value: Any) {
+                superAttributes[key] = value
+            }
+
+            override fun onAspectSuperAttributeRemoved(key: String) {
+                superAttributes.remove(key)
+            }
+        }
+        aspect = TracklyticsAspect()
+        TracklyticsAspect.subscribe(aspectListener)
+        Mockito.`when`(joinPoint.signature).thenReturn(methodSignature)
+    }
+
+    @Throws(Throwable::class)
+    private fun invokeMethod(klass: Class<*>, methodName: String, vararg parameterTypes: Class<*>): Method {
+        val method = initMethod(klass, methodName, *parameterTypes)
+        val instance = Any()
+        Mockito.`when`(joinPoint.getThis()).thenReturn(instance)
+        aspect.weaveJoinPointTrackEvent(joinPoint)
+        return method
+    }
+
+    @Throws(Throwable::class)
+    private fun initMethod(klass: Class<*>, name: String, vararg parameterTypes: Class<*>): Method {
+        val method = klass.getMethod(name, *parameterTypes)
+        Mockito.`when`(methodSignature.method).thenReturn(method)
+        return method
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun trackEventWithoutAttributes() {
+        class Foo {
+            @TrackEvent("title")
+            fun foo() {
+            }
+        }
+        invokeMethod(Foo::class.java, "foo")
+        val argument = ArgumentCaptor.forClass(
+            MutableMap::class.java
+        )
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .noAttributes()
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun useReturnValueAsAttribute() {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute("key")
+            fun foo(): String {
+                return "test"
+            }
+        }
+        Mockito.`when`(joinPoint.proceed()).thenReturn("test")
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("title")
+            .noTags()
+            .noFilters()
+            .attribute("key", "test")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun useReturnValueAndParametersAsAttributes() {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute("key1")
+            fun foo(@Attribute("key2") param: String?): String {
+                return "test"
+            }
+        }
+        Mockito.`when`(joinPoint.proceed()).thenReturn("test")
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf("param"))
+        invokeMethod(Foo::class.java, "foo", String::class.java)
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "test")
+            .attribute("key2", "param")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun useDefaultValueWhenThereIsNoReturnValue() {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute(value = "key1", defaultValue = "defaultValue")
+            fun foo() {
+            }
+        }
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "defaultValue")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun useReturnValueWhenItIsNotNull() {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute(value = "key1", defaultValue = "defaulValue")
+            fun foo(): String {
+                return "returnValue"
+            }
+        }
+        Mockito.`when`(joinPoint.proceed()).thenReturn("returnValue")
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "returnValue")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun useDefaultValueWhenParameterValueIsNull() {
+        class Foo {
+            @TrackEvent("title")
+            fun foo(@Attribute(value = "key1", defaultValue = "default") `val`: String?) {
+            }
+        }
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf(null))
+        invokeMethod(Foo::class.java, "foo", String::class.java)
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "default")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun fixedAttributeOnMethodScope() {
+        class Foo {
+            @TrackEvent("title")
+            @FixedAttribute(key = "key1", value = "value")
+            fun foo(): String {
+                return "returnValue"
+            }
+        }
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "value")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun fixedAttributeOnClassScope() {
+        @FixedAttributes(FixedAttribute(key = "key1", value = "value1"), FixedAttribute(key = "key2", value = "value2"))
+        @FixedAttribute(key = "key3", value = "value3")
+        class Foo {
+            @TrackEvent("title")
+            @FixedAttribute(key = "key4", value = "value4")
+            fun foo() {
+            }
+        }
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "value1")
+            .attribute("key2", "value2")
+            .attribute("key3", "value3")
+            .attribute("key4", "value4")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun fixedAttributeAndAttributeAtSameTime() {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute("key1")
+            @FixedAttribute(key = "key2", value = "value2")
+            fun foo(): String {
+                return "value1"
+            }
+        }
+        Mockito.`when`(joinPoint.proceed()).thenReturn("value1")
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "value1")
+            .attribute("key2", "value2")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun fixedAttributes() {
+        class Foo {
+            @TrackEvent("title")
+            @FixedAttributes(FixedAttribute(key = "key1", value = "value1"), FixedAttribute(key = "key2", value = "value2"))
+            @FixedAttribute(key = "key3", value = "value3")
+            fun foo() {
+            }
+        }
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "value1")
+            .attribute("key2", "value2")
+            .attribute("key3", "value3")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun superAttribute() {
+        class Foo {
+            @TrackEvent("title")
+            @Attribute(value = "key1", isSuper = true)
+            fun foo(@Attribute(value = "key2", isSuper = true) value: String?): String {
+                return "value1"
+            }
+        }
+        Mockito.`when`(joinPoint.proceed()).thenReturn("value1")
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf("value2"))
+        invokeMethod(Foo::class.java, "foo", String::class.java)
+        Truth.assertThat(superAttributes).containsExactly("key1", "value1", "key2", "value2")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun superFixedAttribute() {
+        class Foo {
+            @TrackEvent("title")
+            @FixedAttributes(FixedAttribute(key = "key1", value = "value1"), FixedAttribute(key = "key2", value = "value2", isSuper = true))
+            @FixedAttribute(key = "key3", value = "value3", isSuper = true)
+            fun foo(): String {
+                return "returnValue"
+            }
+        }
+        Mockito.`when`(joinPoint.proceed()).thenReturn("value1")
+        invokeMethod(Foo::class.java, "foo")
+        Truth.assertThat(superAttributes).containsExactly("key2", "value2", "key3", "value3")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun superTransformAttribute() {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(keys = [0, 1], values = ["value1", "value2"])
+            @TransformAttribute(value = "key1", isSuper = true)
+            fun foo(@TransformAttribute(value = "key2", isSuper = true) value: Int): Int {
+                return 0
+            }
+        }
+        Mockito.`when`(joinPoint.proceed()).thenReturn(0)
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf(1))
+        invokeMethod(Foo::class.java, "foo", Int::class.java)
+        Truth.assertThat(superAttributes).containsExactly("key1", "value1", "key2", "value2")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun trackable() {
+        class Bar : Trackable {
+            override fun getTrackableAttributes(): Map {
+                val values: MutableMap = HashMap()
+                values["key1"] = "value1"
+                values["key2"] = "value2"
+                return values
+            }
+        }
+
+        class Foo {
+            @TrackEvent("title")
+            fun foo(@TrackableAttribute bar: Bar?) {
+            }
+        }
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf(Bar()))
+        invokeMethod(Foo::class.java, "foo", Bar::class.java)
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "value1")
+            .attribute("key2", "value2")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun ignoreNullValuesOnTrackable() {
+        class Bar : Trackable {
+            override fun getTrackableAttributes(): Map? {
+                return null
+            }
+        }
+
+        class Foo {
+            @TrackEvent("title")
+            fun foo(@TrackableAttribute bar: Bar?) {
+            }
+        }
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf(Bar()))
+        invokeMethod(Foo::class.java, "foo", Bar::class.java)
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .noAttributes()
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun throwExceptionWhenTrackableAnnotationNotMatchWithValue() {
+        class Foo {
+            @TrackEvent("title")
+            fun foo(@TrackableAttribute bar: String?) {
+            }
+        }
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf("sdfsd"))
+        try {
+            invokeMethod(Foo::class.java, "foo", String::class.java)
+            Assert.fail("Should throw exception")
+        } catch (e: Exception) {
+            Truth.assertThat(e).hasMessage("Trackable interface must be implemented for the parameter type")
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun methodParameterWithoutAnnotation() {
+        class Foo {
+            @TrackEvent("title")
+            fun foo(@Attribute("Key") bar: String?, param2: String?) {
+            }
+        }
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf("sdfsd"))
+        invokeMethod(Foo::class.java, "foo", String::class.java, String::class.java)
+        try {
+            aspect.weaveJoinPointTrackEvent(joinPoint)
+        } catch (e: Exception) {
+            Assert.fail("Method parameters without annotation should be accepted")
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun classWideAttributeInAnonymousClass() {
+        @FixedAttribute(key = "key1", value = "value1")
+        class Foo {
+            @FixedAttribute(key = "key2", value = "value2")
+            inner class Inner {
+                @TrackEvent("title")
+                fun bar() {
+                }
+            }
+        }
+        invokeMethod(Foo.Inner::class.java, "bar")
+        assertTrack()
+            .event("title")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "value1")
+            .attribute("key2", "value2")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun transformAttributeForParameters() {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(keys = [0, 1], values = ["value1", "value2"])
+            fun foo(@TransformAttribute("key1") type: Int) {
+            }
+        }
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf(0))
+        invokeMethod(Foo::class.java, "foo", Int::class.java)
+        assertTrack()
+            .event("event")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "value1")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun transformAttributeMapInvalidState() {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(keys = [0, 1], values = ["value1"])
+            fun foo(@TransformAttribute("key1") type: Int) {
+            }
+        }
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf(0))
+        try {
+            invokeMethod(Foo::class.java, "foo", Int::class.java)
+        } catch (e: Exception) {
+            Truth.assertThat(e).hasMessage("TransformAttributeMap keys and values must have same length")
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun transformAttributeWithoutTransformAttributeMap() {
+        class Foo {
+            @TrackEvent("event")
+            fun foo(@TransformAttribute("key1") type: Int) {
+            }
+        }
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf(0))
+        try {
+            invokeMethod(Foo::class.java, "foo", Int::class.java)
+        } catch (e: Exception) {
+            Truth.assertThat(e).hasMessage("Method must have TransformAttributeMap when TransformAttribute is used")
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun transformAttributeForReturnValue() {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(keys = [0, 1], values = ["value1", "value2"])
+            @TransformAttribute("key1")
+            fun foo(): Int {
+                return 1
+            }
+        }
+        Mockito.`when`(joinPoint.proceed()).thenReturn(1)
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("event")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "value2")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun transformAttributeDefaultValue() {
+        class Foo {
+            @TrackEvent("event")
+            @TransformAttributeMap(keys = [0, 1], values = ["value1", "value2"])
+            @TransformAttribute(value = "key1", defaultValue = "default1")
+            fun foo(@TransformAttribute(value = "key2", defaultValue = "default2") `val`: Int): String? {
+                return null
+            }
+        }
+        Mockito.`when`(joinPoint.args).thenReturn(arrayOf(null))
+        invokeMethod(Foo::class.java, "foo", Int::class.java)
+        assertTrack()
+            .event("event")
+            .noFilters()
+            .noTags()
+            .attribute("key1", "default1")
+            .attribute("key2", "default2")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun trackableAttributeForCurrentClass() {
+        class Foo : Trackable {
+            override fun getTrackableAttributes(): Map {
+                val map: MutableMap = HashMap()
+                map["key"] = "value"
+                return map
+            }
+
+            @TrackEvent("event")
+            @TrackableAttribute
+            fun foo() {
+            }
+        }
+        initMethod(Foo::class.java, "foo")
+        Mockito.`when`(joinPoint.getThis()).thenReturn(Foo())
+        aspect.weaveJoinPointTrackEvent(joinPoint)
+        assertTrack()
+            .event("event")
+            .noFilters()
+            .noTags()
+            .attribute("key", "value")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun doNotUseTrackableAttributesWhenTrackableAttributeNotExists() {
+        class Foo : Trackable {
+            override fun getTrackableAttributes(): Map {
+                val map: MutableMap = HashMap()
+                map["key"] = "value"
+                return map
+            }
+
+            @TrackEvent("event")
+            fun foo() {
+            }
+        }
+        Mockito.`when`(joinPoint.getThis()).thenReturn(Foo())
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("event")
+            .noFilters()
+            .noTags()
+            .noAttributes()
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun ignoreNullValueOnTrackableAttributeForCurrentClass() {
+        class Foo : Trackable {
+            override fun getTrackableAttributes(): Map? {
+                return null
+            }
+
+            @TrackEvent("event")
+            @TrackableAttribute
+            fun foo() {
+            }
+        }
+        initMethod(Foo::class.java, "foo")
+        Mockito.`when`(joinPoint.getThis()).thenReturn(Foo())
+        aspect.weaveJoinPointTrackEvent(joinPoint)
+        assertTrack()
+            .event("event")
+            .noFilters()
+            .noTags()
+            .noAttributes()
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun overrideClassWideAttributeOnMethodWhenAttributesAreSame() {
+        @FixedAttribute(key = "key", value = "class")
+        @FixedAttributes(FixedAttribute(key = "key1", value = "class1"))
+        class Foo {
+            @TrackEvent("event")
+            @FixedAttribute(key = "key", value = "method")
+            @FixedAttributes(FixedAttribute(key = "key1", value = "method1"))
+            fun foo() {
+            }
+        }
+        invokeMethod(Foo::class.java, "foo")
+        assertTrack()
+            .event("event")
+            .noFilters()
+            .noTags()
+            .attribute("key", "method")
+            .attribute("key1", "method1")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun useThisClassWhenCalledFromSuperClass() {
+        @FixedAttribute(key = "key0", value = "value0")
+        open class Base {
+            @TrackEvent("event")
+            fun base() {
+            }
+        }
+
+        @FixedAttribute(key = "key", value = "value")
+        @FixedAttributes(FixedAttribute(key = "key2", value = "value2"))
+        class Foo : Base()
+        initMethod(Foo::class.java, "base")
+        Mockito.`when`(joinPoint.getThis()).thenReturn(Foo())
+        aspect.weaveJoinPointTrackEvent(joinPoint)
+        assertTrack()
+            .event("event")
+            .noFilters()
+            .noTags()
+            .attribute("key0", "value0")
+            .attribute("key", "value")
+            .attribute("key2", "value2")
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun filters() {
+        class Foo {
+            @TrackEvent(value = "event", filters = [100, 200])
+            fun foo() {
+            }
+        }
+        invokeMethod(Foo::class.java, "foo")
+        val tags = intArrayOf(100, 200)
+        assertTrack()
+            .event("event")
+            .noTags()
+            .filters(100, 200)
+            .noAttributes()
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun tags() {
+        class Foo {
+            @TrackEvent(value = "event", tags = ["abc", "123"])
+            fun foo() {
+            }
+        }
+        invokeMethod(Foo::class.java, "foo")
+        val tags = intArrayOf(100, 200)
+        assertTrack()
+            .event("event")
+            .noFilters()
+            .tags("abc", "123")
+            .noAttributes()
+    }
+
+    private fun assertTrack(): AssertTracker {
+        return AssertTracker(trackEvent, attributes)
+    }
+}
\ No newline at end of file