From 4aa4dfdcafb05fcb176fd5282334e954e8d766f8 Mon Sep 17 00:00:00 2001 From: Uku Taht <Uku.taht@gmail.com> Date: Tue, 25 Aug 2020 10:56:36 +0300 Subject: [PATCH] Hash mode (#299) * Build tracker with hash mode * Extract hash fragment in hash mode * Serve new hash-based tracker --- .../controllers/api/external_controller.ex | 13 +++++++- .../controllers/tracker_controller.ex | 11 +++++++ lib/plausible_web/router.ex | 1 + priv/tracker/js/plausible.hash.js | 1 + .../api/external_controller_test.exs | 17 ++++++++++ tracker/compile.js | 8 +++-- tracker/package-lock.json | 32 +++++++++++++++++++ tracker/package.json | 1 + tracker/src/plausible.js | 12 +++++-- 9 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 priv/tracker/js/plausible.hash.js diff --git a/lib/plausible_web/controllers/api/external_controller.ex b/lib/plausible_web/controllers/api/external_controller.ex index d5d253fe..9c6b870d 100644 --- a/lib/plausible_web/controllers/api/external_controller.ex +++ b/lib/plausible_web/controllers/api/external_controller.ex @@ -61,6 +61,7 @@ defmodule PlausibleWeb.Api.ExternalController do "referrer" => params["r"] || params["referrer"], "domain" => params["d"] || params["domain"], "screen_width" => params["w"] || params["screen_width"], + "hash_mode" => params["h"] || params["hashMode"], } uri = params["url"] && URI.parse(URI.decode(params["url"])) @@ -83,7 +84,7 @@ defmodule PlausibleWeb.Api.ExternalController do name: params["name"], hostname: strip_www(uri && uri.host), domain: strip_www(params["domain"]) || strip_www(uri && uri.host), - pathname: uri && (uri.path || "/"), + pathname: get_pathname(uri, params["hash_mode"]), user_id: generate_user_id(conn, params, salts[:current]), country_code: country_code, operating_system: ua && os_name(ua), @@ -108,6 +109,16 @@ defmodule PlausibleWeb.Api.ExternalController do end end + defp get_pathname(nil, _), do: "/" + defp get_pathname(uri, hash_mode) do + pathname = uri.path || "/" + if hash_mode && uri.fragment do + pathname <> "#" <> uri.fragment + else + pathname + end + end + defp visitor_country(conn) do result = PlausibleWeb.RemoteIp.get(conn) diff --git a/lib/plausible_web/controllers/tracker_controller.ex b/lib/plausible_web/controllers/tracker_controller.ex index 50e3349c..c6464cdb 100644 --- a/lib/plausible_web/controllers/tracker_controller.ex +++ b/lib/plausible_web/controllers/tracker_controller.ex @@ -9,6 +9,13 @@ defmodule PlausibleWeb.TrackerController do [:base_url] ) + EEx.function_from_file( + :defp, + :render_plausible_hash, + Application.app_dir(:plausible, "priv/tracker/js/plausible.hash.js"), + [:base_url] + ) + EEx.function_from_file( :defp, :render_p, @@ -23,6 +30,10 @@ defmodule PlausibleWeb.TrackerController do send_js(conn, render_plausible(base_url())) end + def plausible_hash(conn, _params) do + send_js(conn, render_plausible_hash(base_url())) + end + def analytics(conn, _params) do send_js(conn, render_plausible(base_url())) end diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index e3c52125..3901cc98 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -37,6 +37,7 @@ defmodule PlausibleWeb.Router do end get "/js/plausible.js", PlausibleWeb.TrackerController, :plausible + get "/js/plausible.hash.js", PlausibleWeb.TrackerController, :plausible_hash get "/js/analytics.js", PlausibleWeb.TrackerController, :plausible get "/js/p.js", PlausibleWeb.TrackerController, :p diff --git a/priv/tracker/js/plausible.hash.js b/priv/tracker/js/plausible.hash.js new file mode 100644 index 00000000..34e57c83 --- /dev/null +++ b/priv/tracker/js/plausible.hash.js @@ -0,0 +1 @@ +!function(r,i){"use strict";var o=r.location,s=r.document,e=s.querySelector('[src*="'+i+'"]'),l={domain:e&&e.getAttribute("data-domain")||o.hostname};function c(e){console.warn("[Plausible] Ignore event: "+e)}function t(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(o.hostname)||"file:"===o.protocol)return c("running locally");if("prerender"===s.visibilityState)return c("prerendering");var n={};n.n=e,n.u=o.href,n.d=l.domain,n.r=s.referrer||null,n.w=r.innerWidth,n.h=1;var a=new XMLHttpRequest;a.open("POST",i+"/api/event",!0),a.setRequestHeader("Content-Type","text/plain"),a.send(JSON.stringify(n)),a.onreadystatechange=function(){4==a.readyState&&t&&t.callback&&t.callback()}}function n(){t("pageview")}try{var a,u=r.history;u.pushState&&(a=u.pushState,u.pushState=function(){a.apply(this,arguments),n()},r.addEventListener("popstate",n)),r.addEventListener("hashchange",n);var p=r.plausible&&r.plausible.q||[];r.plausible=t;for(var d=0;d<p.length;d++)t.apply(this,p[d]);n()}catch(e){(new Image).src=i+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>"); \ No newline at end of file diff --git a/test/plausible_web/controllers/api/external_controller_test.exs b/test/plausible_web/controllers/api/external_controller_test.exs index 0e438399..78614332 100644 --- a/test/plausible_web/controllers/api/external_controller_test.exs +++ b/test/plausible_web/controllers/api/external_controller_test.exs @@ -434,6 +434,23 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do assert pageview["screen_size"] == "Mobile" end + test "records hash when in hash mode", %{conn: conn} do + params = %{ + n: "pageview", + u: "http://www.example.com/#page-a", + d: "external-controller-test-23.com", + h: 1 + } + + conn + |> put_req_header("content-type", "text/plain") + |> post("/api/event", Jason.encode!(params)) + + pageview = get_event("external-controller-test-23.com") + + assert pageview["pathname"] == "/#page-a" + end + test "responds 400 when required fields are missing", %{conn: conn} do params = %{} diff --git a/tracker/compile.js b/tracker/compile.js index 9ec48562..dbb7b4fe 100644 --- a/tracker/compile.js +++ b/tracker/compile.js @@ -1,17 +1,21 @@ const uglify = require("uglify-js"); const fs = require('fs') const path = require('path') +const Handlebars = require("handlebars"); function relPath(segment) { return path.join(__dirname, segment) } -function compilefile(input, output) { +function compilefile(input, output, templateVars = {}) { const code = fs.readFileSync(input).toString() - const result = uglify.minify(code) + const template = Handlebars.compile(code) + const rendered = template(templateVars) + const result = uglify.minify(rendered) fs.writeFileSync(output, result.code) } compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.js')) +compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.hash.js'), {hashMode: true}) compilefile(relPath('src/p.js'), relPath('../priv/tracker/js/p.js')) fs.copyFileSync(relPath('../priv/tracker/js/plausible.js'), relPath('../priv/tracker/js/analytics.js')) diff --git a/tracker/package-lock.json b/tracker/package-lock.json index ff852c51..d4d1b5ae 100644 --- a/tracker/package-lock.json +++ b/tracker/package-lock.json @@ -7,6 +7,33 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, "uglify-js": { "version": "3.9.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.4.tgz", @@ -14,6 +41,11 @@ "requires": { "commander": "~2.20.3" } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" } } } diff --git a/tracker/package.json b/tracker/package.json index 93e1c621..b616320e 100644 --- a/tracker/package.json +++ b/tracker/package.json @@ -4,6 +4,7 @@ }, "license": "MIT", "dependencies": { + "handlebars": "^4.7.6", "uglify-js": "^3.9.4" } } diff --git a/tracker/src/plausible.js b/tracker/src/plausible.js index b6def331..dcbbb46f 100644 --- a/tracker/src/plausible.js +++ b/tracker/src/plausible.js @@ -5,8 +5,7 @@ var document = window.document var scriptEl = document.querySelector('[src*="' + plausibleHost +'"]') - var domainAttr = scriptEl && scriptEl.getAttribute('data-domain') - var CONFIG = {domain: domainAttr || location.hostname} + var domain = scriptEl && scriptEl.getAttribute('data-domain') function ignore(reason) { console.warn('[Plausible] Ignore event: ' + reason); @@ -19,9 +18,12 @@ var payload = {} payload.n = eventName payload.u = location.href - payload.d = CONFIG['domain'] + payload.d = domain payload.r = document.referrer || null payload.w = window.innerWidth + {{#if hashMode}} + payload.h = 1 + {{/if}} var request = new XMLHttpRequest(); request.open('POST', plausibleHost + '/api/event', true); @@ -51,6 +53,10 @@ window.addEventListener('popstate', page) } + {{#if hashMode}} + window.addEventListener('hashchange', page) + {{/if}} + var queue = (window.plausible && window.plausible.q) || [] window.plausible = trigger for (var i = 0; i < queue.length; i++) { -- GitLab