Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I honor Open in new tab requests when onclick event executes asynchronous code?

The setup

I have implemented an "onclick" method which calls a bit of third party code asynchronously. It provides a callback, and they suggest using a simple function that sets document.location = href to make sure the page only changes after their method has returned. This works fine if you want to open the link in the same tab, but if you want to open in a new tab then one of two things happens:

The problems

  1. If you middle-clicked, Ctrl+click, Shift+click, or Cmd+click it opens in the same tab after having executed the callback function.
  2. If you right-click and select 'Open in new tab' then the onclick code is not called at all.

The questions

Is it possible to have the callback function return true to the onclick event (in effect, making the entire chain of events synchronous)? Or, can you think of a way to preserve standard browser behavior in Case 1 above?

The code

https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce:

<a href="/product_details?id=P12345" onclick="clicked(); return !ga.loaded;">Product Link</a>

<script>
// Called when a link to a product is clicked.
function clicked() {
  ga('ec:addProduct', {
    'id': 'P12345',
    'name': 'Android Warhol T-Shirt',
    'category': 'Apparel',
    'brand': 'Google',
    'variant': 'black',
    'position': 1
  });
  ga('ec:setAction', 'click', {list: 'Search Results'});

  // Send click with an event, then send user to product page.
  ga('send', 'event', 'UX', 'click', 'Results', {
      'hitCallback': function() {
        document.location = '/product_details?id=P12345';
      }
  });
}
</script>
like image 980
nachito Avatar asked Nov 10 '22 04:11

nachito


1 Answers

This is a tricky issue that I've had to tackle multiple times in my career. I've taken various approaches, and in this answer I'll try to summarize my latest thinking:

Don't prevent default.

When you prevent the browser from taking its default action you end up having to recode that stuff yourself, and it gets pretty hairy to manually handle all the possible combinations of user intent and browser/platform variations. It's not worth it. There are some things (like checking which keys are pressed) that can be accounted for, but there are other things (like did the user right click and choose "open in a new tab") that are essentially impossible to detect.

Personally, I think it's best to just let the browser do its thing, and figure out another way to accomplish what you're trying to do.

Make the ga() function sync.

In the sample code you provide, you're trying to send e-commerce and event hits to Google Analytics when the user clicks on a link. The problem is that the ga() call is async, so if the user clicks on a link, the call may or may not finish before the browser loads the new page.

One possible solution would be to override how analytics.js is sending the hit to make it sync instead of async. This will allow you to send the hit without having to prevent default on the event and deal with opening the link manually.

analytics.js recently introduced Tasks, which allow you to override or modify various built-in behaviors of the library. Here's an example of how you'd override the sendHitTask to implement a sync version:

ga('create', 'UA-XXXX-Y', 'auto');

ga(function(tracker) {

  // Grab a reference to the default sendHitTask function.
  var originalSendHitTask = tracker.get('sendHitTask');

  // Modify the send hit task to be sync in cases where an
  // external link was clicked.
  tracker.set('sendHitTask', function(model) {
    if (wasExternalLinkClicked()) {
      var xhr = new XMLHttpRequest();
      var url = '//www.google-analytics.com/collect?' + model.get('hitPayload');
      // Specifying `false` as the third parameter makes the request sync.
      xhr.open('GET', url, false);
      xhr.send();
    } else {
      originalSendHitTask(model);
    }
  });
});

ga('send', 'pageview');

Beware, I haven't tested this code, but I think it should work. And, in case it's not obvious, you'll have to implement wasExternalLinkClicked() yourself, but that shouldn't be too difficult.

Anyway, hopefully that help!

like image 72
Philip Walton Avatar answered Nov 14 '22 21:11

Philip Walton