Add goatcounter as analytics source
This commit is contained in:
parent
4229060233
commit
2bbdef77a3
8 changed files with 296 additions and 11 deletions
|
@ -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):
|
||||
|
|
|
@ -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
233
articles/static/vendor/goatcounter.js
vendored
Normal 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()
|
||||
}
|
||||
})();
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
30
poetry.lock
generated
|
@ -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"},
|
||||
|
|
|
@ -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"
|
||||
|
|
Reference in a new issue