Skip to content

Commit

Permalink
Merge pull request #105 from Parsely/improve_singleton_handling
Browse files Browse the repository at this point in the history
Improve how `ParselyTracker` is accessed
  • Loading branch information
wzieba authored Jan 29, 2024
2 parents 704f372 + c45ef7b commit 222b14b
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 136 deletions.
6 changes: 3 additions & 3 deletions example/src/main/java/com/example/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);

// initialize the Parsely tracker with your site id and the current Context
ParselyTracker.sharedInstance("example.com", 30, this, true);
ParselyTracker.init("example.com", 30, this, true);

final TextView intervalView = (TextView) findViewById(R.id.interval);

Expand Down Expand Up @@ -62,7 +62,7 @@ public void run() {

private void updateEngagementStrings() {
StringBuilder eMsg = new StringBuilder("Engagement is ");
if (ParselyTracker.sharedInstance().engagementIsActive() == true) {
if (ParselyTracker.sharedInstance().engagementIsActive()) {
eMsg.append("active.");
} else {
eMsg.append("inactive.");
Expand All @@ -73,7 +73,7 @@ private void updateEngagementStrings() {
eView.setText(eMsg.toString());

StringBuilder vMsg = new StringBuilder("Video is ");
if (ParselyTracker.sharedInstance().videoIsActive() == true) {
if (ParselyTracker.sharedInstance().videoIsActive()) {
vMsg.append("active.");
} else {
vMsg.append("inactive.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class FunctionalTests {
scenario.onActivity { activity: Activity ->
beforeEach(activity)
server.enqueue(MockResponse().setResponseCode(200))
parselyTracker = initializeTracker(activity)
initializeTracker(activity)

repeat(51) {
parselyTracker.trackPageview("url")
Expand Down Expand Up @@ -91,7 +91,7 @@ class FunctionalTests {
scenario.onActivity { activity: Activity ->
beforeEach(activity)
server.enqueue(MockResponse().setResponseCode(200))
parselyTracker = initializeTracker(activity)
initializeTracker(activity)

parselyTracker.trackPageview("url")
}
Expand Down Expand Up @@ -135,7 +135,7 @@ class FunctionalTests {
beforeEach(activity)
server.enqueue(MockResponse().setResponseCode(200))
server.enqueue(MockResponse().setResponseCode(200))
parselyTracker = initializeTracker(activity, flushInterval = 1.hours)
initializeTracker(activity, flushInterval = 1.hours)

repeat(20) {
parselyTracker.trackPageview("url")
Expand Down Expand Up @@ -169,7 +169,7 @@ class FunctionalTests {
scenario.onActivity { activity: Activity ->
beforeEach(activity)
server.enqueue(MockResponse().setResponseCode(200))
parselyTracker = initializeTracker(activity)
initializeTracker(activity)

repeat(eventsToSend) {
parselyTracker.trackPageview("url")
Expand Down Expand Up @@ -217,7 +217,7 @@ class FunctionalTests {
scenario.onActivity { activity: Activity ->
beforeEach(activity)
server.enqueue(MockResponse().setResponseCode(200))
parselyTracker = initializeTracker(activity, flushInterval = 30.seconds)
initializeTracker(activity, flushInterval = 30.seconds)

// when
startTimestamp = System.currentTimeMillis().milliseconds
Expand Down Expand Up @@ -314,13 +314,14 @@ class FunctionalTests {
private fun initializeTracker(
activity: Activity,
flushInterval: Duration = defaultFlushInterval
): ParselyTracker {
) {
val field: Field = ParselyTrackerInternal::class.java.getDeclaredField("ROOT_URL")
field.isAccessible = true
field.set(this, url)
return ParselyTracker.sharedInstance(
ParselyTracker.init(
siteId, flushInterval.inWholeSeconds.toInt(), activity.application
)
parselyTracker = ParselyTracker.sharedInstance()
}

private companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.parsely.parselyandroid

public class ParselyAlreadyInitializedException() :
Exception("Parse.ly SDK has been already initialized. Reinitialization is not supported.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.parsely.parselyandroid

public class ParselyNotInitializedException() :
Exception("Parse.ly client has not been initialized. Call ParselyTracker#init before using the SDK.")
141 changes: 125 additions & 16 deletions parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,60 +16,162 @@
package com.parsely.parselyandroid

import android.content.Context
import org.jetbrains.annotations.TestOnly

/**
* Tracks Parse.ly app views in Android apps
*
*
* Accessed as a singleton. Maintains a queue of pageview events in memory and periodically
* flushes the queue to the Parse.ly pixel proxy server.
*/
public interface ParselyTracker {

/**
* Get the heartbeat interval
*
* @return The base engagement tracking interval.
*/
public val engagementInterval: Double?

public val videoEngagementInterval: Double?

/**
* Returns the interval at which the event queue is flushed to Parse.ly.
*
* @return The interval at which the event queue is flushed to Parse.ly.
*/
public val flushInterval: Long

/**
* Returns whether the engagement tracker is running.
*
* @return Whether the engagement tracker is running.
*/
public fun engagementIsActive(): Boolean

/**
* Returns whether video tracking is active.
*
* @return Whether video tracking is active.
*/
public fun videoIsActive(): Boolean

/**
* Register a pageview event using a URL and optional metadata.
*
* @param url The URL of the article being tracked
* (eg: "http://example.com/some-old/article.html")
* @param urlRef Referrer URL associated with this video view.
* @param urlMetadata Optional metadata for the URL -- not used in most cases. Only needed
* when `url` isn't accessible over the Internet (i.e. app-only
* content). Do not use this for **content also hosted on** URLs Parse.ly
* would normally crawl.
* @param extraData A Map of additional information to send with the event.
*/
public fun trackPageview(
url: String,
urlRef: String = "",
urlMetadata: ParselyMetadata? = null,
extraData: Map<String, Any>? = null,
)

/**
* Start engaged time tracking for the given URL.
*
*
* This starts a timer which will send events to Parse.ly on a regular basis
* to capture engaged time for this URL. The value of `url` should be a URL for
* which `trackPageview` has been called.
*
* @param url The URL to track engaged time for.
* @param urlRef Referrer URL associated with this video view.
*/
public fun startEngagement(
url: String,
urlRef: String = "",
extraData: Map<String, Any>? = null
)

/**
* Stop engaged time tracking.
*
*
* Stops the engaged time tracker, sending any accumulated engaged time to Parse.ly.
* NOTE: This **must** be called in your `MainActivity` during various Android lifecycle events
* like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background
* and Parse.ly values may be inaccurate.
*/
public fun stopEngagement()

/**
* Start video tracking.
*
*
* Starts tracking view time for a video being viewed at a given url. Will send a `videostart`
* event unless the same url/videoId had previously been paused.
* Video metadata must be provided, specifically the video ID and video duration.
*
*
* The `url` value is *not* the URL of a video, but the post which contains the video. If the video
* is not embedded in a post, then this should contain a well-formatted URL on the customer's
* domain (e.g. http://<CUSTOMERDOMAIN>/app-videos). This URL doesn't need to return a 200 status
* when crawled, but must but well-formatted so Parse.ly systems recognize it as belonging to
* the customer.
*
* @param url URL of post the video is embedded in. If videos is not embedded, a
* valid URL for the customer should still be provided.
* (e.g. http://<CUSTOMERDOMAIN>/app-videos)
* @param urlRef Referrer URL associated with this video view.
* @param videoMetadata Metadata about the video being tracked.
* @param extraData A Map of additional information to send with the event.
</CUSTOMERDOMAIN></CUSTOMERDOMAIN> */
public fun trackPlay(
url: String,
urlRef: String = "",
videoMetadata: ParselyVideoMetadata,
extraData: Map<String, Any>? = null,
)

/**
* Pause video tracking.
*
*
* Pauses video tracking for an ongoing video. If [.trackPlay] is immediately called again for
* the same video, a new video start event will not be sent. This models a user pausing a
* playing video.
*
*
* NOTE: This or [.resetVideo] **must** be called in your `MainActivity` during various Android lifecycle events
* like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background
* and Parse.ly values may be inaccurate.
*/
public fun trackPause()

/**
* Reset tracking on a video.
*
*
* Stops video tracking and resets internal state for the video. If [.trackPlay] is immediately
* called for the same video, a new video start event is set. This models a user stopping a
* video and (on [.trackPlay] being called again) starting it over.
*
*
* NOTE: This or [.trackPause] **must** be called in your `MainActivity` during various Android lifecycle events
* like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background
* and Parse.ly values may be inaccurate.
*/
public fun resetVideo()
public fun flushEventQueue()

public fun flushTimerIsActive(): Boolean

public companion object {
private const val DEFAULT_FLUSH_INTERVAL_SECS = 60
private var instance: ParselyTrackerInternal? = null

/**
* Singleton instance accessor. Note: This must be called after [.sharedInstance]
*
* @return The singleton instance
*/
@JvmStatic
public fun sharedInstance(): ParselyTracker? {
return instance
private fun ensureInitialized(): ParselyTracker {
return instance ?: run {
throw ParselyNotInitializedException()
}
}

/**
Expand All @@ -83,17 +185,24 @@ public interface ParselyTracker {
*/
@JvmStatic
@JvmOverloads
public fun sharedInstance(
public fun init(
siteId: String,
flushInterval: Int = DEFAULT_FLUSH_INTERVAL_SECS,
context: Context,
dryRun: Boolean = false,
): ParselyTracker {
return instance ?: run {
val newInstance = ParselyTrackerInternal(siteId, flushInterval, context, dryRun)
instance = newInstance
return newInstance
) {
if (instance != null) {
throw ParselyAlreadyInitializedException()
}
instance = ParselyTrackerInternal(siteId, flushInterval, context, dryRun)
}

@JvmStatic
public fun sharedInstance(): ParselyTracker = ensureInitialized()

@TestOnly
internal fun tearDown() {
instance = null
}
}
}
Loading

0 comments on commit 222b14b

Please sign in to comment.