Josh Josh - 3 months ago 8
HTML Question

Is it possible to detect the number of users currently interacting a section of html code?

This may be a naive question, sorry for that, but I'm trying to sort out a potential concurrency issue. I have a registration procedure which begins with the user selecting their category from a drop down menu. That triggers a query to a particular page in a google sheet where it retrieves an available ID number that is displayed to the user. There are a couple steps required before the final submit button is pressed. This (I think) creates a chance for more than one person to retrieve the same ID. I do use google's lockservice but on the function which writes the form information to my spreadsheet (based on a script of Martin Hawksley). If it was possible to determine how many people were currently viewing the registration page I could use that value in the query with an if statement such that a different row number is retrieved. This would eliminate the chance of duplicates.

Does that sound reasonable? Perhaps there is a much better way.

Any advice would be most appreciated.

Answer

If it was possible to determine how many people were currently viewing the registration page

If you don't want to use Google Analytics. Here is a simple example of how you can poll with a client to maintain a session, and count how many users are active.

NOTE: I threw this together, it could be refactored a bit to be prettier, but it should get the gist of it across

Working Example Open this a couple times, you will see your session IDs in each and a count. Sessions expire after 60 seconds of no activity, and auto-end if you close the page.

Project file structure:

  • Code.gs
  • index.html
  • javascript.html

Apps Script

var sessionTimeout = 60; //60 seconds

//Triggered when the page is navigated to, serves up HTML
function doGet(){
  var template = HtmlService.createTemplateFromFile('index');
  template.userID = NewSession();
  return template.evaluate()
      .setTitle('Active Users')
      .setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

/*================ Polling ================*/

//Client calls this function to poll, updates cache, returns active users
function ClientPoll(id){
  var scriptCache = CacheService.getScriptCache();
  UpdateScriptCache(id, 0, scriptCache, true);

  var activeIDs = GetActiveIDs(scriptCache);
  return activeIDs;
}

function EndSession(id){
  var scriptCache = CacheService.getScriptCache();
  scriptCache.remove(id);
  var activeIDs = GetActiveIDs(scriptCache);
  activeIDs.splice(activeIDs.indexOf(id), 1);
  UpdateScriptCache('ActiveIDs', activeIDs, scriptCache, false);
}

//Server calls every minute to check for inactive sessions
function CheckForInactiveSessions(){
  var scriptCache = CacheService.getScriptCache();
  var activeIDs = GetActiveIDs(scriptCache);
  var allSessions = scriptCache.getAll(activeIDs);

  if(Object.keys(allSessions).length > 0){
    var keys = Object.keys(allSessions);
    var newActiveIDs = [];

    for(var i = 0; i < keys.length; i++){
      newActiveIDs.push(keys[i]);

    }
    Logger.log(keys.length);
    UpdateScriptCache('ActiveIDs', newActiveIDs, scriptCache, false);
  }  
}

/*================ Session Creation & Cache ================*/

//Handles setting up a new session, called when page is opened
function NewSession(){
  var id = GenerateUID();
  AddNewActiveID(id);
  return id;
}

//Gets a list of all active IDs
function GetActiveIDs(scriptCache){

  if(!scriptCache){
    scriptCache = CacheService.getScriptCache();
  }

  var active = scriptCache.get('ActiveIDs');

  if(active !== null){
    return JSON.parse(active);
  }
  return [];
}

//Adds a new ID to the cache
function AddNewActiveID(id){
  var scriptCache = CacheService.getScriptCache();
  var activeIDs = JSON.parse(scriptCache.get('ActiveIDs'));
  if(activeIDs == null){
    activeIDs = [];
  }
  activeIDs.push(id);

  //Update the Active ID List
  UpdateScriptCache('ActiveIDs', activeIDs, scriptCache, false);

  //Add new ID to cache
  UpdateScriptCache(id, 0, scriptCache, true);
}

//Handles updating the Active IDs cache and prevents race conditions or collisions
function UpdateScriptCache(key, data, cache, timeout){
  var lock = LockService.getScriptLock();
  lock.waitLock(15000);
  if(timeout){
    cache.put(key, JSON.stringify(data), sessionTimeout);
  } else {
    cache.put(key, JSON.stringify(data), 21600)
  }


  lock.releaseLock();
}

/*================ ID Generation ================*/

//Handles generating and returning a new ID
function GenerateUID(){
  var generator = new IDGenerator();
  var id = generator.generate();
  return id;  
}

//Generates a random(ish) ID;
function IDGenerator() {

  this.length = 10;
  this.timestamp = new Date().getTime();

  var getRandomInt = function( min, max ) {
    return Math.floor( Math.random() * ( max - min + 1 ) ) + min;
  }

  this.generate = function(){
    var timestamp = this.timestamp.toString();
    var parts = timestamp.split('').reverse();
    var id = '';

    for(var i = 0; i < this.length; i++){
      var index = getRandomInt(0, parts.length - 1);
      id += parts[index];
    }

    return id;
  }

}

JavaScript:

<script>

//Initilization
$(function(){

  //Set the users ID in HTML
  $('#userID').text(userID);

  //Setup handler to end the session before the page closes
  $(window).bind('beforeunload', function(){
     EndSession();
  });  

  //Start the timer
  var deadline = new Date(Date.parse(new Date()) + 5000);
  initializeTimer('pollingIn', deadline);  
});



//Polls the server to update session and get active users
function PollServer(){
  console.log('Polling server');
  google.script.run.withSuccessHandler(UpdateActiveUsers).ClientPoll(userID);
  var deadline = new Date(Date.parse(new Date()) + 5000);
  initializeTimer('pollingIn', deadline);  
}

//Ends the session right before the page closes
function EndSession(){
  google.script.run.withSuccessHandler().EndSession(userID);
}

//Updates the active users div
function UpdateActiveUsers(users){
  console.log(users)

  var userText = '';
  for(var i = 0; i < users.length; i++){
    if(i == 0){
      userText += users[i];
      continue;
    }  
    userText += ', ' + users[i];
  }

  $('#activeUsersCount').text(users.length);
  $('#activeUsers').text(userText);
}

//Initilizes the timer
function initializeTimer(id, endtime) {
  var timer = $('#'+id);

  function updateTimer() {
    var time = Date.parse(endtime) - Date.parse(new Date());
    var seconds = Math.floor((time / 1000) % 60);
    timer.text(seconds);

    if (time <= 0) {
      clearInterval(timeInterval);
      PollServer();
    }
  }

  updateTimer();
  var timeInterval = setInterval(updateTimer, 1000);
}
</script>

HTML:

<!DOCTYPE html>
<html>

    <head>
        <base target="_top">
        <link href="https://ssl.gstatic.com/docs/script/css/add-ons1.css" rel="stylesheet">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js" type="text/javascript"></script>
    </head>

    <body>
        <div id="mainForm">
            <h1>Active Users</h1>
            <div class="box">
                Active Users Count:
                <span id="activeUsersCount">0</span>
            </div>            
            <div class="box">
                Active Users:
                <span id="activeUsers"></span>
            </div>
            <div class="box">
                Polling in:
                <span id="pollingIn"></span>
            </div>
            <div class="box">
                You Are:
                <span id="userID"></span>
            </div>            
        </div>
        <?!= HtmlService.createHtmlOutputFromFile('javascript').getContent(); ?>
        <script>
          var userID = <?= userID ?>;
        </script>
    </body>

</html>

<style>
    .box {
        display: block;
        padding: 0.5em;
    }

    body {
        padding: 1em;
    }
</style>
Comments