8

Question:

How do you normalize client-side timestamps in javascript for users whose internal computer clocks are off? Note that I am dealing with time in UTC.

Context:

I have an AWS ElasticSearch instance set up with several batched and throttled operations along the way, making server-side timestamps unreliable (since the data could come in out-of-order, and the order matters). I therefore need to make my client-side timestamps more reliable.

Constraints:

I can't make any server-side requests (need to keep HTTP requests to a minimum), but I can include a server-side timestamp generated when my javascript is first loaded on the client.


Attempted solution:

Externally-defined variables:

  • serverTimestamp - a UTC timestamp (in milliseconds), generated server-side when the javascript is loaded.
  • getCookie - a function that gets a cookie value for a given key (or an empty string if not found).

Cache-control settings for the file are "public,max-age=300,must-revalidate" (so 5 minutes).

const getTimestamp = (function() {
    // This cookie is set on the `unload` event, and so should be greater than
    // the server-side timestamp when set.
    /** @type {!number} */
    const cookieTimestamp = parseInt(getCookie("timestamp_cookie"), 10) || 0;
    // This timestamp _should_ be a maximum of 5 minutes behind on page load
    // (cache lasts 5 min for this file).
    /** @type {!number} */
    const storedTimestamp = cookieTimestamp > serverTimestamp ?
                            cookieTimestamp : serverTimestamp;
    return function () {
        /** @type {!number} */
        const timestamp = Date.now();
        // This timestamp should be, at a *maximum*, 5-6 minutes behind
        // (assuming the user doesn't have caching issues)
        /** @type {!number} */
        const backupTimestamp = storedTimestamp
            + parseFloat(window.performance.now().toFixed(0));
        // Now let's check to see if the user's clock is
        // either too fast, or too slow:
        if (
            // Timestamp is smaller than the stored one.
            // This means the user's clock is too slow.
            timestamp < backupTimestamp
            // Timestamp is more than 6 minutes ahead. User's clock is too fast.
            // (Using 6 minutes instead of 5 to have 1 minute of padding)
            || (timestamp - backupTimestamp) > 360000
        ) {
            return backupTimestamp;
        } else {
            // Seems like the user's clock isn't too fast or too slow
            // (or just maximum 1 minute fast)
            return timestamp;
        }
    }
})();

Problem with solution:

Using the above getTimestamp function, running (new Date(getTimestamp())).getUTCDate() is returning the next day for some users, and getUTCHours seems to be all over the place in edge cases. I'm unable to diagnose the problem on my own.

jperezov
  • 3,041
  • 1
  • 20
  • 36
  • 1
    Don't timestamps mark the date from GMT, then you can translate to your local TZ if desired? – symlink Dec 31 '19 at 00:17
  • I'm actually trying to keep everything in GMT / UTC. I don't want this to be specific to any timezone--I'm trying to normalize the time across weird timezones / browsers. – jperezov Dec 31 '19 at 00:18
  • Use Date.toUTCString() instead of Date.toString(): https://stackoverflow.com/questions/17545708/parse-date-without-timezone-javascript – symlink Dec 31 '19 at 00:22
  • I already use `getUTCDate` to get the value I want (see: "Problem with solution"). The date seems to be off in edge cases though, indicating that my assumptions about how caching works is off (unless there's a bug in my code). – jperezov Dec 31 '19 at 00:27
  • If you are saving the timestamp to the Browser like `var dt = new Date; localStorage.timestamp = dt.getTime();`, then you can access like `var gotdate = new Date(+localStorage.timestamp);`. – StackSlave Dec 31 '19 at 00:55
  • Of course, if you are trying to calculate anything based on your personal timezone and lets say you're `var offset = -7` hours then the math is `var millisecondsDifference = gotdate.getTime()+offset*3600000`. – StackSlave Dec 31 '19 at 01:02
  • @StackSlave I'm trying to use these timestamps for two purposes: 1) timestamps for my data sent to ElasticSearch, and 2) labels for my ElasticSearch indices. Everything needs stays UTC (so I use `.getUTCDate()` and `.now()`), and I'm not trying to make any local calculations dependent on timezones. – jperezov Dec 31 '19 at 01:07
  • @StackSlave I see what was causing the confusion--it was my bad. I'd meant to ask how to normalize time for clients whose computer clocks are off, rather than normalizing for timezones. I got it mixed up in my head. – jperezov Dec 31 '19 at 01:50
  • 1
    You cannot rely on the Client time, as it can be altered. You will have to rely on the Server, where you control the time. In PHP you would do something like `time = time(); echo json_encode($o); /* now you have object with time property for javascript AJAX argument */ } ?>` – StackSlave Dec 31 '19 at 02:17
  • 1
    What accuracy do you need (ms, s, minutes?); and do you trust the client enough? i.e. what's the worst that could happen to your system if a client manipulated the javascript or posted wrong data? NTP exists because it's hard to do distributed time synchronisation with accuracy, but if you don't care too much about seconds and are more interested in fixing client clocks that are off by a day or so, then @symcbean solution is probably enough. I'm hesitant because you state "the order matters"! I'm interested in your final solution, would love to see you post it as an answer. – thinkOfaNumber Jan 24 '20 at 01:06
  • I just went with the solution I already had, as it had a small enough error rate that's acceptable for my use case. I couldn't use @symcbean's solution since one of my prerequisites is not pinging the back-end, and attempting it without pinging the back-end causes a "runaway delta" problem. – jperezov Feb 03 '20 at 19:45

2 Answers2

1

Stick to timestamps on the client side - don't bother with dates/times. Store the delta between the serverside and the client clock on the client. Send the client timestamp and delta with the messages then reconstruct the date/time on the server.

Then you can make sensible decisions serverside about how to deal with jitter and jumps in the clock sync.

symcbean
  • 47,736
  • 6
  • 59
  • 94
  • Oooh, that's an interesting idea to save the delta between the clocks. Maybe I can start saving that info to see how big the deltas can get, and how often this issue occurs. – jperezov Dec 31 '19 at 01:02
0

You should not rely on the Client for guaranteed time. Your JavaScript may look something like:

var fd = new FormData, xhr = new XMLHttpRequest;
fd.append('get_time', '1'); xhr.open('POST', 'gettime.php');
xhr.onload = function(response){
  var obj = JSON.parse(response.responseText);
  console.log(obj.time);
}
xhr.send(fd);

If using PHP:

<?php // gettime.php
date_default_timezone_set('UTC'); $o = new StdClass;
if(isset($_POST['get_time'])){
  $o->time = time();
  echo json_encode($o);
}
?>
StackSlave
  • 10,613
  • 2
  • 18
  • 35