Add google tag manager

Also supports do not track (DNT) as a means of opt out.

NOTES:
- GMT must be double iframed to not leak the referrer.
- since the page can be CDN cached we need to check DNT client side.
- we do not respect the value of DNT in IE10 since they default it
to on (against the spec).
- GMT is disabled unless the DNT feature flag is enabled.
This commit is contained in:
David Wick
2015-07-29 14:35:22 -07:00
parent e723cad3a1
commit 075b692fca
21 changed files with 535 additions and 166 deletions

View File

@@ -180,6 +180,8 @@ googleanalytics_gold =
googleanalytics_sample_rate_gold = 100
# secret used for signing information on the above tracking pixels
tracking_secret = abcdefghijklmnopqrstuvwxyz0123456789
# google tag manager container id
googletagmanager =
#### Wiki Pages
wiki_page_content_policy = contentpolicy

View File

@@ -170,6 +170,9 @@ def make_map(config):
mc("/newsletter", controller="newsletter", action="newsletter")
mc("/gtm/jail", controller="googletagmanager", action="jail")
mc("/gtm", controller="googletagmanager", action="gtm")
mc('/oembed', controller='oembed', action='oembed')
mc('/about/sidebar', controller='front', action='sidebar')

View File

@@ -69,6 +69,7 @@ def load_controllers():
from awards import AwardsController
from errorlog import ErrorlogController
from newsletter import NewsletterController
from googletagmanager import GoogleTagManagerController
from promotecontroller import PromoteController
from promotecontroller import SponsorController
from promotecontroller import PromoteApiController

View File

@@ -0,0 +1,49 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
###############################################################################
from pylons import app_globals as g
from pylons import tmpl_context as c
from pylons import request
from r2.controllers.reddit_base import MinimalController
from r2.lib.pages import (
GoogleTagManagerJail,
GoogleTagManager,
)
class GoogleTagManagerController(MinimalController):
def pre(self):
if request.host != g.media_domain:
# don't serve up untrusted content except on our
# specifically untrusted domain
self.abort404()
MinimalController.pre(self)
c.allow_framing = True
def GET_jail(self):
return GoogleTagManagerJail().render()
def GET_gtm(self):
return GoogleTagManager().render()

View File

@@ -405,12 +405,29 @@ module = {}
catch_errors = "try {{ {content} }} catch (err) {{ r.sendError('Error running module', '{name}', ':', err.toString()) }}"
module["gtm-jail"] = Module("gtm-jail.js",
"lib/json2.js",
"custom-event.js",
"frames.js",
"google-tag-manager/gtm-jail-listener.js",
)
module["gtm"] = Module("gtm.js",
"lib/json2.js",
"custom-event.js",
"frames.js",
"google-tag-manager/gtm-listener.js",
)
module["reddit-embed-base"] = Module("reddit-embed-base.js",
"lib/es5-shim.js",
"lib/json2.js",
"embed/custom-event.js",
"custom-event.js",
"frames.js",
"embed/utils.js",
"embed/post-message.js",
"embed/pixel-tracking.js",
)
@@ -438,6 +455,7 @@ module["reddit-init-base"] = LocalizedModule("reddit-init-base.js",
"lib/bootstrap.tooltip.js",
"lib/reddit-client-lib.js",
"lib/jquery.cookie.js",
"do-not-track.js",
"bootstrap.tooltip.extension.js",
"base.js",
"hooks.js",
@@ -482,11 +500,12 @@ module["reddit-init"] = LocalizedModule("reddit-init.js",
module["reddit"] = LocalizedModule("reddit.js",
"lib/jquery.url.js",
"lib/backbone-1.0.0.js",
"embed/custom-event.js",
"custom-event.js",
"frames.js",
"embed/utils.js",
"embed/post-message.js",
"embed/pixel-tracking.js",
"embed/comment-embed.js",
"google-tag-manager/gtm.js",
"timings.js",
"templates.js",
"scrollupdater.js",

View File

@@ -269,6 +269,7 @@ class Reddit(Templated):
self.loginbox = loginbox
self.show_sidebar = show_sidebar
self.space_compress = space_compress
self.dnt_enabled = feature.is_enabled("do_not_track")
# instantiate a footer
self.header = header
self.footer = RedditFooter() if footer else None
@@ -5409,6 +5410,14 @@ class PolicyPage(BoringPage):
return toolbars
class GoogleTagManagerJail(Templated):
pass
class GoogleTagManager(Templated):
pass
class Newsletter(BoringPage):
extra_page_classes = ['newsletter']

View File

@@ -0,0 +1,11 @@
!(function(global, undefined) {
var RE_IE_VERSION = /(?:\b(?:MS)?IE\s+|\bTrident\/7\.0;.*\s+rv:)(\d+(?:\.?\d+)?)/i;
var uaMatches = navigator.userAgent.match(RE_IE_VERSION);
var ieVersion = uaMatches && uaMatches[1] && parseFloat(uaMatches[1]);
var doNotTrack = navigator.doNotTrack ||
global.doNotTrack ||
navigator.msDoNotTrack;
global.DO_NOT_TRACK = /^(yes|1)$/i.test(doNotTrack) && ieVersion !== 10;
})(this);

View File

@@ -63,7 +63,8 @@
return;
}
App.addPostMessageOrigin(embed.getAttribute('data-embed-media'));
r.frames.addPostMessageOrigin(embed.getAttribute('data-embed-media'));
r.frames.listen('embed');
iframe.height = iframe.style.height = 0;
iframe.width = iframe.style.width = '100%';
@@ -82,12 +83,12 @@
iframe.style.boxSizing = 'border-box';
iframe.src = getEmbedUrl(commentUrl, embed);
App.receiveMessageOnce(iframe, 'ping', function(e) {
r.frames.receiveMessageOnce(iframe, 'ping.embed', function(e) {
embed.parentNode.removeChild(embed);
iframe.style.display = 'block';
callback(e);
App.postMessage(iframe.contentWindow, 'pong', {
r.frames.postMessage(iframe.contentWindow, 'pong.embed', {
type: embed.getAttribute('data-embed-parent') === 'true' ?
'comment_and_parent' : 'comment',
location: location,
@@ -95,7 +96,7 @@
});
});
var resizer = App.receiveMessage(iframe, 'resize', function(e) {
var resizer = r.frames.receiveMessage(iframe, 'resize.embed', function(e) {
if (!iframe.parentNode) {
resizer.off();
@@ -115,4 +116,4 @@
App.init();
})((window.rembeddit = window.rembeddit || {}), this);
})((window.rembeddit = window.rembeddit || {}), window.r, this);

View File

@@ -1,4 +1,4 @@
;(function(App, window, undefined) {
;(function(App, r, window, undefined) {
App.VERSION = '0.1';
var RE_HOST = /^https?:\/\/([^\/|?]+).*/;
@@ -6,16 +6,18 @@
var thing = config.thing;
if (document.referrer && document.referrer.match(RE_HOST)) {
App.addPostMessageOrigin(RegExp.$1);
r.frames.addPostMessageOrigin(RegExp.$1);
}
r.frames.listen('embed');
function checkHeight() {
var height = document.body.clientHeight;
if (height && App.height !== height) {
App.height = height;
App.postMessage(window.parent, 'resize', height, '*');
r.frames.postMessage(window.parent, 'resize.embed', height, { targetOrigin: '*' });
}
}
@@ -84,7 +86,7 @@
setInterval(checkHeight, 100);
App.receiveMessage(window.parent, 'pong', function(e) {
r.frames.receiveMessage(window.parent, 'pong.embed', function(e) {
var type = e.detail.type;
var options = e.detail.options;
var location = e.detail.location;
@@ -159,8 +161,8 @@
});
App.postMessage(window.parent, 'ping', {
r.frames.postMessage(window.parent, 'ping.embed', {
config: config,
});
})((window.rembeddit = window.rembeddit || {}), this);
})((window.rembeddit = window.rembeddit || {}), window.r, this);

View File

@@ -1,115 +0,0 @@
;(function(App, window, undefined) {
var WILDCARD = '*';
var ALLOW_WILDCARD = '.*';
var RE_WILDCARD = /\*/;
var allowedOrigins = [ALLOW_WILDCARD];
var re_postMessageAllowedOrigin = compileOriginRegExp(allowedOrigins);
function receiveMessage(e) {
if (!re_postMessageAllowedOrigin.test(e.origin) && e.origin !== 'null') {
return;
}
try {
var message = JSON.parse(e.data);
var customEvent = new CustomEvent(message.type, {detail: message.data});
customEvent.source = e.source;
window.dispatchEvent(customEvent);
} catch (x) {}
}
function compileOriginRegExp(origins) {
return new RegExp('^http(s)?:\\/\\/' + origins.join('|'), 'i');
}
function isWildcard(origin) {
return RE_WILDCARD.test(origin);
}
App.utils.extend(App, {
postMessage: function(target, type, data, options) {
type += '.postMessage';
var defaults = {
targetOrigin: WILDCARD,
delay: 100
};
options = App.utils.extend({}, defaults, options);
target.postMessage(JSON.stringify({type: type, data: data}), options.targetOrigin);
},
receiveMessage: function(source, type, callback, context) {
if (typeof source === 'string') {
context = callback;
callback = type;
type = source;
source = null;
}
type += '.postMessage';
context = context || this;
var scoped = function(e) {
if (source &&
source !== e.source &&
source.contentWindow !== e.source) {
return;
}
callback.apply(context, arguments);
}
window.addEventListener(type, scoped);
return {
off: function () { window.removeEventListener(type, scoped); }
};
},
receiveMessageOnce: function(source, type, callback, context) {
var listener = App.receiveMessage(source, type, function() {
callback && callback.apply(this, arguments);
listener.off();
}, context);
return listener;
},
addPostMessageOrigin: function(origin) {
if (isWildcard(origin)) {
allowedOrigins = [ALLOW_WILDCARD];
} else if (allowedOrigins.indexOf(origin) === -1) {
App.removePostMessageOrigin(ALLOW_WILDCARD);
allowedOrigins.push(origin);
re_postMessageAllowedOrigin = compileOriginRegExp(allowedOrigins);
}
},
removePostMessageOrigin: function(origin) {
var index = allowedOrigins.indexOf(origin);
if (index !== -1) {
allowedOrigins.splice(index, 1);
re_postMessageAllowedOrigin = compileOriginRegExp(allowedOrigins);
}
}
});
if ('addEventListener' in window) {
window.addEventListener('message', receiveMessage, false);
} else if ('attachEvent' in window) {
window.attachEvent('onmessage', receiveMessage)
}
})((window.rembeddit = window.rembeddit || {}), this);

View File

@@ -3,40 +3,6 @@
App.utils = App.utils || {};
App.utils.extend = function(obj) {
if (typeof obj !== 'object') {
return obj;
}
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
if (hasOwnProperty.call(source, prop)) {
obj[prop] = source[prop];
}
}
}
return obj;
};
App.utils.find = function(array, test) {
var found;
for (var i = 0; l = array.length, i < l; i++) {
var item = array[i];
if (test(item, i, array)) {
found = item;
break;
}
}
return found;
};
// http://stackoverflow.com/a/8809472/704286
App.utils.uuid = function() {
var d = new Date().getTime();

View File

@@ -0,0 +1,246 @@
;(function(r, global, undefined) {
'use strict';
var ALLOW_WILDCARD = '.*';
var DEFAULT_MESSAGE_NAMESPACE = '.postMessage';
var DEFAULT_POSTMESSAGE_OPTIONS = {
targetOrigin: '*',
};
var allowedOrigins = [ALLOW_WILDCARD];
var re_postMessageAllowedOrigin = compileOriginRegExp(allowedOrigins);
var messageNamespaces = [DEFAULT_MESSAGE_NAMESPACE];
var re_messageNamespaces = compileNamespaceRegExp(messageNamespaces);
var proxies = {};
var listening = false;
function receiveMessage(e) {
if (e.origin !== location.origin &&
!re_postMessageAllowedOrigin.test(e.origin)
&& e.origin !== 'null') {
return;
}
try {
var message = JSON.parse(e.data);
var type = message.type;
// Namespace doesn't match, ignore
if (!re_messageNamespaces.test(type)) {
return;
}
var namespace = type.split('.', 2)[1];
if (proxies[namespace]) {
var proxyWith = proxies[namespace];
r.frames.postMessage(proxyWith.target, type, message.data, message.options);
}
var customEvent = new CustomEvent(type, {detail: message.data});
customEvent.source = e.source;
global.dispatchEvent(customEvent);
} catch (x) {}
}
function _addEventListener(type, handler, useCapture) {
if ('addEventListener' in global) {
global.addEventListener(type, handler, useCapture);
} else if ('attachEvent' in global) {
global.attachEvent('on' + type, handler);
}
}
function _removeEventListener(type, handler, useCapture) {
if ('removeEventListener' in global) {
global.removeEventListener(type, handler);
} else if ('detachEvent' in global) {
global.attachEvent('on' + type, handler);
}
}
function compileOriginRegExp(origins) {
return new RegExp('^http(s)?:\\/\\/' + origins.join('|') + '$', 'i');
}
function compileNamespaceRegExp(namespaces) {
return new RegExp('\\.(?:' + namespaces.join('|') + ')$')
}
function isWildcard(origin) {
return /\*/.test(origin);
}
/** @module frames */
/* @example
* // parent window
* // frames.listen('dfp')
* // frames.receiveMessageOnce('init.dfp', callback)
* @example
* // iframe
* // frames.postMessage(window.parent, 'init.dfp', data);
*/
var frames = r.frames = {
/*
* Send a message to another window.
* param {Window} target The frame to deliver the message to.
* param {String} type The message type. (if it doesn't include a namespace the default namespace will be used)
* param {Object} data The data to send.
* param {Object} options The `postMessage` options.
* param {String} options.targetOrigin Specifies what the origin of otherWindow must be for the event to be dispatched.
*/
postMessage: function (target, type, data, options) {
if (!/\..+$/.test(type)) {
type += DEFAULT_MESSAGE_NAMESPACE;
}
options = options || {};
for (var key in DEFAULT_POSTMESSAGE_OPTIONS) {
if (!options.hasOwnProperty(key)) {
options[key] = DEFAULT_POSTMESSAGE_OPTIONS[key];
}
}
target.postMessage(JSON.stringify({type: type, data: data, options: options}), options.targetOrigin);
},
/*
* Receive a message from another window.
* param {Window} [source] The frame to that send the message.
* param {String} type The message type. (if it doesn't include a namespace the default namespace will be used)
* param {Function} callback The callback to invoke upon retrieval.
* param {Object} [context=this] The context the callback is invoked with.
* returns {Object} The listener.
*/
receiveMessage: function (source, type, callback, context) {
if (typeof source === 'string') {
context = callback;
callback = type;
type = source;
source = null;
}
context = context || this;
var scoped = function(e) {
if (source &&
source !== e.source &&
source.contentWindow !== e.source) {
return;
}
callback.apply(context, arguments);
};
_addEventListener(type, scoped);
return {
off: function() { _removeEventListener(type, scoped); }
};
},
/*
* Proxies messages on a namespace from a frame to a specified target.
* param {String} namespace The namespace to proxy.
* target {Window} [source] The frame to proxy messages to.
*/
proxy: function(namespace, target) {
this.listen(namespace);
proxies[namespace] = {
target: target,
};
},
/*
* Receive a message from another window once.
* param {Window} [source] The frame to that send the message.
* param {String} type The message type. (if it doesn't include a namespace the default namespace will be used)
* param {Function} callback The callback to invoke upon retrieval.
* param {Object} [context=this] The context the callback is invoked with.
* returns {Object} The listener.
*/
receiveMessageOnce: function (source, type, callback, context) {
var listener = frames.receiveMessage(source, type, function() {
callback && callback.apply(this, arguments);
listener.off();
}, context);
return listener;
},
/*
* Adds an allowed origin to be listened to.
* param {String} origin The origin to be added.
*/
addPostMessageOrigin: function (origin) {
if (isWildcard(origin)) {
allowedOrigins = [ALLOW_WILDCARD];
} else if (allowedOrigins.indexOf(origin) === -1) {
frames.removePostMessageOrigin(ALLOW_WILDCARD);
allowedOrigins.push(origin);
re_postMessageAllowedOrigin = compileOriginRegExp(allowedOrigins);
}
},
/*
* Removes an origin from the list of those listened to.
* param {String} origin The origin to be removed.
*/
removePostMessageOrigin: function (origin) {
var index = allowedOrigins.indexOf(origin);
if (index !== -1) {
allowedOrigins.splice(index, 1);
re_postMessageAllowedOrigin = compileOriginRegExp(allowedOrigins);
}
},
/*
* Listens to messages on of the specified namespace.
* param {String} namespace The namespace to be listened to.
*/
listen: function (namespace) {
if (messageNamespaces.indexOf(namespace) === -1) {
messageNamespaces.push(namespace);
re_messageNamespaces = compileNamespaceRegExp(messageNamespaces);
}
if (!listening) {
_addEventListener('message', receiveMessage);
listening = true;
}
},
/*
* Stops listening to messages on of the specified namespace.
* param {String} namespace The namespace to stop listening to.
*/
stopListening: function (namespace) {
var index = messageNamespaces.indexOf(namespace);
if (index !== -1) {
messageNamespaces.splice(index, 1);
if (messageNamespaces.length) {
re_messageNamespaces = compileNamespaceRegExp(messageNamespaces);
} else {
_removeEventListener('message', receiveMessage);
listening = false;
}
}
},
};
})((this.r = this.r || {}), this);

View File

@@ -0,0 +1,5 @@
;(function(global, r, undefined) {
var jail = document.getElementById('jail');
r.frames.proxy('gtm', jail.contentWindow);
})(this, this.r);

View File

@@ -0,0 +1,14 @@
;(function(gtm, global, r, undefined) {
var dataLayer = global.googleTagManager = global.googleTagManager || [];
r.frames.listen('gtm');
r.frames.receiveMessage('data.gtm', function(e) {
dataLayer.push(e.detail);
});
r.frames.receiveMessage('event.gtm', function(e) {
dataLayer.push(e.detail);
});
})((this.gtm = this.gtm || {}), this, this.r);

View File

@@ -0,0 +1,22 @@
;(function(r, global, undefined) {
var jail = document.getElementById('gtm-jail');
r.gtm = {
trigger: function(eventName, payload) {
if (payload) {
this.set(payload);
}
r.frames.postMessage(jail.contentWindow, 'event.gtm', {
event: eventName,
});
},
set: function(data) {
r.frames.postMessage(jail.contentWindow, 'data.gtm', data);
},
};
})((this.r = this.r || {}), this);

View File

@@ -0,0 +1,26 @@
;(function(r, global, undefined) {
var jail = document.getElementById('gtm-sandbox');
r.jail = {
postMessage: function(id, eventName, data) {
},
trigger: function(eventName, payload) {
if (payload) {
this.set(payload);
}
r.frames.postMessage(sandbox.contentWindow, 'event.gtm', {
event: eventName,
});
},
set: function(data) {
r.frames.postMessage(sandbox.contentWindow, 'data.gtm', data);
},
};
})((this.r = this.r || {}), this);

View File

@@ -26,7 +26,7 @@
from r2.models import Link, Comment, Subreddit
from r2.lib import tracking
%>
<%namespace file="utils.html" import="js_setup, googleanalytics, classes"/>
<%namespace file="utils.html" import="js_setup, googleanalytics, googletagmanager, classes"/>
<html xmlns="http://www.w3.org/1999/xhtml" lang="${c.lang}"
xml:lang="${c.lang}">
<head>
@@ -57,6 +57,7 @@
</head>
<body ${classes(*thing.page_classes())}>
${googletagmanager()}
${self.bodyContent()}
${self.javascript_bottom()}
</body>

View File

@@ -0,0 +1,51 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2014
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from r2.lib.filters import scriptsafe_dumps, unsafe
from r2.lib import js
%>
<%namespace file="utils.html" import="googletagmanager"/>
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<meta name="referrer" content="no-referrer">
<title></title>
</head>
<body>
${unsafe(js.use('gtm'))}
<noscript>
<iframe src="//www.googletagmanager.com/ns.html?id=${g.googletagmanager}"
height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<script>
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','googleTagManager',${scriptsafe_dumps(g.googletagmanager)});
</script>
</body>
</html>

View File

@@ -0,0 +1,39 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2014
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from r2.lib.filters import unsafe
from r2.lib import js
%>
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<meta name="referrer" content="no-referrer">
<title></title>
</head>
<body>
<iframe id="jail" src="/gtm" style="display:none;" referrer="no-referrer">
${unsafe(js.use('gtm-jail'))}
</body>
</html>

View File

@@ -25,7 +25,7 @@
from datetime import datetime
from r2.lib import tracking
from r2.lib.filters import spaceCompress, unsafe, safemarkdown, jssafe
from r2.lib.filters import spaceCompress, unsafe, safemarkdown, jssafe, scriptsafe_dumps
from r2.lib.template_helpers import (
add_sr,
html_datetime,
@@ -445,6 +445,23 @@ ${unsafe(txt)}
</script>
</%def>
<%def name="googletagmanager()">
%if g.googletagmanager and thing.site_tracking and thing.dnt_enabled:
<script>
if (!window.DO_NOT_TRACK) {
var frame = document.createElement('iframe');
frame.style.display = 'none';
frame.referrer = 'no-referrer';
frame.id = 'gtm-jail';
frame.src = '//' + ${scriptsafe_dumps(g.media_domain)} + '/gtm/jail';
document.body.appendChild(frame);
}
</script>
%endif
</%def>
<%def name="googleanalytics(uitype, is_gold_page=False)">
%if (g.googleanalytics or g.googleanalytics_gold) and thing.site_tracking:
<script type="text/javascript">