Skip to content

Commit

Permalink
Checkpoint commit with a much better engagement timer working.
Browse files Browse the repository at this point in the history
  • Loading branch information
kbourgoin committed Feb 18, 2019
1 parent cbee3ee commit 3028bf5
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 44 deletions.
2 changes: 1 addition & 1 deletion COPYING
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2016 Parse.ly, Inc.
Copyright 2016-2019 Parse.ly, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
39 changes: 25 additions & 14 deletions ParselyExample/app/src/main/java/com/example/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);

// initialize the Parsely tracker with your API key and the current Context
ParselyTracker.sharedInstance("examplesite.com", 15, this);
ParselyTracker.sharedInstance("examplesite.com", 30, this);

// Set debugging to true so we don't actually send things to Parse.ly
ParselyTracker.sharedInstance().setDebug(true);
Expand All @@ -35,7 +35,7 @@ protected void onCreate(Bundle savedInstanceState) {
final TextView intervalView = (TextView)findViewById(R.id.interval);
storedView.setText(String.format("Flush interval: %d", ParselyTracker.sharedInstance().flushInterval));

updateEngagementString();
updateEngagementStrings();

final TextView views[] = new TextView[3];
views[0] = queueView;
Expand All @@ -59,7 +59,7 @@ public void handleMessage(Message msg) {
iView.setText("Flush timer inactive");
}

updateEngagementString();
updateEngagementStrings();
}
};

Expand All @@ -86,35 +86,46 @@ protected void onDestroy() {
super.onDestroy();
}

private void updateEngagementString() {
StringBuilder message = new StringBuilder("Engagement is ");
private void updateEngagementStrings() {
StringBuilder eMsg = new StringBuilder("Engagement is ");
if(ParselyTracker.sharedInstance().engagementIsActive() == true) {
message.append("active.");
eMsg.append("active.");
} else {
message.append("inactive.");
eMsg.append("inactive.");
}
message.append(String.format(" (interval: %.01fs)", ParselyTracker.sharedInstance().getEngagementInterval()));
eMsg.append(String.format(" (interval: %.01fs)", ParselyTracker.sharedInstance().getEngagementInterval()));

TextView view = findViewById(R.id.et_interval);
view.setText(message.toString());
TextView eView = findViewById(R.id.et_interval);
eView.setText(eMsg.toString());

StringBuilder vMsg = new StringBuilder("Video is ");
if(ParselyTracker.sharedInstance().videoIsActive() == true) {
vMsg.append("active.");
} else {
vMsg.append("inactive.");
}
vMsg.append(String.format(" (interval: %.01fs)", ParselyTracker.sharedInstance().getEngagementInterval()));

TextView vView = findViewById(R.id.video_interval);
vView.setText(vMsg.toString());
}

public void trackURL(View view) {
ParselyTracker.sharedInstance().trackURL("http://example.com/article1.html");
ParselyTracker.sharedInstance().trackURL("http://example.com/article1.html", null);
}

public void startEngagement(View view) {
ParselyTracker.sharedInstance().startEngagement("http://example.com/article1.html");
updateEngagementString();
updateEngagementStrings();
}

public void stopEngagement(View view) {
ParselyTracker.sharedInstance().stopEngagement();
updateEngagementString();
updateEngagementStrings();
}

public void trackPlay(View view) {
ParselyTracker.sharedInstance().trackPlay("http://example.com/article1", "video1");
ParselyTracker.sharedInstance().trackPlay("http://example.com/article1", null);
}

public void trackPause(View view) {
Expand Down
16 changes: 12 additions & 4 deletions ParselyExample/app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,29 +65,37 @@
android:layout_height="wrap_content"
android:layout_below="@+id/pause_video_button"
android:layout_centerHorizontal="true"
android:text="" />
android:text="Queued events: 0" />

<TextView android:id="@+id/stored_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/queue_size"
android:text=""/>
android:text="Stored events: 0"/>

<TextView android:id="@+id/interval"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/stored_size"
android:text=""/>
android:text="Flush timer inactive"/>

<TextView
android:id="@+id/et_interval"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/interval"
android:layout_centerHorizontal="true"
android:text="" />
android:text="Engagement is inactive." />

<TextView
android:id="@+id/video_interval"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/et_interval"
android:layout_centerHorizontal="true"
android:text="Video is inactive." />


</RelativeLayout>
150 changes: 125 additions & 25 deletions parsely/parselyandroid/ParselyTracker.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,20 @@
public class ParselyTracker {
private static ParselyTracker instance = null;
private static int DEFAULT_FLUSH_INTERVAL = 60;
private static double DEFAULT_ENGAGEMENT_INTERVAL = 10.5;
private static int DEFAULT_ENGAGEMENT_INTERVAL_MILLIS = 10500;
private static String DEFAULT_URLREF = "parsely_mobile_sdk";

private String apikey, rootUrl, storageKey, uuidkey, urlref, adKey, engagementUrl;
private boolean isDebug = false;
private SharedPreferences settings;
private int queueSizeLimit, storageSizeLimit;
public int flushInterval;
private double engagementInterval;
protected ArrayList<Map<String, Object>> eventQueue;
private Map<String, String> deviceInfo;
private Context context;
private Timer flushTimer, engagementTimer;
// TODO: Get rid of flushTimer
private Timer flushTimer, timer;
private EngagementManager engagementManager, videoEngagementManager;

protected ParselyTracker(String apikey, int flushInterval, String urlref, Context c){
this.context = c.getApplicationContext();
Expand All @@ -81,13 +82,13 @@ protected ParselyTracker(String apikey, int flushInterval, String urlref, Contex
// get the adkey straight away on instantiation
new GetAdKey(c).execute();
this.flushInterval = flushInterval;
this.engagementInterval = DEFAULT_ENGAGEMENT_INTERVAL;
this.storageKey = "parsely-events.ser";
this.rootUrl = "https://srv.pixel.parsely.com/";
this.urlref = urlref;
this.queueSizeLimit = 50;
this.storageSizeLimit = 100;
this.deviceInfo = this.collectDeviceInfo();
this.timer = new Timer();

this.eventQueue = new ArrayList<>();

Expand All @@ -97,13 +98,18 @@ protected ParselyTracker(String apikey, int flushInterval, String urlref, Contex
}

public double getEngagementInterval() {
return this.engagementInterval;
return DEFAULT_ENGAGEMENT_INTERVAL_MILLIS;
}

public boolean engagementIsActive() {
return this.timerIsActive(this.engagementTimer);
return this.engagementManager != null;
}

public boolean videoIsActive() {
return this.videoEngagementManager != null;
}


/*! \brief Getter for this.isDebug
*/
public boolean getDebug() {
Expand All @@ -128,32 +134,54 @@ public void setDebug(boolean debug) {
* @param url The canonical URL of the article being tracked
* (eg: "http://samplesite.com/some-old/article.html")
*/
public void trackURL(String url){
public void trackURL(String url, ParselyMetadata urlMetadata){
this.enqueueEvent(this.buildEvent(url, "pageview"));
}

public void startEngagement(String url) {
PLog("startEngagement called");
final String targetUrl = url;
TimerTask task = new TimerTask(){
public void run(){
Map <String, Object> event = buildEvent(targetUrl, "heartbeat");
event.put("inc", String.format("%.0f", engagementInterval));
enqueueEvent(event);
}
};
this.engagementTimer = setTimer(this.engagementTimer, task, (int) (this.engagementInterval * 1000));

// Cancel anything running
this.stopEngagement();

// Start a new EngagementTask
this.engagementManager = new EngagementManager(this.timer, DEFAULT_ENGAGEMENT_INTERVAL_MILLIS, "heartbeat", url);
this.engagementManager.start();
}

public void stopEngagement() {
if(this.engagementManager == null) {
PLog("No ongoing engagement to stop.");
return;
}
PLog("stopEngagement called");
this.stopTimer(this.engagementTimer);
this.engagementTimer = null;
this.engagementManager.cancel();
this.engagementManager = null;
}

public void trackPlay(String url, ParselyVideoMetadata videoMetadata) {
PLog("trackPlay called");

this.enqueueEvent(this.buildEvent(url, "videostart"));

// Cancel anything running
this.trackPause();

// Start a new EngagementTask
this.videoEngagementManager = new EngagementManager(this.timer, DEFAULT_ENGAGEMENT_INTERVAL_MILLIS, "vheartbeat", url);
this.videoEngagementManager.start();
}

public void trackPlay(String url, String vId) { PLog("trackPlay called"); }
public void trackPause() {
if(this.videoEngagementManager == null) {
PLog("No ongoing video to stop.");
return;
}
PLog("trackPause called");
this.videoEngagementManager.cancel();
this.videoEngagementManager = null;
}

public void trackPause() { PLog("trackPause called"); }


/*! \brief Create an event Map
Expand All @@ -162,7 +190,7 @@ public void stopEngagement() {
* @param action Action kind to use (e.g. pageview, heartbeat)
*/
private Map<String, Object> buildEvent(String url, String action) {
PLog("Track called for %s", url);
PLog("buildEvent called for %s", url);

Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
double timestamp = calendar.getTimeInMillis() / 1000.0;
Expand Down Expand Up @@ -206,10 +234,6 @@ private void enqueueEvent(Map<String, Object> event){
}
}

private void track(String engagementUrl, String heartbeat, double engagementInterval) {
PLog("Tracking heartbeat");
}

/*! \brief Generate pixel requests from the queue
*
* Empties the entire queue and sends the appropriate pixel requests.
Expand Down Expand Up @@ -582,4 +606,80 @@ protected void onPostExecute(String advertId) {
}

};


/*! \brief Engagement manager for article and video engagement.
*
* Implemented to handle its own queuing of future executions to accomplish
* two things:
*
* 1. Flushing any engaged time before canceling.
* 2. Progressive backoff for long engagements to save data.
*/
private class EngagementManager {

private String action, url;
private Timer parentTimer;
private TimerTask waitingTimerTask;
private long latestDelayMillis, totalTime;


public EngagementManager(Timer parentTimer, int intervalMillis, String action, String url) {
this.parentTimer = parentTimer;
this.action = action;
this.url = url;
this.latestDelayMillis = intervalMillis;
this.totalTime = 0;
}

public void start() {
this.scheduleNextExecution(this.latestDelayMillis);
}

public boolean cancel() {
return this.waitingTimerTask.cancel();
}

private void scheduleNextExecution(long delay) {
TimerTask task = new TimerTask(){
public void run(){
doEnqueue(this.scheduledExecutionTime());
updateLatestInterval();
scheduleNextExecution(latestDelayMillis);
}

public boolean cancel() {
doEnqueue(this.scheduledExecutionTime());
return super.cancel();
}
};
PLog(String.format("latestDelayMillis: %d", delay));
this.latestDelayMillis = delay;
this.parentTimer.schedule(task, delay);
this.waitingTimerTask = task;
}

private void doEnqueue(long scheduledExecutionTime) {
PLog(String.format("Enqueuing %s event.", this.action));
Map <String, Object> event = buildEvent(this.url, this.action);

// Adjust inc by execution time in case we're late or early.
long executionDiff = (System.currentTimeMillis() - scheduledExecutionTime);
long inc = (this.latestDelayMillis + executionDiff) / 1000;
this.totalTime += inc;
event.put("inc", inc);
event.put("tt", this.totalTime);

enqueueEvent(event);
}

private void updateLatestInterval() {
// Update latestDelayMillis to be used for next execution. The interval
// increases by 25% for each successive call, up to a max of 90s, to cut down on
// data use for very long engagements (e.g. streaming video).
this.latestDelayMillis = (int) Math.min(90000, this.latestDelayMillis * 1.25);
}


}
}

0 comments on commit 3028bf5

Please sign in to comment.