27

I am adding some external JavaScript to the end of the page via my chrome extension. The external JavaScript then tries to post some data back to the server, however that is not taking place.

The JavaScript wants to get the url of the current page and the referrer and post it back to the server.

Can anyone please advice me what is wrong here and how can I if not possible this way can I post data from the current page back to the server.

manifest.json

{
  "name": "Website Safety",
  "version": "1.0",
  "manifest_version": 2,
  "description": "The Website Safety Extension.",
  "browser_action": {
    "name": "View the website information for ",
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },
  "permissions": [
    "tabs", "http://*/*", "https://*/*"
  ],
  "background": {
  //  "scripts": ["refresh.js"]
    "page": "background.html"
  },
  "content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'",
  //"background_page": "background.html"

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["contentScript.js"]
    }
  ]
}

for now contentScript.js

var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-31046309-1']);
_gaq.push(['_setAllowLinker', true]);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
//ga.src = 'https://ssl.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();

var _Hasync= _Hasync|| [];
_Hasync.push(['Histats.start', '1,1342541,4,0,0,0,00000000']);
_Hasync.push(['Histats.fasi', '1']);
_Hasync.push(['Histats.track_hits', '']);
(function() {
var hs = document.createElement('script'); hs.type = 'text/javascript'; hs.async = true;
hs.src = ('http://s10.histats.com/js15_as.js');
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(hs);
})();
Rob W
  • 341,306
  • 83
  • 791
  • 678
Vish
  • 4,508
  • 10
  • 42
  • 74
  • Some code would be good. – Lupus Apr 23 '12 at 18:13
  • 2
    You want to get the URL of the current page, and its referrer, and send that user-specific private data off to an arbitrary server?! – Andrew Leach Apr 23 '12 at 18:33
  • 2
    not to a random server, the same server it got the js from. the user should be able to see what someone else had been upto in the time they were gone away by activating this extension. – Vish Apr 23 '12 at 23:48
  • There's a new way of dealing with this. See my answer: https://stackoverflow.com/questions/10285886/chrome-extension-adding-external-javascript-to-current-pages-html/30503705#30503705 – gblazex May 28 '15 at 10:41

3 Answers3

44

Content scripts do not run in the scope of the page (see also), they run in a context between your extension and the web page.

Since the trackers are of the type "Injected script", these fully run in the context of the web page. But the _gaq and Hasync variables don't. As a result, the track scripts cannot read the configuration variables.

There are two (three) ways to fix it.

  1. Inject your code (as posted in the question) using this method. Using this method for your purpose is discouraged, because your script overwrites a commonly used global variable. Implementing your script using this method will break the tracking on many websites - do not use it.
  2. Fully run the code within the scope of a content script:
    Two options:
    1. Include the external files in the extension
    2. Include the external files in the extension, plus implement an auto-update feature.

Method 1: Fully local copy

manifest.json (only the relevant parts are shown):

{
  "name": "Website Safety",
  "version": "1.0",
  "manifest_version": 2,
  "description": "The Website Safety Extension.",
  "permissions": [
    "tabs", "http://*/*", "https://*/*"
  ],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["ga-config.js", "ga.js", "js15_as.js"]
    }
  ]
}

In ga-config.js, define the variables as follows:

var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-31046309-1']);
_gaq.push(['_setAllowLinker', true]);
_gaq.push(['_trackPageview']);

var _Hasync= _Hasync|| [];
_Hasync.push(['Histats.start', '1,1342541,4,0,0,0,00000000']);
_Hasync.push(['Histats.fasi', '1']);
_Hasync.push(['Histats.track_hits', '']);

Download https://ssl.google-analytics.com/ga.js, and save it as ga.js.
Download http://s10.histats.com/js15_as.js, and save it as js15_as.js.

Method 2: Injecting a Up-to-date GA

If you want to have an up-to-date version of GA, a convoluted way of injecting the code has to be used, because Content scripts cannot be included from an external URL.

An old version of this answer relied on the background page and chrome.tabs.executeScript for this purpose, but since Chrome 20, a better method has become available: Use the chrome.storage API to cache the JavaScript code. To keep the code updated, I will store a "last updated" timestamp in the storage; you can also use the chrome.alarms API instead.

Note: Do not forget to include a local copy of the external file in your extension, in case the user does not have an internet connection, etc. Without an internet connection, Google Analytics wouldn't work anyway.

Content script, activate-ga.js.

var UPDATE_INTERVAL = 2 * 60 * 60 * 1000; // Update after 2 hour

// Retrieve GA from storage
chrome.storage.local.get({
    lastUpdated: 0,
    code: ''
}, function(items) {
    if (Date.now() - items.lastUpdated > UPDATE_INTERVAL) {
        // Get updated file, and if found, save it.
        get('https://ssl.google-analytics.com/ga.js', function(code) {
            if (!code) return;
            chrome.storage.local.set({lastUpdated: Date.now(), code: code});
        });
    }
    if (items.code) // Cached GA is available, use it
        execute(items.code);
    else // No cached version yet. Load from extension
        get(chrome.extension.getURL('ga.js'), execute);
});

// Typically run within a few milliseconds
function execute(code) {
    try { window.eval(code); } catch (e) { console.error(e); }
    // Run the rest of your code.
    // If your extension depends on GA, initialize it from here.
    // ...
}

function get(url, callback) {
    var x = new XMLHttpRequest();
    x.onload = x.onerror = function() { callback(x.responseText); };
    x.open('GET', url);
    x.send();
}

Minimum manifest file:

{
  "name": "Website Safety",
  "version": "1.0",
  "manifest_version": 2,
  "permissions": [
    "tabs", "http://*/*", "https://*/*"
  ],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["activate-ga.js"]
    }
  ],
  "web_accessible_resources": ["ga.js"]
}

The same method can be used for other trackers. The minimum permission requirements:

  • https://ssl.google-analytics.com/ga.js, so that should be added at the permissions section. https://*/* or <all_urls> is also sufficient.
  • optional: unlimitedStorage - If you want to store a large piece of data with chrome.storage.
Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • 3
    I'm sorry but I have to downvote this because the suggested method locks him in to a static version of ga.js. It prevents him from getting updates and for this reason it should be served by Google. See the official recommendation: http://support.google.com/googleanalytics/bin/answer.py?hl=en&answer=55466 – gblazex Apr 29 '12 at 23:50
  • 3
    @galambalazs Updated my answer to include that feature. – Rob W Apr 30 '12 at 09:32
  • Hi, that sounds brilliant but the problem I am facing is that it is still not tracking the pages that the user is browsing. It is unable to post any data back to google's servers. Can you please help me figure what may I have done wrong. I have used the code you have given me. BTW, thanks for helping me brush up my knowledge on this topic as although it may seem that it is easy to understand from google's documentation, I didn't really find it that way. I am upvoting this, gonna give a few of tries to fix what i might have got wrong. – Vish May 01 '12 at 02:08
  • Got it working, however the strange thing is that the google analytics is not tracking the user on which websites are visited and other data about where the user went on the web. Also, is there a way to know when the user opens/closes a tab or a window? – Vish May 01 '12 at 03:41
  • @user658911 Bind an event listener to [`chrome.tabs.onRemoved`](http://code.google.com/chrome/extensions/tabs.html#event-onRemoved). Regarding the tracking: My demo works well. Do you have any other background scripts which binds an event listener to `chrome.extension.onRequest`? – Rob W May 01 '12 at 07:05
  • nothing else is bound to chrome.extension.onRequest, also is there a way to access chrome.tabs.onRemoved from the contentscript? – Vish May 01 '12 at 19:59
  • @user658911 `chrome.tabs.onRemoved` is [not accessible from the content script](http://code.google.com/chrome/extensions/extension.html#content%20scripts). Are you sure that no request has been send? Check the Devtool's Networks panel of the page **and** background. Also check the console for any errors (do you have any conflicting code/extensions?). If you're still stuck, publish relevant code. If applicable, also say which Chrome version you're using. – Rob W May 01 '12 at 20:09
  • I think the problem was with google's servers and it is now fixed. thank you very much. You were brilliant in answering my questions. If not bothering you too much, is there a way to message you directly to help me develop my extension (I will pay)! :) – Vish May 01 '12 at 22:24
  • @RobW super impressed by this answer again. Maybe stupid, but I am trying to push tracking events from content script to background script via chrome.extension.sendRequest() This means I need to have a custom handler in background.js for all kinds of tracking. Any other downsides? – Cilvic Aug 04 '12 at 22:15
  • @Cilvic Don't place the tracker in the background page, because the tracker reads many page-specific variables. There's a deleted answer on this question with the same comment as yours. See [this screenshot](http://i.stack.imgur.com/idvaH.png), the (links point to https://ssl.google-analytics.com/ga.js, http://jsbeautifier.org/ and http://pastebin.com/WQxqR3Z3). – Rob W Aug 04 '12 at 22:46
  • There seems to be a mistake in the activate-ga.js example, the line if (Date.now() - items.lastUpdated > UODATE_INTERVAL), should be UPDATE_INTERVAL not UODATE_INTERVAL – TheGeneral Jan 03 '14 at 08:53
  • By "inject your code using this method" I assume you mean "Inject embedded code" option from the linked answer? – rogerdpack Oct 17 '16 at 23:09
  • @rogerdpack Any of the methods from that answer. They all lead to the same result: Some code from the extension is run in the context of the web page. – Rob W Oct 17 '16 at 23:40
6

2015 Update

The new Universal Analytics snippet can definitely handle multiple trackers, so assuming you give yours a unique name and run all Analytics code in the page's context, you should be good to go.

// add Universal Analytics to the page (could be made conditional)
runFunctionInPageContext(function () {
  (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);
  a.async=1;a.src=g;document.documentElement.appendChild(a)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
});

// all Google Analytics calls should use our tracker name  
// and be run inside the page's context
runFunctionInPageContext(function () {
  ga('create', 'UA-12345-6', 'auto', {'name': 'myTracker'});
  ga('myTracker.send', 'pageview'); // note the prefix
});

// simple helper function
function runFunctionInPageContext(fn) {
  var script = document.createElement('script');
  script.textContent = '(' + fn.toString() + '());';
  document.documentElement.appendChild(script);
  document.documentElement.removeChild(script);
}

Note, there's one slight modification in the analytics snippet to use document.documentElement instead of the first <script> element. It is because google assumes you add analytics in an inline script block, whereas here you add it from a content script.

gblazex
  • 49,155
  • 12
  • 98
  • 91
5

I read through this thread: https://groups.google.com/a/chromium.org/forum/#!topic/chromium-extensions/yc-ouDqfMw0 and found that there's an official chrome method for adding the analytics script to the page, and it's not in the official docs.

you can refer to this extension for reference: https://github.com/GoogleChrome/chrome-app-samples/tree/master/samples/analytics and it uses this lib: https://github.com/GoogleChrome/chrome-platform-analytics

basically you manually include the script locally:

  <script src="your_path/google-analytics-bundle.js"></script>
  <script src="main.js"></script>

then you call some library functions:

var service, tracker, out;

function initAnalyticsConfig(config) {
  document.getElementById('settings-loading').hidden = true;
  document.getElementById('settings-loaded').hidden = false;

  var checkbox = document.getElementById('analytics');
  checkbox.checked = config.isTrackingPermitted();
  checkbox.onchange = function() {
    config.setTrackingPermitted(checkbox.checked);
  };
}

note: apparently, you have to have an opt out function https://github.com/GoogleChrome/chrome-platform-analytics/wiki#add-privacy-support-aka-opt-out

function startApp() {
  // Initialize the Analytics service object with the name of your app.
  service = analytics.getService('ice_cream_app');
  service.getConfig().addCallback(initAnalyticsConfig);

  // Get a Tracker using your Google Analytics app Tracking ID.
  tracker = service.getTracker('UA-XXXXX-X');

  // Record an "appView" each time the user launches your app or goes to a new
  // screen within the app.
  tracker.sendAppView('MainView');
}

window.onload = startApp;
Greg Sadetsky
  • 4,863
  • 1
  • 38
  • 48
awongh
  • 1,308
  • 12
  • 21
  • This was the best answer I have found yet. It was so simple to get working with the help of this bundle that you shared. Thank you @awongh – frosty Dec 15 '16 at 07:38