Search code examples
javascriptruby-on-railsajaxevent-tracking

Tracking "video duration watched" analytics from JavaScript


I want to track video views for each user, with a table in the database called Viewings. It belongs to both a user and a video, and records the duration watched, and percentage of the video completed.

When the play button is clicked, I want to either find an existing Viewing based on the id, which is stored in a 30-minute persisting cookie (which is created when the play button is first clicked, if a cookie doesn't already exist). I then want to periodically update the length attribute of this Viewing for every 10 seconds the user has watched the video, using a timeupdate event listener.

I have a JavaScript function that returns lengthWatched as an integer, which represents how many seconds in the video currently is.

I've tested to make sure this works with the following function:

video.addEventListener 'timeupdate', ((e) ->
  console.log(lengthWatched(e));
  return
  ), false

So now all I have to do is update length whenever this function returns an integer divisible by 10.

I'm quite sure I'll have to use AJAX here. But I'm not too sure of the best way to proceed onto this next step, or maybe even if there's a better way entirely to record this data.

Any suggestions?

Edit:

Using the suggestions from agmcleod and some of my own, I ended up with these working JS additions:

#global variables
videoId = location['pathname'].split('/').pop()
viewingCookieName = "Video "+videoId.toString()

#taken from http://www.quirksmode.org/js/cookies.html for easy reading of cookies
readCookie = (name) ->
  nameEQ = name + '='
  ca = document.cookie.split(';')
  i = 0
  while i < ca.length
    c = ca[i]
    while c.charAt(0) == ' '
      c = c.substring(1, c.length)
    if c.indexOf(nameEQ) == 0
      return c.substring(nameEQ.length, c.length)
    i++
  null

video.addEventListener 'timeupdate', ((e) ->
  duration = lengthWatched(e)
  if (duration % 5 == 0)
    currentLength = 0
    $.get "/viewings/" + readCookie(viewingCookieName), (data) ->
      currentLength = data['length']
      return
    $.ajax(
      url: "/viewings/" + readCookie(viewingCookieName) + ".json"
      type: 'PUT'
      data: { viewing: {length: currentLength + 5}}
      dataType: 'json').success ->
        console.log('put')
  return
  ), false

#only including relevant code, took out other video play functionality
video.onplay = (e) ->
  unless readCookie(viewingCookieName)
    $.ajax(
      url: "/viewings.json"
      type: 'POST'
      data: { viewing: { video_id: videoId, user_id: gon.current_user_id } },
      dataType: 'json').success (data) ->
        viewingId = data['id']
        time = new Date(Date.now() + 1800000).toUTCString()
        document.cookie = viewingCookieName+"="+ data['id']+"; expires="+time
  return

Used basically the same controller provided by agmcleod. I'm still working on fixing timeupdate to only hit every 1 second, but I'm sure that'll be fairly simple. May also add max_duration_reached.


Solution

  • Ajax is precisely what you need. Given you're using rails, I presume you have the jquery kicking around. You can do something like this:

    video.addEventListener 'timeupdate', ((e) ->
      duration = lengthWatched(e);
      console.log(duration);
      if (duration % 10 === 0)
        $.ajax({
          url: "/viewings/" + viewingsId + ".json",
          type: "PUT",
          data: { viewing: { length: duration } },
          dataType: "json"
        }).done(() -> {
          // callback
        });
      return
      ), false
    

    Sorry if my coffeescript is off, it's been a while. Anyways that posts a json body of the length property to a controller end point using PUT (since we're updating a record). Be sure to populate the viewingsId from your cookie.

    In the routes side, you can have a standard resource here:

     resources :viewings
    

    Then in the controller:

    class ViewingsController < ApplicationController
      def update
        viewing = Viewing.find(params[:id])
        if viewing.update_attributes viewing_attributes
          respond_to do |format|
            format.html
            format.json { render json: viewing }
          end
        else
          respond_to do |format|
            format.html { redirect_to viewing }
            format.json { render errors: viewing.errors, status: 422 }
          end
        end
      end
    
    private
    
      def viewing_attributes
        params.require(:viewing).permit(:length)
      end
    
    end
    

    The strong parameters bit is optional, just trying to keep with good practices. Update the permit on any other parameters you might need. I wrote the update action so it can work with html requests as well (form updates). Hope that helps you.