Problem/Motivation

Reusable/swapable tracking code with rendering/theming logic.

Proposed resolution

API to allow other modules to alter/extend the tracking code.

Remaining tasks

Write tests for the Google Analytics Test module (see tests/modules/google_analytics_test).

User interface changes

None

API changes

  • hook_google_analytics_tracker_alter()

Original report by @username

Since ga.js it looks impossible (untested) for other modules to set a custom variable in head. So it becomes impossible to add tracking data like shopping data to the ga tracking. See http://0-code.google.com.millennium.unicatt.it/apis/analytics/docs/gaJSA... for some good examples.

var pageTracker = _gat._getTracker("UA-12345-1");
   pageTracker._initData();
   pageTracker._trackPageview();
   pageTracker._addTrans(
      "1234", // order ID - required
      "Womens Apparel", // affiliation or store name
      "11.99", // total - required
      "1.29", // tax
      "15.00", // shipping
      "San Jose", // city
      "California", // state or province
      "USA" // country
    );
   pageTracker._addItem(
      "1234", // order ID - required
      "DD44", // SKU/code
      "T-Shirt", // product name
      "Olive Medium", // category or variation
      "11.99", // unit price - required
      "1" // quantity - required
   );
   pageTracker._trackTrans();

It looks like we need a hook or something else that would allow other modules to set custom variables and urls. We should do some brainstroming what the best way would be.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

hass’s picture

Marked #231451 as duplicate.

Anonymous’s picture

Nobody is interested on this?

I think it is the 'natural' next step for Analytics module. Now we have tracking and segmentation, but Analytics module has to be capable to track: subscriptions, e-commerce, registrations, newsletter or any other 'calls to action' to use all the power features from Google Analytics.

hass’s picture

Priority: Normal » Minor

I thought a little bit more about this and i think there are possible workarounds. There is no real need to add this in the hook way. Someone could create a variable in the page head like

var ecommerceTracker =_addTrans(...);
ecommerceTracker =_addTrans(...);

and add something like:

if (ecommerceTracker) { pageTracker = ecommerceTracker; }

to the "Custom JavaScript code" text area... not perfect, but this should work, too.

hass’s picture

The implementation must allow to add code *before* and *after* _trackPageview() call. See #275359: Allow adding custom javascript code before and after _trackPageview()

hass’s picture

Title: Add hook_google_analytics for other modules » Add hook_googleanalytics for other modules
hass’s picture

Status: Active » Needs review
FileSize
2.52 KB

This is a first try to implement such a hook for all other modules that likes to hook into GA module. It allows to push in every custom tracking method available in the GA API (http://code.google.com/apis/analytics/docs/gaJSApi.html).

I hope one of the google adsense, ubercart or ecommerce module maintainers could take a look if they are comfortable with this code or if they have a better idea. Feedback is very welcome!!! This patch will stay in queue until a maintainer depend on this code and approved it is working well.

Here follows a code example how this could be integrated in other modules:

function mymodule_googleanalytics(&$hook) {

  switch ($hook) {
    case 'preprocess':
      $value['methods'][]['_setDomainName'] = array('.example.com');
      $value['methods'][]['_clearIgnoredOrganic'] = array();
      $value['methods'][]['_setSampleRate'] = array('80');
      break;

    case 'process':
      $value['methods'][]['_addTrans'] = array(
        "1234", //order ID - required
        "My Partner Store", //affiliation or store name
        "84.99", //total - required
        "7.66", //tax
        "15.99", //shipping
        "Boston", //city
        "MA", //state or province
        "USA" //country
      );
      $value['methods'][]['_addItem'] = array(
        '343212',
        'DD4444',
        'Lava Lamp',
        'Decor',
        '34.99',
        '1',
      );
      $value['methods'][]['_addItem'] = array(
        '46464',
        'CC1111',
        'Black Lamp',
        'Decor',
        '2.99',
        '5',
      );
      $value['methods'][]['_trackTrans'] = array();
      break;

  }
  return $value;
}
moshe weitzman’s picture

Strictly speaking, I don't think this is needed. Modules can use hook_preprocess_page to fiddle with the $javascript array. They can inject their own calls wherever needed. They should do so before template_preprocess_page() does the final drupal_get_js().

hass’s picture

I'm not sure how they should insert something before the 'pageTracker._initData();'; or after 'pageTracker._trackPageview('. $url_custom .');';. If they like to do - they need to alter the $script added in footer with regexes or so and I'm not sure if template_preprocess_page() have the ability to alter this at this late stage. I've got some other troubles at this late stage in D6 that made me to move some parts to the hook_init(), but not all.

I will try to test template_preprocess_page() for altering the footer code. Aside this is only available in D6.

hass’s picture

Status: Needs review » Needs work

I've dumped all $variables in module_preprocess_page() and the only place I can see the footer code is in $variables['closure']. This is completely ready HTML code. No way to alter an array of javascript code. Therefor this will become a funny regex, but could work.

Marking need work to make clear this needs some more investigation and testing.

hass’s picture

Status: Needs work » Needs review

Ok, here the funny regex... the patch above will not be committed by this way. I hope this regex is correct. Maybe someone else could review this. Then we are able to add this to a readme or a handbook page to give other maintainers the ability to reuse this variant.

function mymodule_preprocess_page(&$variables) {

  // Example code. Make sure your module runs all values through drupal_to_js() to be protected against XSS.
  $before = 'pageTracker._setDomainName(".example.com");';
 
  $after  = 'pageTracker._addTrans("1234", "My Partner Store", "84.99", "7.66", "15.99", "Boston", "MA", "USA");';
  $after .= 'pageTracker._addItem("343212", "DD4444", "Lava Lamp", "Decor", "34.99", "1");';
  $after .= 'pageTracker._trackTrans();';
  
  $variables['closure'] = preg_replace('/(.*)(<script type="text\/javascript">var pageTracker = _gat._getTracker\("(UA-\d{4,}-\d+)"\);)(.*)(pageTracker._initData\(\);pageTracker._trackPageview\(\);)(.*)(<\/?script>)(.*)/i', "$2$before$5$after$7", $variables['closure']);

}

The only issue we really have with this solution is - it is not working for D5 and all e-commerce packages are only available for D5 today. I also expect that many people are not able or don't like to upgrade to D6 for a good time...

We could also change the (UA-\d{4,}-\d+) to (.*) validation here to reduce complexity (untested). We don't really need to validation here.

hass’s picture

Title: Add hook_googleanalytics for other modules » hook_preprocess_page example for other modules
hass’s picture

Status: Needs review » Needs work

There are some bugs... I will update asap.

hass’s picture

Status: Needs work » Needs review

Ok, new version that fixes to keep custom URL's, segmentation data and custom javascript code settings as is. Custom javascript code take recedence over module provided google api methods.

function mymodule_preprocess_page(&$variables) {

  // Example code. Make sure your module runs all values through drupal_to_js() to be protected against XSS.
  $before = 'pageTracker._setDomainName(".example.com");';
 
  $after  = 'pageTracker._addTrans("1234", "My Partner Store", "84.99", "7.66", "15.99", "Boston", "MA", "USA");';
  $after .= 'pageTracker._addItem("343212", "DD4444", "Lava Lamp", "Decor", "34.99", "1");';
  $after .= 'pageTracker._trackTrans();';
  
  $variables['closure'] = preg_replace('/(.*)(<script type="text\/javascript">var pageTracker = _gat._getTracker\("(UA-\d{4,}-\d+)"\);)(.*)(pageTracker._initData\(\);pageTracker._trackPageview\((.*)?\);)(.*)(<\/?script>)(.*)/i', "$2$before$4$5$7$after$8", $variables['closure']);

}
hass’s picture

Component: Code » Documentation
Category: feature » task
Priority: Minor » Normal
Status: Needs review » Fixed

Book page have been added at http://drupal.org/node/284599.

Anonymous’s picture

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for two weeks with no activity.

greggles’s picture

The original idea seems completely reasonable to me. In fact, I think we should extend the hook idea to include the ability to add multiple trackers.

Modifying this information via a preprocess hook feels quite strange and un-Drupal to me. Where else do we recommend that people do this? Does core ever recommend that people modify it via preg_replace in the theme layer?

hass’s picture

greggles’s picture

That should also be possible with a theme_form_id function and/or a hook_form_alter which give the data in a structured way (much like the initial hook you proposed here...).

hass’s picture

Could you provide an example? I asked this label question very long time and found many many people doing string replacements and such crazy things...

I have thought about a template file in 2.x, but not yet sure how this theming function should looks like best. Otherwise I'm not sure how moshe would do it, but he said we don't need this hook...

hass’s picture

hass’s picture

Status: Closed (fixed) » Active

@moshe: could you please enlighten us why we don't need it? Others don't like the regex stuff I've created very much... what would be the best solution?

moshe weitzman’s picture

I'm don't have enough familiarity with javascript or pagetracker (in the example). my idea was that modules could call pagetracker methods in their javascript that fiddle with variables. then the javascript that finally sends info to google runs last.

another idea is for this module to structure its javascript injection into a few different drupal_add_js() calls. Then modules can remove/add the parts they want during MODULE_process_page(). for an example of this fiddling, see jquery_update_preprocess_age() at http://cvs.drupal.org/viewvc.py/drupal/contributions/modules/jquery_upda...

hass’s picture

hass’s picture

Title: hook_preprocess_page example for other modules » Add hook_google_analytics for other modules
Component: Documentation » Code
Category: task » feature
drumm’s picture

Another use case is changing $url_custom from modules. The Apache Solr search module, used on Drupal.org, should set a custom URL to enable site search tracking.

drumm’s picture

For $url_custom, it would be more simple to do something like:

drupal_add_js(array('googleAnalyticsPageURL' => ...), 'setting');

And from JS, pageTracker._trackPageview(Drupal.settings.googleAnalyticsPageURL).

hass’s picture

hass’s picture

@drumm: logic wise this sounds difficult as we process the most code in hook_footer and hook_footer does not have the ability to add a JS setting to head... so core is blocking here :-(

Anonymous’s picture

The preg_replace code has no try{} and catch (err) {} statement, without them the replace doesn't do anything.

hass’s picture

Yeah, this needs an update.

danithaca’s picture

Damien (DamZ) proposed a much cleaner way of doing it:

// use hook_preprocess_page or hook_init, etc. 
function mymodule_block() {
  $node = menu_get_object();
  $your_ga_code = "pageTracker._trackEvent('Click', ${node->nid)";
  $GLOBALS['conf']['googleanalytics_codesnippet_after'] .= $your_ga_code;
}

The trick is that $GLOBALS['conf'] got flushed for every page request. so you can just use .= to append your GA code.

I've tested the code, and it works!! Thanks Damien!

hass’s picture

This is the most dirty way I can think of and not a way we go here.

patcon’s picture

oh SNAP. :)

mrfelton’s picture

The hook idea in http://drupal.org/node/231451#comment-900139 seems like by far the most flexible way to allow other modules to cater for additional tracking needs. After reading through this entire thread, I'm still unsure why that was dismissed...?

JimNastic’s picture

Sounds interesting, but complicated. I don't follow all the discussion but am very keen to find a way to add new events to GA.
Any updates? Is something like this likely to make it into an upcoming release of the module?

theapi’s picture

Version: 6.x-1.x-dev » 6.x-3.x-dev
FileSize
1.39 KB

Here's a patch for the latest google analytics 6.x-3.x-dev that gives a module a hook:
hook_googleanalytics_push_commands
which allows other modules to to add _gaq.push([commandArray]) commands.

It also provides the hook:
hook_googleanalytics_custom_url
Which allows modules to create their own custom_url to pass to _trackPageview

hass’s picture

Status: Active » Needs work

Your overloading variant cannot go in. Please read above concepts first. We cannot fire 20 hooks for performance reasons.

theapi’s picture

What 20 hooks? It's just two hooks.

What "overloading variant"? The above concepts are two years old and barely relavant to Google's latest code.

hass’s picture

Your hooks are tooo many. You made them only to solve your specific issues, but not the issues from others in general. Next time someone else need to hook something else and we have number 3 and so on. We need one hook that does all.

Only the age of a topic doesn't make the #6 concept wrong. Otherwise we could go with a "theme" like function.

theapi’s picture

Ok, I see.
With the new version of ga.js most of the commands can be entered using the _gaq.push(commandArray) function so rather than having to handle each method seperately and validate it etc as in #6, let the module implementing the hook add whatever commandArrays it wants.

So it would be something like:

function mymodule_googleanalytics(&$commands) {
$commands[] = "['_setCustomVar', 1, 'make', 'ford', 1 ]";
}

hass’s picture

This may work, but it must be a real API to allow other modules to detect the ga tracker type like ga.js async/legacy/ga.js and whatever may come in future. I'm fine to change API only with major version changes, but this will not solve the problem that Ubercart have now a problem to detect if GA is 2.x or 3.x.

theapi’s picture

So you need to continue to support the way the current googleanalytics module works using the old way "_gat._getTracker" etc, aswell as support the new async way? Is that correct?

hass’s picture

In general yes, but for now it would be great to make the api save for future and only implement the api to work with 3.x async. We can use 2.x for testing if it really makes Ubercart happy.

theapi’s picture

Do you want to do something like the views module that requires a module to declare what version of the api it works with?
Then get a list of all hooks that implement the api version and call only those functions relavant to the api.

hass’s picture

Yes :-)

theapi’s picture

Sounds like a plan...

...Trouble is I need to find time / persuade my bosses to allot time to do it.

stella’s picture

subscribe

mikeytown2’s picture

subscribe

This is how I see it:
P1: allowing custom push commands.
P2: allowing $url_custom to be set.
P3: allowing custom segmentation (might not be required after P1 is done).
P4: api hook & backport to 2.x.

I try to avoid putting code into text fields as much as possible, so $codesnippet_before & $codesnippet_after isn't my style. I could do a hook_init() {global $conf;} hack to set these variables at runtime via code (kinda like strongarm), but this isn't ideal for module development. For the hook I can see use cases for before and after, so I think there should be 2 hooks for push commands. In the mean time looks like I'll use the strongarm hack to get stuff done for custom modules we have.

Might want to look into this module
http://drupal.org/project/ga_customvars

hass’s picture

P4: 2.x will no longer supported very soon.
P3: #609892: Multiple custom variables
P2: #807320: Rollback: Track outgoing links as targets / Disable event tracking optionally for outgoing links. not active. Seems nobody really interrested in.
P1: 95% or more are static setting only and can be configured today. P3 may solve P1 for you too. I currently have no other pushes in mind, but i'm sure there might be some. The dynamic stuff may be very difficult to press into code in a generic way accept this hook implementation.

The last beein said I wish the hook would exist asap, but nobody seems to be really interrested, too.

interestingaftermath’s picture

subscribe

EvanDonovan’s picture

Seems like a hook like the one in #40 would be what I want in order to implement having a third tracker on my site based on what the $custom_theme variable is set to on a particular page. (We need to do that because of using one domain to display content from various content partners that need their own Analytics stats.)

I might look into writing a hook along those lines, but I wouldn't support the old GA APIs, since that would require a totally different way of approaching the problem.

For now, I think I'm just going to code the .gaq_push('custom variables/command') directly in my themes, like I had done in the past.

danithaca’s picture

@hass: The snippet you wrote at #13 using "mymodule_preprocess_page" is outdated. Would you please write a new snippet to support the new GA APIs? It would be very useful for people to add customized GA code in their modules since the hook is not ready soon. Thanks!

miro_dietiker’s picture

I've seen many hacked google_analytics due to this missing hook...

We might push this a little soon.

firebus’s picture

I agree with greggles et. al. that we need a hook. preprocess page and other workarounds are terrible - yes, you can do it, but it's kludgy and most importantly not at all readable or maintainable from a code perspective.

If your module implements hook_googleanalytics, it's pretty clear to anyone reading the code what's going on, and every module that implements hook_googleanalytics will have similar code.

If your module writes a custom preprocess function full of regex, then every module will do things differently and it will reduce maintainability/increase complexity.

hass’s picture

I have never said to go with hook_preprocess. No idea why people are not able to look into #6.

JeremyFrench’s picture

I have patched with the patch in #36, it seems to work well. Will know more when we have a few days reports in.

hass’s picture

Will never get committed because of design flaws.

hass’s picture

j0rd’s picture

Here's a modest proposal TM for getting stuff like this done.

Rules integration for Google Analytics goals tracking.
http://drupal.org/node/1243544

Added benefit is more modules will end up implementing rules, if they want to integrate with Google Analytics goals.

Added bonus, any module which already implements rules in to their workflow will be able to be easily tracked.

udipl’s picture

subscribe

wiifm’s picture

Going to post this here, in case a hook is never written to alter $url_custom.

The problem:

We are using drupal 7 with apachesolr search pages. These search pages can have any URL defined, and you can have many of these pages on your site (this is vastly different from drupal 6.x).

At the moment we have the defined search pages (where 'monkey' is the search term):

/search/results/monkey
/publications/monkey

This line was proving to be most troublesome, as the GA module assumes all search pages start with the word 'search'

if (module_exists('search') && variable_get('googleanalytics_site_search', FALSE) && arg(0) == 'search' && $keys = googleanalytics_search_get_keys()) {

This obviously was not the case with our site (and I imagine a growing number of sites going forward for drupal 7 apachesolr).

Instead of going down the road of forking this module and adding in the necessary hooks to alter $url_custom, I set about implementing hook_js_alter()

Here is my code:

/**
 * Massive hack in lieu of the GA module not wanting to support alter hooks
 */
function example_module_js_alter(&$javascript) {
  // modify the GA code if on a search results page
  if (module_exists('search') && variable_get('googleanalytics_site_search', FALSE) && arg(0) == 'publications') {

    // We allow different scopes. Default to 'header' but allow user to override if they really need to.
    $scope = variable_get('googleanalytics_js_scope', 'header');

    foreach ($javascript as $index => $js) {
      if ($js['type'] == 'inline' && $js['scope'] == $scope && strpos($js['data'], '_gaq.push([') > 0) {
        // publications search tracking support
        $url_custom = '';
        if ($keys = example_module_publications_search_get_keys()) {
          $url_custom = '(window.googleanalytics_search_results) ? ' . drupal_json_encode(url('search/' . arg(0), array('query' => array('search' => $keys)))) . ' : ' . drupal_json_encode(url('search/' . arg(0), array('query' => array('search' => 'no-results:' . $keys, 'cat' => 'no-results'))));
        }
        if (!empty($url_custom)) {
          $javascript[$index]['data'] = str_replace('_gaq.push(["_trackPageview"]);', '_gaq.push(["_trackPageview",' . $url_custom . ']);', $js['data']);
        }
      }
    }
  }
}

/**
 * Helper function for grabbing search keys. Function is missing in D7.
 */
function example_module_publications_search_get_keys() {
  static $return;
  if (!isset($return)) {
    // Extract keys as remainder of path
    // Note: support old GET format of searches for existing links.
    $path = explode('/', $_GET['q'], 2);
    $keys = empty($_REQUEST['keys']) ? '' : $_REQUEST['keys'];
    $return = count($path) == 2 ? $path[1] : $keys;
  }
  return $return;
}

I hope this helps someone else
Sean

hass’s picture

erlendstromsvik’s picture

I just read through this whole thread.

What you proposed in #6 does not seem to do anything about the $custom_url variable problems people have?

This is something we use in the ga-module. First module to return a custom_url "wins". We need to track categories visited and we use solr for search/categories. We also concluded that any module which returns a custom url for the current page, knows what it is doing. So first come, first served.
This is a simplified example on how we have done it:

foreach (module_implements('ga_custom_url') as $module) {
      $func = $module.'_ga_custom_url';
      if ($url_custom = $func()) {
        break;
      }
    }
function search_statistics_ga_custom_url() {
  if (!apachesolr_has_searched()) {
    return NULL;
  }
  
  $query = apachesolr_current_query();
  $phrase = trim($query->get_query_basic());
  if(empty($phrase)) {
      return NULL;
  }
  
  return drupal_to_js(url('', array('query' => 'search='. $phrase)));
}
hass’s picture

This was just a design idea. It is nothing ready... I believe we may also need weights...

tunic’s picture

Status: Needs work » Needs review

I want to introduce de GA Push sandbox module, that I think offers the discussed functionality not using hooks but direct function call. Well, offers this functionality for Drupal 7.x, but I although this issue is discussed for the 6.x branch I guess all people interested in this issue wants this in the 7.x branch too.

I don't see the point of using hooks to add GA push items to pages. Hooks are intended to 'observe' module actions and react accordingly. But in this case modules just need to send some GA push items to Google Analytics servers. So I think a good approach is to simply offer a function that any module can call that sends info to GA servers.

GA Push does this and something more. It offers a function to send info to GA (event and ecommerce info at the moment, page view tracking is not included as GA Drupal module already does it). The info is sent to GA using a 'backend': PHP or JS are the defined backends. If PHP method is selected the Drupal server makes a request to GA servers to send tracking information; JS method puts the info in the Drupal.settings array and the request is made by the client browser when page is loaded in the client side.

Both methods uses the Google Analytics Drupal module configuration, and in case of JS method it uses the customizations made to GA JS object (I'm thinking in the domain stuff).

Also allows to define via GUI JS events that will be tracked (elements clicked, elements hovered an so on).

GA Push has a submodule for tracking form validations errors (some people from marketing love this).

So, GA Push is an extension for Google Analytics module. We'd planned to release it as a full separate project, but we've been told to try integrate this module in GA. Personally I don't think GA Push should be part of the main GA module, as its functionality is only needed for advanced users and it's low coupled with GA (as it just uses GA config), but I want to hear more opinons. Also, GA Push should then wait until is integrated and stabilized inside GA before it can be used, and we need to use it in short.

GA Push is currently fully functional but it may be tested more extensively. Also, it may present undetected flaws, but this is why we share it, to improve it and allow others to use it.

Module can be found here:
http://drupal.org/sandbox/GeduR/1652964

Status: Needs review » Needs work

The last submitted patch, push_commands.patch, failed testing.

tunic’s picture

Status: Needs work » Needs review

Did I trigger a patch test? But I didn't attach any patch.... trying to set it to needs review again.

hass’s picture

Version: 6.x-3.x-dev » 7.x-1.x-dev
Status: Needs review » Needs work

Only patches for ga can be reviewed. My last idea was more a theme like function or js alter to change ga, but it need to be a usable array structure that can be "rendered". Also see #1300240: Add tracking for form error messages, please.

mikeytown2’s picture

Currently using similar code in #61 in D6 via AdvAgg's hooks. I wanted to add in the value of a CCK field to the end of a URL for tracking purposes; so /node/1 is /node/1?my_value=BBQ just for GA. Also using httprl_glue_url() in here.

/**
 * Implementation of hook_init
 */
function my_init() {
  // Disable the advagg cache on page nodes.
  $arg = arg();
  if (   isset($arg[0]) && $arg[0] == 'node'
      && isset($arg[1]) && is_numeric($arg[1])
      && empty($arg[2])
        ) {
    // Do stuff with the node id: $arg[1]
    $node = node_load($arg[1]);
    if (!empty($node) && $node->type == 'page') {
      $GLOBALS['conf']['advagg_use_full_cache'] = FALSE;
    }
  }
}


/**
 * Implement hook_advagg_js_header_footer_alter
 */
function my_advagg_js_header_footer_alter(&$master_set, $preprocess_js, $public_downloads) {
  // Return if not a node page.
  $arg = arg();
  if (   !isset($arg[0]) || $arg[0] != 'node'
      || !isset($arg[1]) || !is_numeric($arg[1])
      || !empty($arg[2])
        ) {
    return;
  }

  $node = node_load($arg[1]);
  // Return if node doesn't contain the field I'm looking for.
  if (   empty($node)
      || $node->type != 'page'
      || empty($node->field_my_cck_field[0]['value'])
        ) {
    return;
  }

  foreach ($master_set as $scope => &$data) {
    // Skip if no inline data.
    if (empty($data['inline'])) {
      continue;
    }
    foreach ($data['inline'] as $key => &$value) {
      // Skip if no inline code.
      if (empty($value['code'])) {
        continue;
      }
      // Skip if _trackPageview is not here.
      if (strpos($value['code'], '_gaq.push(["_trackPageview"]);') === FALSE) {
        continue;
      }

      // Get current URL and parse it.
      $parsed_url = parse_url(request_uri());

      // Pull out any query strings.
      if (!empty($parsed_url['query'])) {
        $query_vars = array();
        parse_str($parsed_url['query'], $query_vars);
      }
      // Add in our own query strhing
      $query_vars['aid'] = $node->field_my_cck_field[0]['value'];

      // Put the URL back together.
      $parsed_url['query'] = http_build_query($query_vars, '', '&');
      $new_url = httprl_glue_url($parsed_url);

      // String replace the _trackPageview code.
      $value['code'] = str_replace('_gaq.push(["_trackPageview"]);', '_gaq.push(["_trackPageview", "' . $new_url . '"]);', $value['code']);
    }
  }
}

hass’s picture

mikeytown2’s picture

@hass
Code in #69 is for D6.

hass’s picture

mrfelton’s picture

Here is a patch to the effect of what was in #63. It's a simple temporary solution that enables us to alter the url that gets passed to _trackPageview until someone gets around to implementing something better. We are using this with Drupal Commerce in order to append a product SKU to the checkout complete url so that we can do url based tracking with funnel pipelines for purchases of specific products (we were using ga_push to send Events and track those, but GA Event based goals doesn't yet support goal funnels).

/**
 * Implements hook_commerce_checkout_router().
 */
function cw_commerce_commerce_checkout_router($order, $checkout_page) {
  // If the user is on the commerce checkout complete page, set a static
  // variable so that we can react on this in hook_ga_custom_url().
  if ($checkout_page['page_id'] == 'complete') {
    $cw_commerce_commerce_checkout_router_complete = &drupal_static('cw_commerce_commerce_checkout_router_complete');

    $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
    $line_items = $order_wrapper->commerce_line_items->value();

    if ($line_items) {
      $product = commerce_product_load($line_items[0]->commerce_product[LANGUAGE_NONE][0]['product_id']);
      
      // Save the SKU into a static var that we can use later.
      $cw_commerce_commerce_checkout_router_complete = $product->sku;
    }
  }
}

/**
 * Implement hook_ga_custom_url() to override the tracked page url for
 * commerce checkout completions.
 *
 * This is a sort of hack based on http://drupal.org/node/231451#comment-6080926.
 */
function cw_commerce_ga_custom_url() {
  $sku = &drupal_static('cw_commerce_commerce_checkout_router_complete');
  if (empty($sku)) {
    return NULL;
  }

  $current_path = current_path();
  return drupal_json_encode(url($current_path, array('query' => array('sku' => $sku))));
}
hass’s picture

Version: 7.x-1.x-dev » 8.x-2.x-dev
Priority: Normal » Major
Issue summary: View changes
Status: Needs work » Needs review
FileSize
11.86 KB

This patch introduces an API to Google Analytics module. Please review if this fit Your needs or if there is something wrong. My intention is to allow theming of the tracking code.

hass’s picture

Title: Add hook_google_analytics for other modules » Add hook_google_analytics_tracker_alter() for other modules
yang_yi_cn’s picture

#73 seems to be a good solution for Drupal 7?

gapple’s picture

Why the two different formats for specifying values?
I don't see any additional value in the nested array format ($tracker['t0']['set'][] = array('key' => 'value');), and it would make altering more difficult.

I've taken a similar approach to make changes possible on a 7.x site (though only for 'set' commands).

kingandy’s picture

I'm not too keen on the $tracker['t0']['set'] format either. I'm trying to work with some Enhanced Ecommerce commands, which would mean I'd need to add some non-set instructions (specifically ec:addProduct and ec:setAction, and preferably ec:addImpression as well - see the Google docs).

Rather than trying to abstract the Analytics API into a set of action/argument objects, maybe it would be easier to allow modules to return lines (or arrays of lines) of javascript? It could be as simple as this:

$newlines = module_invoke_all('google_analytics_tracker');
$script .= implode("\r\n", $newlines);
$script .= 'ga("send", "pageview");';

I guess for D8 that module_invoke_all would be \Drupal::moduleHandler()->invokeAll('google_analytics_tracker'); but I'm not completely up to speed with D8 yet.

hass’s picture

Thanks for feedback guys.

  • The idea behind is that Google may change their API, but we are not required to change all modules that integrate with GA.
  • The reason for the $tracker['t0']['set'][] is that we have to support unlimited number of set commands. If you have a better idea how we can archive this, I'm open minded.
  • I'd also like to have weights and render arrays implemented. It's currently missing, but I thinks this is the cleanest way.
gapple’s picture

My use case was setting custom dimensions based on the node being viewed; some nodes have the needed properties themselves, others need to get them through relationships, and the default token method wasn't sufficient. Adding an alter hook allowed overwriting values for individual dimensions programmatically.

My critique is that it appears the array could end up something like this:

  'set' => array(
    'dimension1' => 'dimensionValue',
    0 => array(
      'dimension1' => 'aDifferentValue',
      'dimension2' => 'dimension2Value',
    )
  )

If I then tried to alter it like this:

  $tracker['t0']['set']['dimension1'] = 'alteredValue',
  $tracker['t0']['set']['dimension2'] = 'alteredValue2',

The values that would actually get sent to GA would be:

  dimension1 => 'aDifferentValue'
  dimension2 => 'alteredValue2'

In order to correctly set or alter values, I would need to iterate over every value and check both depths, unsetting any duplicate keys to ensure that the values I actually want are set. For these simple commands, only one value can actually be used, so it doesn't make sense to allow them to be set in two different ways.

I see that event tracking or extensions like Enhanced Ecommerce will throw a bit of a wrench in things. Handling advanced cases through objects could be much cleaner than making another ArrayPI and then putting a lot of logic in the output handler to make it work.

My rough thoughts:

  'set' => array(
    'dimension1' => 'value1',
    'dimension2' => 'value2',
  ),
  'send' => array(
    'pageview' => array(
      // array is encoded to a JSON object
      'page' => '/path'
    ),
    'event' => googleAnalyticsTrackerCommandObjectCollection(
      // This object will build it's own output for each item
      'items' => array(
        0 => array,
        1 => array,
      )
    )
  'ec:addImpression => googleAnalyticsTrackerCommandObjectCollection(
    // This object will build it's own output for each item
    'items' => array(
      0 => array,
      1 => array,
      2 => array,
    )
  )
  foreach ($commands as $command->$values) {
    if (is_array($values)) {
      foreach ($values as $key => $value) {
        if ($value instanceOf googleAnalyticsTrackerCommandObjectCollection) {
          $output .= $value->build(array($command, $key));
        }
        else {
          $value = Json::encode($value);
          $output .= "ga('$command', '$key', $value)";
        }
      }
    }
    else if ($values instanceof googleAnalyticsTrackerCommandObjectCollection) {
      $output .= $values->build(array($command));
    }
  }

  googleAnalyticsTrackerCommandObjectCollection::build($command) {
    $command = "'" . implode("','", $command) . "'";
    foreach ($this->items as $item) {
      $output .= "ga($command, Json::encode($item))";
    }
    return $output;
  }
hass’s picture

I think I understand what you are trying, but it does not allow use to have a weight and also does not allow users to order set commands. There are situations that require the correct order. Not for metrics and dimensions, but other may require it. At least with ga.js we had not the ability to add all parameters to one set.

e.g. (no real code, just logic)

_setCustomVar(4, name, value, opt_scope)
_setCustomVar(5, name, value, opt_scope)
if (foo) {_deleteCustomVar(3)}

and we also had situations that require to add some code before send and others after.

With events we may have "unlimited" number of events and none of these has a unique key:

ga('send', 'event', {
  'eventCategory': 'Category',
  'eventAction': 'Action',
  'eventValue': 55
});

and unlimited number of send commands per page (not that this makes sense to me, but some people track one page twice).

ga('send', {
  'hitType': 'pageview',
  'page': '/home'
});
ga('send', {
  'hitType': 'pageview',
  'page': '/de/home'
});
alanburke’s picture

FileSize
1.01 KB

Updated version of Tom's patch in #73

coltrane’s picture

I know there's a larger effort here for D8 but there's still a need for custom dimensions and metrics in D7 set programmatically. Here's a lightweight patch for google analytics 7.x-2.x to drupal_alter() custom dimensions and metrics so that for example, you could implement hook_googleanalytics_custom_dimension_alter() and add dimensions to $googleanalytics_custom_vars for use in the tracker code.

hass’s picture

This is not a D8 only case.

The last submitted patch, 74: Issue-231451-by-hass-Allow-altering-the-Google-Track.patch, failed testing.

The last submitted patch, 74: Issue-231451-by-hass-Allow-altering-the-Google-Track.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 83: 231451-google-analytics-d7-alter-dimensions-83.patch, failed testing.

osopolar’s picture

Here my patch for Drupal 7 (published before on issue #2681357: Posibility to alter token data for custom dimensions and metrics + suggestion from #83). It extents the solution of #83, as it will allow to modify the data array too, as shown in following example implementation for field-collection and taxonomy term pages. To be able to use additional tokens the settings form needs to be altered too, as below.

/**
 * Implements hook_googleanalytics_custom_vars_alter().
 */
function custom_module_googleanalytics_custom_vars_alter(&$data, &$googleanalytics_custom_vars, $googleanalytics_custom_type) {

  // Check if menu object is taxonomy term.
  if (empty($data)) {
    $term = menu_get_object('taxonomy_term', 2);
    if (is_object($term)) {
      $data += array('term' => $term);
    }
  }

  // Check if menu object is fieldcollection item.
  if (empty($data)) {
    $field_collection_item = menu_get_object('field_collection_item', 2);
    if (is_object($field_collection_item)) {
      $data += array('field_collection_item' => $field_collection_item);
    }
  }
}

/**
 * Implements hook_googleanalytics_admin_settings_form_alter().
 */
function custom_module_form_googleanalytics_admin_settings_form_alter(&$form, &$form_state) {
  // Alter settings form to enable all token types for custom dimensions.
  for ($i = 1; $i <= 20; $i++) {
    $form['googleanalytics_custom_dimension']['indexes'][$i]['value']['#token_types'] = array('all');
  }
  if (module_exists('token')) {
    $form['googleanalytics_custom_dimension']['googleanalytics_token_tree']['#token_types'] = 'all';
  }
}

To use the token [term:name] see issue #2037595: Change regex on Term ID privacy warning / Allow taxonomy tokens in dimensions.

hass’s picture

Removed the useless last patch from osopolar.

osopolar’s picture

@hass, very rude and arrogant your last comment. Please explain why my patch is useless and how else get taxonomy term tokens and field-collection tokens replaced?

hass’s picture

Yes, just because you have been so ignorant to read the case and also have not read my last patch #74 and just posted an totally unrelated patch that will never go in here and also goes in a totally wrong direction, too. I wish I would be able to delete the patch from the case.

osopolar’s picture

@hass Thank you for the kind explication.

I have checked the issue queue, didn't find any suitable an opened a new one: #2681357: Posibility to alter token data for custom dimensions and metrics. And guess what, it was closed as a duplicate of this issue, and it was you who closed the issue. Sorry, currently I am a bit confused how to solve correctly the issue I have described in #2681357: Posibility to alter token data for custom dimensions and metrics and where the solution should go.

I guess your patch in #74 should also regard the lines for custom dimensions:

// Add variables to tracker.
          $custom_var .= 'ga("set", ' . Json::encode($google_analytics_custom_type . $google_analytics_custom_var['index']) . ', ' . Json::encode($google_analytics_custom_var['value']) . ');';

The hook_google_analytics_tracker_alter() than practically needs to re-implement the whole custom dimensions and metrics stuff, as token replace clears all tokens.

Maybe the available tokens shouldn't be limited to nodes in first place. I am not yet familiar to D8 and if it is possible to make every entity available for token replacement, see:

// Replace tokens in values.
$types = [];
if ($request->attributes->has('node')) {
  $node = $request->attributes->get('node');
  if ($node instanceof NodeInterface) {
    $types += ['node' => $node];
  }
}

How likely would it be that the changes from this issue will be back-ported to D7?

hass’s picture

I do not like to add 100 alter hooks to the module. We need a flexible api that works for everything. That is what I tried to implement. If it can be backported, ok - if not - shit happens. Maybe an object oriented approach is more future safe than a hook.

Plazik’s picture

Title: Add hook_google_analytics_tracker_alter() for other modules » Add hook to alter data before sending it to browser

I also need this hook for adding Experiment ID and Experiment Variant depends of context (ie current page).

I've read all comments of this 8 years issue twice and I want to summarise available information from this issue and Analytics.js Field Reference.
We need this hook to:

  1. Modify "Create Only Fields"
  2. Modify code before ga("send", "pageview");
  3. Modify code after ga("send", "pageview");
  4. Modify code inside ga("send", "pageview"); (example)
  5. Modify these codes depends of some logic

There are several ways to do it:

  1. Render arrays
  2. Theme function
    1. drupal_alter() before sending variables to theme function
    2. Preprocess function

Render arrays is too comptlex for this case.

almost any time a module creates content, it should be in the form of a render array.

https://www.drupal.org/node/930760.
We do not create any content. GA code should be added to page only once.

Theme function is more interesting option. The module can prepare his data in array and send this variable array to theme function. Only one hook can be added before sending to allow other modules to alter this data depends of current page. All module settings should be in this array too ($url_custom, $library_tracker_url, $googleanalytics_adsense_script and others).

Preprocess function looks like the same. It will allow to modify data before sending to theme function. Hooks are typically used by modules, preprocess functions are by themes.

I like the version with custom hook more than with preprocess function.

I imagine that sending data must be something like this:

$varibles = array(
  // Some module settings.
  'settings' => array(),
  // Google Analytics code in an array.
  // Our function will convert it to valid JS code.
  'ga' => array(
    'create' => array(
      'UA-XXXX-Y',
      // Create Only Fields.
      'cookieDomain' => 'auto',
      'name' => 'myTracker',
      'sampleRate' => 5,
    ),
    'set' => array(
      'anonymizeIp' => TRUE,
      array(
        'page' => '/about.html',
        'title' => 'About',
      ),
      'referrer' => 'http://example.com',
      // Custom Dimension.
      'dimension14' => 'Sports',
    ),
    'require' => array(
      'ec',
    ),
    'send' => array(
      'event' => array(
        array(
          'eventCategory' => 'Category',
          'eventAction' => 'Action',
        ),
      ),
      // Pageview should be run at the end.
      'pageview',
      'social' => array(
        array(
          'socialNetwork' => 'Facebook',
          'socialAction' => 'Like',
          'socialTarget' => 'http://foo.com',
        ),
      ),
      // Timing.
      'timing' => array(
        array(
          'timingCategory' => 'category',
          'timingVar' => 'lookup',
          'timingValue' => 123,
        ),
      ),
    ),
    // Enhanced E-Commerce.
    'ec:addProduct' => array(
      array(
        'name' => 'Android T-Shirt',
        'brand' => 'Google',
      ),
    ),
    'ec:setAction' => array(
      'detail',
      'purchase' => array(
        'id' => 'T1234',
      ),
    ),
  ),
);

// Our hook.
drupal_alter('google_analytics', $variables, $page);

$script = theme('google_analytics', $variables);

And then in custom module:

/**
 * Implements hook_google_analytics_alter().
 */
function mymodule_google_analytics_alter(&$variables, $page) {
  if ('my first condition' == $page['something']) {
    // I don't want to sending Timing.
    unset($variables['ga']['send']['timing']);

    // Set Experiment ID.
    $variables['ga']['set']['expId'] = 'my experiment id key';
  }

  if ('my second condition' == $page['something']) {
    // Remove one event.
    if (isset($variables['ga']['send']['event'])) {
      foreach ($variables['ga']['send']['event'] as $key => $event) {
        if ('eventCategory' == $event['eventCategory']) {
          unset($variables['ga']['send']['event'][$key]);
        }
      }
    }
  }
}
I'd also like to have weights and render arrays implemented. It's currently missing, but I thinks this is the cleanest way.

Which GA code request correct order? I didn't find it on https://developers.google.com/analytics/devguides/collection/analyticsjs... page.

yonailo’s picture

+1 to backport this to D7 version

bmcclure’s picture

Here's the start of another approach for the D8 version of this patch. Now it creates an event to subscribe to ("google_analytics.override")

Currently, this only allows overriding the account ID, so this patch isn't complete yet, but the event can be expanded to encompass any properties desired.

hass’s picture

japerry’s picture

Status: Needs work » Closed (outdated)

With the sunset of legacy google analytics, the 8.x-2.x module is now unsupported. If this is still an issue with the 4.x version, please file a new issue in the queue.