diff --git a/lib/plausible_web/plugs/tracker.ex b/lib/plausible_web/plugs/tracker.ex index 04e2b69f7742d80777144271483b231cd73b526f..b02d5976d2b0ccb2f60cebb976c9895281d5accd 100644 --- a/lib/plausible_web/plugs/tracker.ex +++ b/lib/plausible_web/plugs/tracker.ex @@ -2,30 +2,41 @@ defmodule PlausibleWeb.Tracker do import Plug.Conn use Agent - @templates [ - "plausible.js", - "plausible.exclusions.js", - "plausible.hash.js", - "plausible.outbound-links.js", - "plausible.hash.exclusions.js", - "plausible.hash.outbound-links.js", - "plausible.hash.exclusions.outbound-links.js", - "plausible.exclusions.outbound-links.js", - "p.js" - ] - @aliases %{ - "plausible.js" => ["analytics.js"], - "plausible.hash.outbound-links.js" => ["plausible.outbound-links.hash.js"], - "plausible.hash.exclusions.js" => ["plausible.exclusions.hash.js"], - "plausible.exclusions.outbound-links.js" => ["plausible.outbound-links.exclusions.js"], - "plausible.hash.exclusions.outbound-links.js" => [ - "plausible.exclusions.hash.outbound-links.js", - "plausible.exclusions.outbound-links.hash.js", - "plausible.hash.outbound-links.exclusions.js", - "plausible.outbound-links.hash.exclusions.js", - "plausible.outbound-links.exclusions.hash.js" - ] - } + base_variants = ["hash", "outbound-links", "exclusions"] + + # Generates Power Set of all variants + variants = + 1..Enum.count(base_variants) + |> Enum.map(fn x -> + Combination.combine(base_variants, x) + |> Enum.map(fn y -> Enum.sort(y) |> Enum.join(".") end) + end) + |> List.flatten() + + # Formats power set into filenames + files_available = + ["plausible.js", "p.js"] ++ Enum.map(variants, fn v -> "plausible.#{v}.js" end) + + # Computes permutations for every power set elements, formats them as alias filenames + aliases_available = + Enum.map(variants, fn x -> + variants = + String.split(x, ".") + |> Combination.permutate() + |> Enum.map(fn p -> Enum.join(p, ".") end) + |> Enum.filter(fn permutation -> permutation != x end) + |> Enum.map(fn v -> "plausible.#{v}.js" end) + + if Enum.count(variants) > 0 do + {"plausible.#{x}.js", variants} + end + end) + |> Enum.reject(fn x -> x == nil end) + |> Enum.into(%{}) + |> Map.put("plausible.js", ["analytics.js"]) + + @templates files_available + @aliases aliases_available # 1 hour @max_age 3600 diff --git a/mix.exs b/mix.exs index f6e3b9aef38fc9bad545bb80601dfcbfe7bbd2fb..932ad9369c7117c6cdae89da272afc3aa22c94d1 100644 --- a/mix.exs +++ b/mix.exs @@ -51,6 +51,7 @@ defmodule Plausible.MixProject do defp deps do [ {:bcrypt_elixir, "~> 2.0"}, + {:combination, "~> 0.0.3"}, {:cors_plug, "~> 1.5"}, {:ecto_sql, "~> 3.0"}, {:elixir_uuid, "~> 1.2"}, diff --git a/mix.lock b/mix.lock index 2abdce41cb6d91cea0c6e5d3941d0c31f558ba8f..73174ccdd931967889293d702d3f05ba56109d21 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, "clickhouse_ecto": {:git, "https://github.com/plausible/clickhouse_ecto.git", "1969f14ecef7c357b2bd8bdc3e566234269de58c", []}, "clickhousex": {:git, "https://github.com/plausible/clickhousex", "0832dd4b1af1f0eba1d1018c231bf0d8d281f031", []}, + "combination": {:hex, :combination, "0.0.3", "746aedca63d833293ec6e835aa1f34974868829b1486b1e1cb0685f0b2ae1f41", [:mix], [], "hexpm", "72b099f463df42ef7dc6371d250c7070b57b6c5902853f69deb894f79eda18ca"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.3.2", "5c2f893d05c56ae3f5e24c1b983c2d5dfb88c6d979c9287a76a7feb1e1d8d646", [:mix], [], "hexpm", "d0993402844c49539aeadb3fe46a3c9bd190f1ecf86b6f9ebd71957534c95f04"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, diff --git a/priv/tracker/js/plausible.exclusions.hash.js b/priv/tracker/js/plausible.exclusions.hash.js new file mode 100644 index 0000000000000000000000000000000000000000..efa52f734c97b5b0a35ba081e25ef3549fa8f4b8 --- /dev/null +++ b/priv/tracker/js/plausible.exclusions.hash.js @@ -0,0 +1 @@ +!function(i,o){"use strict";var e,l=i.location,s=i.document,t=s.querySelector('[src*="'+o+'"]'),c=t&&t.getAttribute("data-domain"),p=i.localStorage.plausible_ignore,u=t&&t.getAttribute("data-exclude").split(",");function g(e){console.warn("Ignoring Event: "+e)}function a(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(l.hostname)||"file:"===l.protocol)return g("localhost");if(!(i.phantom||i._phantom||i.__nightmare||i.navigator.webdriver)){if("true"==p)return g("localStorage flag");if(u)for(var a=0;a<u.length;a++)if("pageview"==e&&l.pathname.match(new RegExp("^"+u[a].trim().replace(/\*\*/g,".*").replace(/[^\.]\*/g,"[^\\s/]*")+"/?$")))return g("exclusion rule");var n={};n.n=e,n.u=l.href,n.d=c,n.r=s.referrer||null,n.w=i.innerWidth,t&&t.meta&&(n.m=JSON.stringify(t.meta)),t&&t.props&&(n.p=JSON.stringify(t.props)),n.h=1;var r=new XMLHttpRequest;r.open("POST",o+"/api/event",!0),r.setRequestHeader("Content-Type","text/plain"),r.send(JSON.stringify(n)),r.onreadystatechange=function(){4==r.readyState&&t&&t.callback&&t.callback()}}}function n(){e=l.pathname,a("pageview")}try{i.addEventListener("hashchange",n);var r=i.plausible&&i.plausible.q||[];i.plausible=a;for(var h=0;h<r.length;h++)a.apply(this,r[h]);"prerender"===s.visibilityState?s.addEventListener("visibilitychange",function(){e||"visible"!==s.visibilityState||n()}):n()}catch(e){console.error(e),(new Image).src=o+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>"); \ No newline at end of file diff --git a/priv/tracker/js/plausible.exclusions.hash.outbound-links.js b/priv/tracker/js/plausible.exclusions.hash.outbound-links.js new file mode 100644 index 0000000000000000000000000000000000000000..3f854ae4a34a903e5743e871fd1673072d5b5d3a --- /dev/null +++ b/priv/tracker/js/plausible.exclusions.hash.outbound-links.js @@ -0,0 +1 @@ +!function(i,o){"use strict";var e,l=i.location,s=i.document,t=s.querySelector('[src*="'+o+'"]'),c=t&&t.getAttribute("data-domain"),p=i.localStorage.plausible_ignore,u=t&&t.getAttribute("data-exclude").split(",");function h(e){console.warn("Ignoring Event: "+e)}function a(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(l.hostname)||"file:"===l.protocol)return h("localhost");if(!(i.phantom||i._phantom||i.__nightmare||i.navigator.webdriver)){if("true"==p)return h("localStorage flag");if(u)for(var a=0;a<u.length;a++)if("pageview"==e&&l.pathname.match(new RegExp("^"+u[a].trim().replace(/\*\*/g,".*").replace(/[^\.]\*/g,"[^\\s/]*")+"/?$")))return h("exclusion rule");var r={};r.n=e,r.u=l.href,r.d=c,r.r=s.referrer||null,r.w=i.innerWidth,t&&t.meta&&(r.m=JSON.stringify(t.meta)),t&&t.props&&(r.p=JSON.stringify(t.props)),r.h=1;var n=new XMLHttpRequest;n.open("POST",o+"/api/event",!0),n.setRequestHeader("Content-Type","text/plain"),n.send(JSON.stringify(r)),n.onreadystatechange=function(){4==n.readyState&&t&&t.callback&&t.callback()}}}function r(){e=l.pathname,a("pageview")}function n(e){for(var t=e.target,a="auxclick"==e.type&&2==e.which,r="click"==e.type;t&&(void 0===t.tagName||"a"!=t.tagName.toLowerCase()||!t.href);)t=t.parentNode;t&&t.href&&t.host&&t.host!==l.host&&((a||r)&&plausible("Outbound Link: Click",{props:{url:t.href}}),t.target&&!t.target.match(/^_(self|parent|top)$/i)||e.ctrlKey||e.metaKey||e.shiftKey||!r||(setTimeout(function(){l.href=t.href},150),e.preventDefault()))}try{i.addEventListener("hashchange",r),s.addEventListener("click",n),s.addEventListener("auxclick",n);var f=i.plausible&&i.plausible.q||[];i.plausible=a;for(var g=0;g<f.length;g++)a.apply(this,f[g]);"prerender"===s.visibilityState?s.addEventListener("visibilitychange",function(){e||"visible"!==s.visibilityState||r()}):r()}catch(e){console.error(e),(new Image).src=o+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>"); \ No newline at end of file diff --git a/tracker/compile.js b/tracker/compile.js index e33ba321d862eaee8d333a2684ce5cb2604fa5b8..7f67804f4b12b89d102a76fbb43040f2f7623af2 100644 --- a/tracker/compile.js +++ b/tracker/compile.js @@ -2,6 +2,7 @@ const uglify = require("uglify-js"); const fs = require('fs') const path = require('path') const Handlebars = require("handlebars"); +const g = require("generatorics"); function relPath(segment) { return path.join(__dirname, segment) @@ -15,13 +16,14 @@ function compilefile(input, output, templateVars = {}) { fs.writeFileSync(output, result.code) } +const base_variants = ["hash", "outbound-links", "exclusions"] +const variants = [...g.clone.powerSet(base_variants)].filter(a => a.length > 0).map(a => a.sort()); + compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.js')) -compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.exclusions.js'), {exclusionMode: true}) -compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.hash.js'), {hashMode: true}) -compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.hash.exclusions.js'), {hashMode: true, exclusionMode: true}) -compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.outbound-links.js'), {outboundLinks: true}) -compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.exclusions.outbound-links.js'), {outboundLinks: true, exclusionMode: true}) -compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.hash.outbound-links.js'), {hashMode: true, outboundLinks: true}) -compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.hash.exclusions.outbound-links.js'), {hashMode: true, outboundLinks: true, exclusionMode: true}) +compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/analytics.js')) 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')) + +variants.map(variant => { + const options = variant.map(variant => variant.replace('-', '_')).reduce((acc, curr) => (acc[curr] = true, acc), {}) + compilefile(relPath('src/plausible.js'), relPath(`../priv/tracker/js/plausible.${variant.join('.')}.js`), options) +}) diff --git a/tracker/package-lock.json b/tracker/package-lock.json index cb50639fb8af402e63bd4cc7c65f435bbbf7ff11..f75c6ff886caf3bc2930ee947085d5fac368f511 100644 --- a/tracker/package-lock.json +++ b/tracker/package-lock.json @@ -6,6 +6,7 @@ "": { "license": "MIT", "dependencies": { + "generatorics": "^1.1.0", "handlebars": "^4.7.7", "uglify-js": "^3.9.4" } @@ -15,6 +16,14 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/generatorics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/generatorics/-/generatorics-1.1.0.tgz", + "integrity": "sha1-aVBgu42IuQmzAXGlyz1CR2hmETg=", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/handlebars": { "version": "4.7.7", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", @@ -79,6 +88,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "generatorics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/generatorics/-/generatorics-1.1.0.tgz", + "integrity": "sha1-aVBgu42IuQmzAXGlyz1CR2hmETg=" + }, "handlebars": { "version": "4.7.7", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", diff --git a/tracker/package.json b/tracker/package.json index 46c3a9c601631d7667a04d2bea3d114413a809a9..88ce3cb434d036f26e394db91217496ddd0e6c82 100644 --- a/tracker/package.json +++ b/tracker/package.json @@ -4,6 +4,7 @@ }, "license": "MIT", "dependencies": { + "generatorics": "^1.1.0", "handlebars": "^4.7.7", "uglify-js": "^3.9.4" } diff --git a/tracker/src/plausible.js b/tracker/src/plausible.js index aff85ede580c674d98f6ecefec370e96e3017b39..ffb6fa64430965b9f00fb45c7d7b1850a714a860 100644 --- a/tracker/src/plausible.js +++ b/tracker/src/plausible.js @@ -7,7 +7,7 @@ var scriptEl = document.querySelector('[src*="' + plausibleHost +'"]') var domain = scriptEl && scriptEl.getAttribute('data-domain') var plausible_ignore = window.localStorage.plausible_ignore; - {{#if exclusionMode}} + {{#if exclusions}} var excludedPaths = scriptEl && scriptEl.getAttribute('data-exclude').split(','); {{/if}} var lastPage; @@ -20,7 +20,7 @@ if (/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(location.hostname) || location.protocol === 'file:') return warn('localhost'); if (window.phantom || window._phantom || window.__nightmare || window.navigator.webdriver || window.Cypress) return; if (plausible_ignore=="true") return warn('localStorage flag') - {{#if exclusionMode}} + {{#if exclusions}} if (excludedPaths) for (var i = 0; i < excludedPaths.length; i++) if (eventName == "pageview" && location.pathname.match(new RegExp('^' + excludedPaths[i].trim().replace(/\*\*/g, '.*').replace(/([^\.])\*/g, '$1[^\\s\/]*') + '\/?$'))) @@ -39,7 +39,7 @@ if (options && options.props) { payload.p = JSON.stringify(options.props) } - {{#if hashMode}} + {{#if hash}} payload.h = 1 {{/if}} @@ -57,7 +57,7 @@ } function page() { - {{#unless hashMode}} + {{#unless hash}} if (lastPage === location.pathname) return; {{/unless}} lastPage = location.pathname @@ -70,7 +70,7 @@ } } - {{#if outboundLinks}} + {{#if outbound_links}} function handleOutbound(event) { var link = event.target; var middle = event.type == "auxclick" && event.which == 2; @@ -102,7 +102,7 @@ {{/if}} try { - {{#if hashMode}} + {{#if hash}} window.addEventListener('hashchange', page) {{else}} var his = window.history @@ -116,7 +116,7 @@ } {{/if}} - {{#if outboundLinks}} + {{#if outbound_links}} registerOutboundLinkEvents() {{/if}}