Add goatcounter as analytics source

This commit is contained in:
Gabriel Augendre 2021-01-04 21:32:03 +01:00
parent 4229060233
commit 2bbdef77a3
No known key found for this signature in database
GPG key ID: 1E693F4CE4AEE7B4
8 changed files with 296 additions and 11 deletions

View file

@ -34,8 +34,11 @@ def git_version(request):
return {"git_version": version, "git_version_url": url}
def plausible(request):
return {"plausible_domain": settings.PLAUSIBLE_DOMAIN}
def analytics(request):
return {
"plausible_domain": settings.PLAUSIBLE_DOMAIN,
"goatcounter_domain": settings.GOATCOUNTER_DOMAIN,
}
def open_graph_image_url(request):

View file

@ -1,10 +1,12 @@
'use strict';
// Explicitely not using ES 6 features because the compressor doesn't support them.
window.onload = function () {
const adminLinkElement = document.querySelector("a#admin-link");
var adminLinkElement = document.querySelector("a#admin-link");
if (adminLinkElement === undefined || adminLinkElement === null) {
return;
}
const adminLocation = adminLinkElement.href;
document.addEventListener("keydown", event => {
var adminLocation = adminLinkElement.href;
document.addEventListener("keydown", function(event) {
if (event.code === "KeyE") {
window.location = adminLocation;
}

233
articles/static/vendor/goatcounter.js vendored Normal file
View file

@ -0,0 +1,233 @@
// GoatCounter: https://www.goatcounter.com
// This file (and *only* this file) is released under the ISC license:
// https://opensource.org/licenses/ISC
(function() {
'use strict';
if (window.goatcounter && window.goatcounter.vars) // Compatibility
window.goatcounter = window.goatcounter.vars
else
window.goatcounter = window.goatcounter || {}
// Get all data we're going to send off to the counter endpoint.
var get_data = function(vars) {
var data = {
p: (vars.path === undefined ? goatcounter.path : vars.path),
r: (vars.referrer === undefined ? goatcounter.referrer : vars.referrer),
t: (vars.title === undefined ? goatcounter.title : vars.title),
e: !!(vars.event || goatcounter.event),
s: [window.screen.width, window.screen.height, (window.devicePixelRatio || 1)],
b: is_bot(),
q: location.search,
}
var rcb, pcb, tcb // Save callbacks to apply later.
if (typeof(data.r) === 'function') rcb = data.r
if (typeof(data.t) === 'function') tcb = data.t
if (typeof(data.p) === 'function') pcb = data.p
if (is_empty(data.r)) data.r = document.referrer
if (is_empty(data.t)) data.t = document.title
if (is_empty(data.p)) data.p = get_path()
if (rcb) data.r = rcb(data.r)
if (tcb) data.t = tcb(data.t)
if (pcb) data.p = pcb(data.p)
return data
}
// Check if a value is "empty" for the purpose of get_data().
var is_empty = function(v) { return v === null || v === undefined || typeof(v) === 'function' }
// See if this looks like a bot; there is some additional filtering on the
// backend, but these properties can't be fetched from there.
var is_bot = function() {
// Headless browsers are probably a bot.
var w = window, d = document
if (w.callPhantom || w._phantom || w.phantom)
return 150
if (w.__nightmare)
return 151
if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate)
return 152
if (navigator.webdriver)
return 153
return 0
}
// Object to urlencoded string, starting with a ?.
var urlencode = function(obj) {
var p = []
for (var k in obj)
if (obj[k] !== '' && obj[k] !== null && obj[k] !== undefined && obj[k] !== false)
p.push(encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]))
return '?' + p.join('&')
}
// Show a warning in the console.
var warn = function(msg) {
if (console && 'warn' in console)
console.warn('goatcounter: ' + msg)
}
// Get the endpoint to send requests to.
var get_endpoint = function() {
var s = document.querySelector('script[data-goatcounter]');
if (s && s.dataset.goatcounter)
return s.dataset.goatcounter
return (goatcounter.endpoint || window.counter) // counter is for compat; don't use.
}
// Get current path.
var get_path = function() {
var loc = location,
c = document.querySelector('link[rel="canonical"][href]')
if (c) { // May be relative or point to different domain.
var a = document.createElement('a')
a.href = c.href
if (a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, ''))
loc = a
}
return (loc.pathname + loc.search) || '/'
}
// Filter some requests that we (probably) don't want to count.
goatcounter.filter = function() {
if ('visibilityState' in document && (document.visibilityState === 'prerender' || document.visibilityState === 'hidden'))
return 'visibilityState'
if (!goatcounter.allow_frame && location !== parent.location)
return 'frame'
if (!goatcounter.allow_local && location.hostname.match(/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.)/))
return 'localhost'
if (!goatcounter.allow_local && location.protocol === 'file:')
return 'localfile'
if (localStorage && localStorage.getItem('skipgc') === 't')
return 'disabled with #toggle-goatcounter'
return false
}
// Get URL to send to GoatCounter.
window.goatcounter.url = function(vars) {
var data = get_data(vars || {})
if (data.p === null) // null from user callback.
return
data.rnd = Math.random().toString(36).substr(2, 5) // Browsers don't always listen to Cache-Control.
var endpoint = get_endpoint()
if (!endpoint)
return warn('no endpoint found')
return endpoint + urlencode(data)
}
// Count a hit.
window.goatcounter.count = function(vars) {
var f = goatcounter.filter()
if (f)
return warn('not counting because of: ' + f)
var url = goatcounter.url(vars)
if (!url)
return warn('not counting because path callback returned null')
var img = document.createElement('img')
img.src = url
img.style.position = 'absolute' // Affect layout less.
img.setAttribute('alt', '')
img.setAttribute('aria-hidden', 'true')
var rm = function() { if (img && img.parentNode) img.parentNode.removeChild(img) }
setTimeout(rm, 3000) // In case the onload isn't triggered.
img.addEventListener('load', rm, false)
document.body.appendChild(img)
}
// Get a query parameter.
window.goatcounter.get_query = function(name) {
var s = location.search.substr(1).split('&')
for (var i = 0; i < s.length; i++)
if (s[i].toLowerCase().indexOf(name.toLowerCase() + '=') === 0)
return s[i].substr(name.length + 1)
}
// Track click events.
window.goatcounter.bind_events = function() {
if (!document.querySelectorAll) // Just in case someone uses an ancient browser.
return
var send = function(elem) {
return function() {
goatcounter.count({
event: true,
path: (elem.dataset.goatcounterClick || elem.name || elem.id || ''),
title: (elem.dataset.goatcounterTitle || elem.title || (elem.innerHTML || '').substr(0, 200) || ''),
referrer: (elem.dataset.goatcounterReferrer || elem.dataset.goatcounterReferral || ''),
})
}
}
Array.prototype.slice.call(document.querySelectorAll("*[data-goatcounter-click]")).forEach(function(elem) {
if (elem.dataset.goatcounterBound)
return
var f = send(elem)
elem.addEventListener('click', f, false)
elem.addEventListener('auxclick', f, false) // Middle click.
elem.dataset.goatcounterBound = 'true'
})
}
// Add a "visitor counter" frame or image.
window.goatcounter.visit_count = function(opt) {
opt = opt || {}
opt.type = opt.type || 'html'
opt.append = opt.append || 'body'
opt.path = opt.path || get_path()
opt.attr = opt.attr || {width: '200', height: (opt.no_branding ? '60' : '80')}
opt.attr['src'] = get_endpoint() + 'er/' + encodeURIComponent(opt.path) + '.' + opt.type + '?'
if (opt.no_branding) opt.attr['src'] += '&no_branding=1'
if (opt.style) opt.attr['src'] += '&style=' + encodeURIComponent(opt.style)
var tag = {png: 'img', svg: 'img', html: 'iframe'}[opt.type]
if (!tag)
return warn('visit_count: unknown type: ' + opt.type)
if (opt.type === 'html') {
opt.attr['frameborder'] = '0'
opt.attr['scrolling'] = 'no'
}
var d = document.createElement(tag)
for (var k in opt.attr)
d.setAttribute(k, opt.attr[k])
var p = document.querySelector(opt.append)
if (!p)
return warn('visit_count: append not found: ' + opt.append)
p.appendChild(d)
}
// Make it easy to skip your own views.
if (location.hash === '#toggle-goatcounter')
if (localStorage.getItem('skipgc') === 't') {
localStorage.removeItem('skipgc', 't')
alert('GoatCounter tracking is now ENABLED in this browser.')
}
else {
localStorage.setItem('skipgc', 't')
alert('GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.')
}
if (!goatcounter.no_onload) {
var go = function() {
goatcounter.count()
if (!goatcounter.no_events)
goatcounter.bind_events()
}
if (document.body === null)
document.addEventListener('DOMContentLoaded', function() { go() }, false)
else
go()
}
})();

View file

@ -32,7 +32,6 @@
{% endcompress %}
{% include "articles/snippets/favicon.html" %}
{% include "articles/snippets/analytics.html" %}
</head>
<body>
<header>
@ -60,6 +59,7 @@
for ongoing builds <a href="{{ blog_pipelines_url }}">here</a>.
</p>
</footer>
{% include "articles/snippets/analytics.html" %}
</body>
{% endspaceless %}
</html>

View file

@ -1,4 +1,19 @@
{% load static compress %}
{% if plausible_domain is not None and not user.is_authenticated %}
<link rel="preconnect" href="https://plausible.augendre.info">
<script async defer data-domain="{{ plausible_domain }}" src="https://plausible.augendre.info/js/plausible.js"></script>
<script async defer data-domain="{{ plausible_domain }}"
src="https://plausible.augendre.info/js/plausible.js">
</script>
{% endif %}
{% compress js inline %}
{% if goatcounter_domain is not None and not user.is_authenticated %}
<script>
window.goatcounter = {
endpoint: 'https://{{ goatcounter_domain }}/count',
allow_local: true,
}
</script>
<script async defer src="{% static "vendor/goatcounter.js" %}"></script>
{% endif %}
{% endcompress %}

View file

@ -99,7 +99,7 @@ TEMPLATES = [
"articles.context_processors.drafts_count",
"articles.context_processors.date_format",
"articles.context_processors.git_version",
"articles.context_processors.plausible",
"articles.context_processors.analytics",
"articles.context_processors.open_graph_image_url",
"articles.context_processors.blog_metadata",
],
@ -191,6 +191,7 @@ SHORTPIXEL_RESIZE_WIDTH = int(os.getenv("SHORTPIXEL_RESIZE_WIDTH", 750))
SHORTPIXEL_RESIZE_HEIGHT = int(os.getenv("SHORTPIXEL_RESIZE_HEIGHT", 10000))
PLAUSIBLE_DOMAIN = os.getenv("PLAUSIBLE_DOMAIN")
GOATCOUNTER_DOMAIN = os.getenv("GOATCOUNTER_DOMAIN")
LOGIN_URL = "admin:login"
@ -203,7 +204,9 @@ COMPRESS_FILTERS = {
"compressor.filters.css_default.CssAbsoluteFilter",
"compressor.filters.cssmin.rCSSMinFilter",
],
"js": ["compressor.filters.jsmin.JSMinFilter"],
"js": [
"compressor.filters.jsmin.CalmjsFilter",
],
}
if DEBUG:
COMPRESS_DEBUG_TOGGLE = "nocompress"

30
poetry.lock generated
View file

@ -54,6 +54,17 @@ soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""}
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]]
name = "calmjs.parse"
version = "1.2.5"
description = "Various parsers for ECMA standards."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
ply = ">=3.6"
[[package]]
name = "certifi"
version = "2020.12.5"
@ -327,6 +338,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
name = "ply"
version = "3.11"
description = "Python Lex & Yacc"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pre-commit"
version = "2.9.3"
@ -606,7 +625,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "0b9ac9fa575f31f30485f510b5da016453e0387bc8805e56ea52f5dc09de095d"
content-hash = "e55cff591236bac6ee7f57a2da6cb48fcf0484f5a1d17a26949c3c724dad0087"
[metadata.files]
appdirs = [
@ -630,6 +649,11 @@ beautifulsoup4 = [
{file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"},
{file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"},
]
"calmjs.parse" = [
{file = "calmjs.parse-1.2.5-py2-none-any.whl", hash = "sha256:dd2c576169e683f0a06b5e8908d7b9d134e756ae75763c21b721d9937b0b422a"},
{file = "calmjs.parse-1.2.5-py3-none-any.whl", hash = "sha256:163ae575c478944c3a54c8e26d5cca794a977c5bf160f6aaa3731b51ae88ab99"},
{file = "calmjs.parse-1.2.5.zip", hash = "sha256:693d18bcfdb14f2a33f64288680adc138275d39de5980fb46f5b882df6e1dbde"},
]
certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
@ -881,6 +905,10 @@ pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
ply = [
{file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"},
{file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"},
]
pre-commit = [
{file = "pre_commit-2.9.3-py2.py3-none-any.whl", hash = "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0"},
{file = "pre_commit-2.9.3.tar.gz", hash = "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4"},

View file

@ -34,6 +34,7 @@ requests = "^2.24"
html2text = "^2020.1.16"
readtime = "^1.1.1"
django-compressor = "^2.4"
"calmjs.parse" = "^1.2.5"
[tool.poetry.dev-dependencies]
pre-commit = "^2.7"