2

I currently developing a third party widget for online publishers and would like to track its performance on different websites. I thought of implementing Upworthy's code (http://upworthy.github.io/2014/06/implementing-attention-minutes-part-1/) for calculating average time spent per user on my widget only. So I decided to implement it in 3 steps:

  1. Start my "focus" event when my widget is visible in the viewport only (my widget is generally embedded at the bottom of the article)
  2. Next, as shown in Upworthy's method, I will merge my customised "focus" event with blur event to develop an isFocused event.
  3. Then, I can implement their recentlyActive method to find out if users are clicking on widget or not i.e if they are interacting with my widget.

I have implemented the above and source code can be found here: http://jsfiddle.net/q21gzjmf/13/

//simple jquery function to detect when my widget is visible   
  function widgetFocus(){ 
  $(window).scroll(function () {

  if ($(window).scrollTop() + $(window).height() > $('.footer').offset().top) {
    alert('visible');
    return true;
  }
  else{
    return false;
  }
  });
     }

function merge(stream1, stream2) {
return stream1.merge(stream2);
  }

function eventStream(eventName) {
return $(window).asEventStream(eventName);
 }

  var isFocused = eventStream("focus").map(widgetFocus)
  .merge(eventStream("blur").map(false))
  .toProperty(true);

  var EVENT_NAMES = ["focus", "click", "scroll", "mousemove", "touchstart", "touchend", "touchcancel", "touchleave", "touchmove"];
  var streams = _.map(EVENT_NAMES, eventStream);
  var interactionStream = _.reduce(streams, merge);


  var recentlyActive = interactionStream
  .map(true)
  .flatMapLatest(function() {
    return Bacon.once(true).merge(Bacon.once(false).delay(DECAY));
  })
  .toProperty(false);

  var isActive = (recentlyActive.and(isFocused));

  var secondsActive = Bacon.mergeAll(isActive.changes(), isActive.sample(1000))
.map(function(isActive) {
  return {
    isActive: isActive,
    timestamp: new Date().getTime()
  };
})
.slidingWindow(2,2)
.filter(function(span) { return span[0].isActive; })
.map(function(span) { return span[1].timestamp - span[0].timestamp; })
.scan(0, function(x,y) { return x + y;})
.map(function(x) { return x / 1000; }) // milliseconds
.map(Math.floor);

secondsActive.assign($("#seconds"), "text");

However, if you scroll down at the bottom you will find that time spent is calculated as 0 and doesn't update itself dynamically unlike Upworthy's implementation shown here http://jsfiddle.net/zanes/mbGBr/. I'm very new to the concept of Functional Reactive programming and still trying to get my head around Bacon.JS so I'm sure I must have made a very silly, conceptual mistake but I'm here to learn. Any help will be greatly appreciated.

ankits
  • 305
  • 1
  • 3
  • 13

1 Answers1

2

To repeat the steps Start focus events when widget is visible:

For that we need a predicate which returns whether widget is visible:

function widgetFocus(){ 
  return $(window).scrollTop() + $(window).height() > $('.footer').offset().top;
}

Then we proceed with all scroll related events, as you did:

function eventStream(eventName) {
  return $(window).asEventStream(eventName);
}

var EVENT_NAMES = ["focus", "click", "scroll", "mousemove", "touchstart", "touchend", "touchcancel", "touchleave", "touchmove"];
var streams = _.map(EVENT_NAMES, eventStream);
var scrollEvents = Bacon.mergeAll(streams)
  .debounceImmediate(50); // we debounce here, so we didn't get too much events

Next you could map scrollEvents to get a stream of booleans which indicate whether the widget is shown.

var isVisible$ = scrollEvents
  .map(function () {
    return widgetFocus();
  })
  .toProperty(false);

var secondsVisible = isVisible$.changes()
  .map(function(isActive) {
    return {
      isActive: isActive,
      timestamp: new Date().getTime()
    };
  })
  .slidingWindow(2,2)
  .filter(function(span) { return span[0].isActive; })
  .map(function(span) { return span[1].timestamp - span[0].timestamp; })
  .scan(0, function(x,y) { return x + y;})
  .map(function(x) { return Math.floor(x / 1000); }); // milliseconds

secondsVisible.log("visible: ");

This is the first step and will probably work for you already.

Next step would be to create a property or stream with whether the page is active (simple version of Upworthy):

var DECAY = 1000;

function decayingStream() {
  return Bacon.once(true).merge(Bacon.once(false).delay(DECAY));
}

var isActive$ = scrollEvents
  .flatMapLatest(decayingStream)
  .toProperty(true);

Next we can combine isActive$ and isVisible$ properties. We are interested only in the time widget is visible and page is active!

var secondsVisibleAndActive = isActive$.and(isVisible$).changes()
  .map(function(isActive) {
    return {
      isActive: isActive,
      timestamp: new Date().getTime()
    };
  })
  .slidingWindow(2,2)
  .filter(function(span) { return span[0].isActive; })
  .map(function(span) { return span[1].timestamp - span[0].timestamp; })
  .scan(0, function(x,y) { return x + y;})
  .map(function(x) { return Math.floor(x / 1000); }); // milliseconds

secondsVisibleAndActive.log("active & visible: ");
secondsVisibleAndActive.assign($("#seconds"), "text");

As always, start simple.


Alternatively you can example the Upworthy's implementation's beauty: the last line:

var hasAttention = (recentlyActive.and(isFocused)).or(videoIsPlaying);

In your case, you could alter it to be:

var widgetHasAttention = recentlyActive.and(isFocused).and(widgetIsVisible);

Working jsfiddle: http://jsfiddle.net/xkn3sc6y/3/

phadej
  • 11,947
  • 41
  • 78
  • Awesome and thanks for explaining it in detail.I must admit that I should have thought of Upworthy's implementation. – ankits Dec 24 '14 at 21:15