After following #2391025: Add support for inline JS/CSS with #attached for a long time, a quick look at #2391029: Attaching inline JS no longer possible. Help get this back into core!, and a look at the code of google_analytics_page_attachments() in 023a2ffc1 of 8.2.x,

I want to propose a naive idea of getting rid of inline js for this module.
I post this here to not the spam the Drupal core issue.
It is based on a limited understanding of google_analytics.

This does not mean I say that core should not support it, I just say that it may not be required for this module.

Here it is:

// -ss for double plural
var argss = Drupal.settings.google_analytics.argss;
for (var i = 0; i < argss.length; ++i) {
  ga.apply(null, argss[i]);
}

Tell me why this would not work.

Comments

donquixote created an issue. See original summary.

donquixote’s picture

hass’s picture

Status: Active » Closed (duplicate)
donquixote’s picture

This other issue is horribly long, and your "reasons" are scattered.

Here is a collection from what I can find in #2391025: Add support for inline JS/CSS with #attached. For a start, I will quote them without adding my own comments.
There may be more, but I want to focus on this.
I think it is reasonable to have a summary of this specific case outside of the other issue.

#37
This is quite lengthy, and I will not quote it.

#51:

If a module like https://www.drupal.org/project/eu_cookie_compliance need to set window['ga-disable-UA-XXXXXX-Y'] = true; this need to be added before GA code. This is currently not possible.

#71:

  1. I guess I need to clarify one thing. I'm not adding an external asset. External assets are URLs like http://example.com/foo.js. See https://developers.google.com/analytics/devguides/collection/analyticsjs... how this dynamic Google inline code looks like, please. This code can change on every page and it is not possible to move this into a static file.
    <!-- Google Analytics -->
    <script>
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
    ga('create', 'UA-XXXX-Y', 'auto');
    ga('send', 'pageview');
    </script>
    <!-- End Google Analytics -->
  2. Google requires that the window['ga-disable-UA-XXXXXX-Y'] = true; is added before the GA code from #1 is added. See https://developers.google.com/analytics/devguides/collection/gajs/#disable, please

    This window property must be set before the tracking code is called. This property must be set on each page where you want to disable Google Analytics tracking. If the property is not set or set to false then the tracking will work as usual.

  3. As I see no way how I can make #1 an external asset and also not how I can make this an internal I'm stuck with inline JS. This means I need to use html_head currently. This html_head does not have a weight. As other modules may add the code from #2 they are not able to make sure this code has a higher weight than #1 JS code and than the #1 JS code tracks the users where it should not.
donquixote’s picture

Now my replies.
First, the part with (function(i,s,o,g,r,a,m)[..].

This code can change on every page and it is not possible to move this into a static file.

The only part that changes is the analytics.js url, e.g. 'analytics.js' vs 'analytics_debug.js', and query parameters.
This can go into a setting easily.

Google requires that the window['ga-disable-UA-XXXXXX-Y'] = true; is added before the GA code from #1 is added.

So, here the problem is the order in which scripts are executed.
There are other ways to make a script run later, without making it inline.
What comes to mind are on-document-load, or Drupal behaviors.

If it is not possible to tell Drupal "Let my script run later", then this is a problem for itself.
Inline JS may have incidentally solved this problem in the past, but this is not what it was made for.

And while we are at it:
If google does not track a page with window['ga-disable-UA-XXXXXX-Y'] === true, then would it not be even smarter to check this variable in the module's js, and skip loading of analytics.js entirely?

donquixote’s picture

Status: Closed (duplicate) » Active

As said in my previous comments, I am not convinced that it "Does not work". This is why I am reopening it.

I also think the details of this discussion should be kept out of the other issue, which is why I think this should not be a duplicate.

hass’s picture

Status: Active » Closed (duplicate)

The only part that changes is the analytics.js url, e.g. 'analytics.js' vs 'analytics_debug.js', and query parameters.
This can go into a setting easily.

You have no idea about ga code and it's flexibility and you have not read the full comment list. You just grabbed the child garden stuff.

Please read the google documentation https://developers.google.com/analytics/devguides/collection/analyticsjs/ and ALL subpages, not only the first page with default tracker. May review modules code, too. When you found the 500 settings in documentation you may get an idea about the challenge and if you know a way how to embed javascript code into drupal.settings, let me know.

Aside from this - only the code documented by Google is supported. I do not plan to reinvent google analytics api.

donquixote’s picture

you have not read the full comment list. You just grabbed the child garden stuff.

I did my best to find the relevant comments.
If I missed something, I would ask you to point out *one* thing that makes the inline js necessary.
If there are 500 different reasons why this is so, then it should be possible to find one among those reasons that actually sticks.

Please read the google documentation https://developers.google.com/analytics/devguides/collection/analyticsjs/ and ALL subpages

I will not read ALL sub-pages.
I have no doubt that GA is super flexible and has many options.
But what I am interested in is something you are currently doing in the GA module (D7 or D8) that can only be done with inline JS.
For this it is not necessary to understand every possible option of GA.

May review modules code

This is what I am doing, and it should be sufficient for participating in this issue.
I assume your goal is to keep the currently implemented features working (in a more clean way, perhaps), maybe gradually add to it, not to implement all the "500 settings" that would be available.

The only places in code (D8) that I find where you are currently adding inline js are:
- google_analytics_page_attachments()
- google_analytics_preprocess_item_list__search_results()

In the Drupal 7 version this is done with drupal_add_js(). But it does not look that much different.

None of those looks like they need to be inline.

hass’s picture

You have no idea about ga module, but try to tell me how it works? Make you homework first, please. If you need a training I can sell it to you.

This module requires inline code. There is no way around. If you spend only 30 minutes with the documentation you may understand. If you can come up with a design change of Google Analytics you may tell Google, but I suspect they made their homework. At least from my pespective they have. Tracking code cannot moved into external files. It is bullsh** that it is just the js filename.

donquixote’s picture

The only reason I am spending time here is because you are requesting a change of Drupal core, and quite loudly so. Your claim that you absolutely need it for this module does hit some opposition in the other thread.
Otherwise I would be perfectly fine letting you do your own thing with your own module.

If you need such a change in core, you should at least be able to explain why you need it, beyond "Either you take my word for it, or you study the entire module and GA API.". This attitude prevents any discussion or cooperation.

Now the funny thing is that I am actually kinda on your side with regard to reintroducing support for inline JS, which was removed without a sufficient public discussion or explanation. But I prefer if things are done for good reasons, and those reasons are sufficiently explained and documented.

Even if this module could live without inline JS, I think I would still support adding it back into core..

I was even considering to write a patch.
But with this attitude, I am out.

hass’s picture

There is no need for a patch for ga module. I explained all in the other thread. Really. Also why it is not possible to use settings. It was also confirmed by _nod that settings will not work. You just ask for something I already explained at least 2-3 times. How annoying is it to explain everything again and again just because for one reason - you do not know the google analytics api. And every week another guy jumps in and ask the same again. Sorry.

donquixote’s picture

It was also confirmed by _nod that settings will not work. You just ask for something I already explained at least 2-3 times.

If you tell me the issue comment numbers, I can be more specific.
I am trying to guess which comments you are referring to. I do not find any comment by @nod_ which confirms explicitly that your use case cannot be done without inline js. I think he is just accepting that you find this necessary.

Most of your comments in #2391025: Add support for inline JS/CSS with #attached are about "There are so many settings, which all change the inline js code.".
This is an argument which I find not convincing. From looking at the module code, all inline JS is produced in google_analytics_page_attachments(). If this can be replaced with a static code + settings, then the argument is invalid.

---------------------------

The other argument is about timing.

Some other contrib modules integrate with GA module and add code before or after mine and they depend on the GA module. Where code is added depends on logic and may require a weight functionality.

The main example was the cookie compliance setting.

From other resources about GA, so far my understanding is that the snippet (which is currently inline) should run quite early in the request, and not wait for full page load. Some suggest to achieve this by making the script async.

<script type="text/javascript"  src="/drupal-ga-integration.js" async></script>

https://stackoverflow.com/a/21034478/246724
https://stackoverflow.com/a/26068519/246724 (other answer on same question)
(There are other Q/As, but they are about the older version of GA.)

The script would need to run after the settings are initialized, obviously, if it wants to use the settings.

I don't know the timing constraints of other modules that need to run after GA.

The question would be: If different modules add external scripts as async, would the order of execution be reliable in any way?
If not, then this is a convincing argument.

---------------------------

Another one, in #37 of the other thread

everyone will ask - why I'm not using the original code from Google. All times in past I've done this - I had tons of request why it's different. And really there was no good reason to do it the other way. OT.

I find this to be a good argument actually.
Even if it was technically possible to do it otherwise (which we disagree on), there is some value in familiarity, and it will just "look wrong" otherwise. Having the inline snippet as recommended by Google means that a site builder can compare this with the docs and understand what is going on.

hass’s picture

This is an argument which I find not convincing. From looking at the module code, all inline JS is produced in google_analytics_page_attachments(). If this can be replaced with a static code + settings, then the argument is invalid.

As noted, this is not possible.

This is NOT how a GA tracker may look like. There can be 100 send, event, ecommerce and other lines. These lines are not static.

ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');

EXAMPLE code you may try to press into settings... good luck!

ga('create', 'UA-XXXXX-Y', 'auto', {
  'clientId': getClientIdFromUrl()
});
ga('require', 'displayfeatures');
ga('require', 'displayfeatures', {cookieName: 'display_features_cookie'});
ga('provide', 'myplugin', MyPlugin);
ga('set', 'anonymizeIp', true);
ga('send', 'pageview', [page], [fieldsObject]);
ga('send', 'pageview', location.pathname
if (document.location.pathname.indexOf('user/' + userID) > -1) {
  var page = document.location.pathname.replace('user/' + userID, 'user');
  ga('send', 'pageview', page);
}
if (document.location.pathname.indexOf('user/' + userID) > -1) {
  var page = document.location.pathname.replace('user/' + userID, 'user');
  ga('send', 'pageview', page);
}
/* 200 lines of custom events */
ga('send', 'event', 'Videos', 'play', 'Fall Campaign');
ga('send', 'event', 'Videos', 'play', 'Fall Campaign', { nonInteraction: true });
ga('send', 'event', [eventCategory], [eventAction], [eventLabel], [eventValue], [fieldsObject]);

function handleOutboundLinkClicks(event) {
  ga('send', 'event', {
    eventCategory: 'Outbound Link',
    eventAction: 'click',
    eventLabel: event.target.href
  });
}
function handleOutboundLinkClicks(event) {
  ga('send', 'event', {
    eventCategory: 'Outbound Link',
    eventAction: 'click',
    eventLabel: event.target.href,
    transport: 'beacon'
  });
}

ga('send', 'pageview', {
  'dimension15':  'My Custom Dimension'
});
ga('send', 'event', 'category', 'action', {
  'metric18': 8000
});

ga('send', 'social', 'Facebook', 'like', 'http://myownpersonaldomain.com');
ga('send', 'social', [socialNetwork], [socialAction], [socialTarget], [fieldsObject]);
ga('send', 'timing', 'JS Dependencies', 'load', 3549);
ga('send', 'timing', [timingCategory], [timingVar], [timingValue], [timingLabel], [fieldsObject]);
try {
  // Runs code that may or may not work.
  window.possiblyUndefinedFunction();
} catch(err) {
  ga('send', 'exception', {
    'exDescription': err.message,
    'exFatal': false
  });
}
// create a tracker named 'foo' for property UA-XXXXX-Y
ga('create', 'UA-XXXXX-Y', {name: 'foo'});
ga('foo.require', 'displayfeatures');
ga('foo.send', 'pageview');

// create a second tracker named 'bar' for a different property UA-XXXX-Z
ga('create', 'UA-XXXXX-Z', {name: 'bar'});
ga('bar.require', 'displayfeatures');
ga('bar.send', 'pageview');
ga('set', 'displayFeaturesTask', null);

ga('ec:addProduct', {               // Provide product details in a productFieldObject.
  'id': 'P12345',                   // Product ID (string).
  'name': 'Android Warhol T-Shirt', // Product name (string).
  'category': 'Apparel',            // Product category (string).
  'brand': 'Google',                // Product brand (string).
  'variant': 'Black',               // Product variant (string).
  'position': 1,                    // Product position (number).
  'dimension1': 'Member'            // Custom dimension (string).
});

ga('ec:setAction', 'click', {       // click action.
  'list': 'Search Results'          // Product list (string).
});

The inline code could also be:

<!-- Google Analytics -->
<script>

// Instructs analytics.js to use the name `analytics`.
window.GoogleAnalyticsObject = 'analytics';

// Creates an initial analytics() function.
// The queued commands will be executed once analytics.js loads.
window.analytics = window.analytics || function() {
  (analytics.q = analytics.q || []).push(arguments)
};

// Sets the time (as an integer) this tag was executed.
// Used for timing hits.
analytics.l = +new Date;

// Creates a default tracker with automatic cookie domain configuration.
analytics('create', 'UA-12345-1', 'auto');

// Sends a pageview hit from the tracker just created.
analytics('send', 'pageview');
</script>

<!-- Sets the `async` attribute to load the script asynchronously. -->
<script async src='//www.google-analytics.com/analytics.js'></script>
<!-- End Google Analytics -->
From other resources about GA, so far my understanding is that the snippet (which is currently inline) should run quite early in the request, and not wait for full page load. Some suggest to achieve this by making the script async.

Not supported by Google and these commenters have no clue about GA code and how it can look like. Again - it is not static code we can put into a JS file. This will end with 1.000.000 files on disk on d.o. as one example.

I don't know the timing constraints of other modules that need to run after GA.

Ecommerce code will be added by other modules. It need typically run after GA.

Even if it was technically possible to do it otherwise (which we disagree on), there is some value in familiarity, and it will just "look wrong" otherwise. Having the inline snippet as recommended by Google means that a site builder can compare this with the docs and understand what is going on.

Often there are issues... and then people ask Google for support. Google only tells them, this is not the standard code, they need to implement this first. I permanently get cases about this... just because Google Support is too stupid to understand the "difference of nothing" of a single and double quote... just to give you one example. I guess I have 5 closed cases only about this...

donquixote’s picture

ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');

These calls are easy, using ga.apply() as in my original post.
First step:

// -ss for double plural
var argss = [
  ['create', 'UA-XXXXX-Y', 'auto'],
  ['send', 'pageview']
];
var argss = Drupal.settings.google_analytics.argss;
for (var i = 0; i < argss.length; ++i) {
  ga.apply(null, argss[i]);
}

Second step:

// -ss for double plural
var argss = Drupal.settings.google_analytics.argss;
for (var i = 0; i < argss.length; ++i) {
  ga.apply(null, argss[i]);
}

We could even go one step further and crack open the ga() function, which itself only queues the arguments for later. But let's focus on the necessary.

--------------

Now the trickier parts.

ga('create', 'UA-XXXXX-Y', 'auto', {
  'clientId': getClientIdFromUrl()
});

I wonder: Where does this come from? I do not find "getClientIdFromUrl" in the module code, or in any other module.
Is this user-provided code?
In the PHP I find this:

    // Add any custom code snippets if specified.
    $codesnippet_create = $config->get('codesnippet.create');
    $codesnippet_before = $config->get('codesnippet.before');
    $codesnippet_after = $config->get('codesnippet.after');

Which corresponds to settings in the admin backend.
These could either be implemented with eval(), or move into a generated js file. After all, this file only would needs to be renewed when someone submits the admin settings form.
(not saying I recommend either of those solutions, I just mention the possibility)

ga('send', 'pageview', [page], [fieldsObject]);

Where are the variables defined? "page" and "fieldObject"?

if (document.location.pathname.indexOf('user/' + userID) > -1) {
  var page = document.location.pathname.replace('user/' + userID, 'user');
  ga('send', 'pageview', page);
}

Well, whatever was in "page" is now overwritten.
And again, I do not find this anywhere in the module code.

This logic looks like it could be done in PHP, where settings are generated. We know in PHP on what page we are on.

function handleOutboundLinkClicks(event) {
  ga('send', 'event', {
    eventCategory: 'Outbound Link',
    eventAction: 'click',
    eventLabel: event.target.href
  });
}

Again, where is this snippet generated? I do not find it anywhere in a Drupal module.
However, I do find it here, https://stackoverflow.com/questions/35827580/google-analytics-url-builde...

This code will only have an effect if you use it as an onclick handler somewhere.

// Instructs analytics.js to use the name `analytics`.
window.GoogleAnalyticsObject = 'analytics';

Again, is this really something that the google_analytics module would do?

-------------

It seems like a lot of the code example you gave are from online examples of what Google Analytics inline js *could* be like.
This is not code that would be typically generated with this module in its current version in Drupal 7 or 8.
Unless this is all from the user-provided js.

Summary:
- ga.apply() + Drupal.settings can get you quite far.
- Some logic can happen in PHP.
- User-generated code, or code that does not vary page by page, could live in generated js files. I am not saying this is a good idea, just that it would be technically possible. Maybe in such a case you would want to put everything into one generated js file, including the parts that are not user-provided.
- As a last resort there is always eval(), but you probably want to avoid that.

Maybe there are some parts that are still problematic.
For those, I would ask:
- are these really relevant for the google_analytics module, or are they just some example found on the web?
- are these the same on every page, or do they vary?
- where are those currently generated in the google_analytics php code?

MustangGB’s picture

It would be really nice if we could remove inline scripts.
@donquixote do you have any patch started?

donquixote’s picture

@MustangGB
I did not produce a patch, because the maintainer does not seem to be interested..

gapple’s picture

The Googalytics module (originally "gacsp") makes use of the same ga.apply() method suggested here to avoid inline scripts.

MustangGB’s picture

@gapple Thanks for the information, sadly no D7 release though, I'll keep it on my radar though.