diff --git a/example/src/main/java/com/example/MainActivity.java b/example/src/main/java/com/example/MainActivity.java
index 10003620..426d3e71 100644
--- a/example/src/main/java/com/example/MainActivity.java
+++ b/example/src/main/java/com/example/MainActivity.java
@@ -9,6 +9,7 @@
import android.widget.EditText;
import android.widget.TextView;
+import com.parsely.parselyandroid.ConversionType;
import com.parsely.parselyandroid.ParselyTracker;
import com.parsely.parselyandroid.SiteIdSource;
import com.parsely.parselyandroid.ParselyTrackerInternal;
@@ -100,7 +101,21 @@ public void trackPageview(View view) {
// the post has an internet-accessible URL, we will crawl it. urlMetadata is only used
// in the case of app-only content that we can't crawl.
ParselyTracker.sharedInstance().trackPageview(
- "http://example.com/article1.html", "http://example.com/", null, null, getSiteId()
+ getUrl(), "http://example.com/", null, null, getSiteId()
+ );
+ }
+
+ public void trackConversion(View view) {
+ final Map extraData = new HashMap<>();
+ extraData.put("plan", "weekly");
+ ParselyTracker.sharedInstance().trackConversion(
+ getUrl(),
+ ConversionType.SUBSCRIPTION,
+ "demo_conversion",
+ "",
+ null,
+ extraData,
+ getSiteId()
);
}
@@ -147,4 +162,12 @@ private SiteIdSource getSiteId() {
}
return new SiteIdSource.Custom(fromEditText.toString());
}
+
+ private String getUrl() {
+ Editable fromEditText = ((EditText) findViewById(R.id.custom_url)).getText();
+ if (fromEditText == null || fromEditText.toString().isEmpty()) {
+ return "http://example.com/article1.html";
+ }
+ return fromEditText.toString();
+ }
}
diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_main.xml
index bae2bae9..4b425e28 100644
--- a/example/src/main/res/layout/activity_main.xml
+++ b/example/src/main/res/layout/activity_main.xml
@@ -23,11 +23,20 @@
android:layout_centerHorizontal="true"
android:hint="Custom site id. Default if empty"/>
+
+
@@ -76,11 +85,20 @@
android:onClick="trackReset"
android:text="@string/button_reset_video" />
+
+
Track VideoPause VideoReset Video
+ Track Conversion
\ No newline at end of file
diff --git a/parsely/api/parsely.api b/parsely/api/parsely.api
index b7b47f19..25016f05 100644
--- a/parsely/api/parsely.api
+++ b/parsely/api/parsely.api
@@ -1,3 +1,15 @@
+public final class com/parsely/parselyandroid/ConversionType : java/lang/Enum {
+ public static final field CUSTOM Lcom/parsely/parselyandroid/ConversionType;
+ public static final field LEAD_CAPTURE Lcom/parsely/parselyandroid/ConversionType;
+ public static final field LINK_CLICK Lcom/parsely/parselyandroid/ConversionType;
+ public static final field NEWSLETTER_SIGNUP Lcom/parsely/parselyandroid/ConversionType;
+ public static final field PURCHASE Lcom/parsely/parselyandroid/ConversionType;
+ public static final field SUBSCRIPTION Lcom/parsely/parselyandroid/ConversionType;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
+ public static fun valueOf (Ljava/lang/String;)Lcom/parsely/parselyandroid/ConversionType;
+ public static fun values ()[Lcom/parsely/parselyandroid/ConversionType;
+}
+
public final class com/parsely/parselyandroid/ParselyAlreadyInitializedException : java/lang/Exception {
public fun ()V
}
@@ -21,6 +33,7 @@ public abstract interface class com/parsely/parselyandroid/ParselyTracker {
public static fun sharedInstance ()Lcom/parsely/parselyandroid/ParselyTracker;
public abstract fun startEngagement (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/parsely/parselyandroid/SiteIdSource;)V
public abstract fun stopEngagement ()V
+ public abstract fun trackConversion (Ljava/lang/String;Lcom/parsely/parselyandroid/ConversionType;Ljava/lang/String;Ljava/lang/String;Lcom/parsely/parselyandroid/ParselyMetadata;Ljava/util/Map;Lcom/parsely/parselyandroid/SiteIdSource;)V
public abstract fun trackPageview (Ljava/lang/String;Ljava/lang/String;Lcom/parsely/parselyandroid/ParselyMetadata;Ljava/util/Map;Lcom/parsely/parselyandroid/SiteIdSource;)V
public abstract fun trackPause ()V
public abstract fun trackPlay (Ljava/lang/String;Ljava/lang/String;Lcom/parsely/parselyandroid/ParselyVideoMetadata;Ljava/util/Map;Lcom/parsely/parselyandroid/SiteIdSource;)V
@@ -36,6 +49,7 @@ public final class com/parsely/parselyandroid/ParselyTracker$Companion {
public final class com/parsely/parselyandroid/ParselyTracker$DefaultImpls {
public static synthetic fun startEngagement$default (Lcom/parsely/parselyandroid/ParselyTracker;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/parsely/parselyandroid/SiteIdSource;ILjava/lang/Object;)V
+ public static synthetic fun trackConversion$default (Lcom/parsely/parselyandroid/ParselyTracker;Ljava/lang/String;Lcom/parsely/parselyandroid/ConversionType;Ljava/lang/String;Ljava/lang/String;Lcom/parsely/parselyandroid/ParselyMetadata;Ljava/util/Map;Lcom/parsely/parselyandroid/SiteIdSource;ILjava/lang/Object;)V
public static synthetic fun trackPageview$default (Lcom/parsely/parselyandroid/ParselyTracker;Ljava/lang/String;Ljava/lang/String;Lcom/parsely/parselyandroid/ParselyMetadata;Ljava/util/Map;Lcom/parsely/parselyandroid/SiteIdSource;ILjava/lang/Object;)V
public static synthetic fun trackPlay$default (Lcom/parsely/parselyandroid/ParselyTracker;Ljava/lang/String;Ljava/lang/String;Lcom/parsely/parselyandroid/ParselyVideoMetadata;Ljava/util/Map;Lcom/parsely/parselyandroid/SiteIdSource;ILjava/lang/Object;)V
}
diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ConversionType.kt b/parsely/src/main/java/com/parsely/parselyandroid/ConversionType.kt
new file mode 100644
index 00000000..fcfffab2
--- /dev/null
+++ b/parsely/src/main/java/com/parsely/parselyandroid/ConversionType.kt
@@ -0,0 +1,15 @@
+package com.parsely.parselyandroid
+
+/**
+ * The category of a conversion event. The [wireValue] of each case is the string sent to
+ * Parse.ly over the wire and must match the values accepted by the Parse.ly conversions
+ * backend. Use [CUSTOM] for conversions that don't fit one of the named categories.
+ */
+public enum class ConversionType(internal val wireValue: String) {
+ NEWSLETTER_SIGNUP("newsletter_signup"),
+ LEAD_CAPTURE("lead_capture"),
+ LINK_CLICK("link_click"),
+ SUBSCRIPTION("subscription"),
+ PURCHASE("purchase"),
+ CUSTOM("custom"),
+}
diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt
index b8155bcc..c75a7526 100644
--- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt
+++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt
@@ -135,6 +135,35 @@ public interface ParselyTracker {
*/
public fun resetVideo()
+ /**
+ * Track a conversion event (e.g. newsletter signup, subscription, purchase).
+ *
+ * The category and label are merged into the event's `extra_data` under the reserved keys
+ * `_conversion_type` and `_conversion_label`, which the Parse.ly conversions backend uses to
+ * identify the goal. Reserved keys cannot be overridden via [extraData].
+ *
+ * @param url The URL at which the conversion occurred.
+ * @param conversionType The category of conversion. Use [ConversionType.CUSTOM] for
+ * conversions that don't fit the named categories.
+ * @param conversionLabel A customer-defined identifier for this conversion (e.g.
+ * "weekly_plan", "homepage_cta"). Events without a label are dropped
+ * by the Parse.ly conversions backend.
+ * @param urlRef The url of the page that linked to the conversion page. Analogous to HTTP referer.
+ * @param urlMetadata Optional metadata for the URL.
+ * @param extraData A map of additional information to send with the event. Reserved keys
+ * `_conversion_type` and `_conversion_label` will be overwritten.
+ * @param siteIdSource The source of the site ID to use for the event.
+ */
+ public fun trackConversion(
+ url: String,
+ conversionType: ConversionType,
+ conversionLabel: String,
+ urlRef: String = "",
+ urlMetadata: ParselyMetadata? = null,
+ extraData: Map? = null,
+ siteIdSource: SiteIdSource = SiteIdSource.Default,
+ )
+
public companion object {
private const val DEFAULT_FLUSH_INTERVAL_SECS = 60
private var instance: ParselyTrackerInternal? = null
diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt
index 2a188550..c062d4cc 100644
--- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt
+++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt
@@ -206,6 +206,38 @@ internal class ParselyTrackerInternal internal constructor(
videoEngagementManager = null
}
+ override fun trackConversion(
+ url: String,
+ conversionType: ConversionType,
+ conversionLabel: String,
+ urlRef: String,
+ urlMetadata: ParselyMetadata?,
+ extraData: Map?,
+ siteIdSource: SiteIdSource,
+ ) {
+ if (url.isBlank()) {
+ Log.e("url cannot be empty")
+ return
+ }
+
+ val mergedExtraData = (extraData ?: emptyMap()) + mapOf(
+ "_conversion_type" to conversionType.wireValue,
+ "_conversion_label" to conversionLabel,
+ )
+
+ enqueueEvent(
+ eventsBuilder.buildEvent(
+ url,
+ urlRef,
+ "conversion",
+ urlMetadata,
+ mergedExtraData,
+ generatePixelId(),
+ siteIdSource
+ )
+ )
+ }
+
/**
* Add an event Map to the queue.
* Place a data structure representing the event into the in-memory queue for later use.
diff --git a/parsely/src/test/java/com/parsely/parselyandroid/EventsBuilderTest.kt b/parsely/src/test/java/com/parsely/parselyandroid/EventsBuilderTest.kt
index f5ee6deb..ebff1ec2 100644
--- a/parsely/src/test/java/com/parsely/parselyandroid/EventsBuilderTest.kt
+++ b/parsely/src/test/java/com/parsely/parselyandroid/EventsBuilderTest.kt
@@ -207,6 +207,43 @@ internal class EventsBuilderTest {
}
+ @Test
+ fun `when building conversion event, then build the correct one`() {
+ // given
+ val extraData: Map = mapOf(
+ "_conversion_type" to "subscription",
+ "_conversion_label" to "weekly_plan",
+ "plan" to "Active",
+ )
+
+ // when
+ val event: Map = sut.buildEvent(
+ TEST_URL,
+ "",
+ "conversion",
+ null,
+ extraData,
+ TEST_UUID,
+ SiteIdSource.Default,
+ )
+
+ // then
+ assertThat(event)
+ .doesNotContainKey("pvid")
+ .doesNotContainKey("vsid")
+ .containsEntry("action", "conversion")
+ .containsEntry("url", TEST_URL)
+ .containsEntry("idsite", TEST_SITE_ID)
+ .hasEntrySatisfying("data") {
+ @Suppress("UNCHECKED_CAST")
+ it as Map
+ assertThat(it)
+ .containsEntry("_conversion_type", "subscription")
+ .containsEntry("_conversion_label", "weekly_plan")
+ .containsEntry("plan", "Active")
+ }
+ }
+
@Test
fun `given custom site id is provided, when creating a pixel, then use the custom site id`() {
// given
diff --git a/parsely/src/test/java/com/parsely/parselyandroid/ParselyTrackerTest.kt b/parsely/src/test/java/com/parsely/parselyandroid/ParselyTrackerTest.kt
index e179d0e4..ef25c827 100644
--- a/parsely/src/test/java/com/parsely/parselyandroid/ParselyTrackerTest.kt
+++ b/parsely/src/test/java/com/parsely/parselyandroid/ParselyTrackerTest.kt
@@ -33,6 +33,17 @@ class ParselyTrackerTest {
ParselyTracker.sharedInstance().startEngagement("url")
}
+ @Test
+ fun `given tracker initialized, when calling trackConversion, do not throw any exception`() {
+ ParselyTracker.init(siteId = "example.com", context = RuntimeEnvironment.getApplication())
+
+ ParselyTracker.sharedInstance().trackConversion(
+ url = "https://example.com/path/test-conversion",
+ conversionType = ConversionType.SUBSCRIPTION,
+ conversionLabel = "weekly_plan",
+ )
+ }
+
@After
fun tearDown() {
ParselyTracker.tearDown()