diff --git a/.gitignore b/.gitignore
index e895f15ed6bad6aa733a8090fd13a850323701d4..7030c1061f848beda6dc79dfe0e6defd47e64e1e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,7 @@ npm-debug.log
 
 # The directory NPM downloads your dependencies sources to.
 /assets/node_modules/
+/tracker/node_modules/
 
 # Since we are building assets from assets/,
 # we ignore priv/static. You may want to comment
diff --git a/Dockerfile b/Dockerfile
index 1b63605bb090dc5b6e46b4665ed755c0fe5be4d9..c5ce01fe1f6a1e14ca77a4b13e73e5b27040a0c8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -39,6 +39,7 @@ RUN set -x \
 
 COPY config ./config
 COPY assets ./assets
+COPY tracker ./tracker
 COPY priv ./priv
 COPY lib ./lib
 COPY mix.exs ./
@@ -51,6 +52,8 @@ RUN mix local.hex --force && \
 RUN npm audit fix --prefix ./assets && \
     npm install --prefix ./assets && \
     npm run deploy --prefix ./assets && \
+    npm install --prefix ./tracker && \
+    npm run deploy --prefix ./tracker && \
     mix phx.digest priv/static
 
 WORKDIR /app
diff --git a/assets/js/p.js b/assets/js/p.js
deleted file mode 100644
index 201d3817dbddf9cd3e6857045734205d2459e603..0000000000000000000000000000000000000000
--- a/assets/js/p.js
+++ /dev/null
@@ -1,127 +0,0 @@
-(function(window, plausibleHost){
-  'use strict';
-
-  try {
-    const CONFIG = {
-      domain: window.location.hostname
-    }
-
-    function setCookie(name,value) {
-      var date = new Date();
-      date.setTime(date.getTime() + (3*365*24*60*60*1000)); // 3 YEARS
-      var expires = "; expires=" + date.toUTCString();
-      document.cookie = name + "=" + (value || "")  + expires + "; samesite=strict; path=/";
-    }
-
-    function getCookie(name) {
-      let matches = document.cookie.match(new RegExp(
-        "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
-      ));
-      return matches ? decodeURIComponent(matches[1]) : null;
-    }
-
-    function ignore(reason) {
-      console.warn('[Plausible] Ignoring event because ' + reason);
-    }
-
-    function getUrl() {
-      return window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.search;
-    }
-
-    function getSourceFromQueryParam() {
-      const result = window.location.search.match(/[?&](ref|source|utm_source)=([^?&]+)/);
-      return result ? result[2] : null
-    }
-
-    function getUserData() {
-      var userData = JSON.parse(getCookie('plausible_user'))
-
-      if (userData) {
-        return {
-          initial_referrer: userData.initial_referrer && decodeURIComponent(userData.initial_referrer),
-          initial_source: userData.initial_source && decodeURIComponent(userData.initial_source)
-        }
-      } else {
-        userData = {
-          initial_referrer: window.document.referrer || null,
-          initial_source: getSourceFromQueryParam(),
-        }
-
-        setCookie('plausible_user', JSON.stringify({
-          initial_referrer: userData.initial_referrer && encodeURIComponent(userData.initial_referrer),
-          initial_source: userData.initial_source && encodeURIComponent(userData.initial_source),
-        }))
-
-        return userData
-      }
-    }
-
-    function trigger(eventName, options) {
-      if (/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(window.location.hostname)) return ignore('website is running locally');
-      if (window.location.protocol === 'file:') return ignore('website is running locally');
-      if (window.document.visibilityState === 'prerender') return ignore('document is prerendering');
-
-      var payload = CONFIG['trackAcquisition'] ? getUserData() : {}
-      payload.name = eventName
-      payload.url = getUrl()
-      payload.domain = CONFIG['domain']
-      payload.referrer = window.document.referrer || null
-      payload.source = getSourceFromQueryParam()
-      payload.user_agent = window.navigator.userAgent
-      payload.screen_width = window.innerWidth
-
-      var request = new XMLHttpRequest();
-      request.open('POST', plausibleHost + '/api/event', true);
-      request.setRequestHeader('Content-Type', 'text/plain');
-
-      request.send(JSON.stringify(payload));
-
-      request.onreadystatechange = function() {
-        if (request.readyState == XMLHttpRequest.DONE) {
-          options && options.callback && options.callback()
-        }
-      }
-    }
-
-    function page(options) {
-      trigger('pageview', options)
-    }
-
-    function trackPushState() {
-      var his = window.history
-      if (his.pushState) {
-        var originalFn = his['pushState']
-        his.pushState = function() {
-          originalFn.apply(this, arguments)
-          page();
-        }
-      }
-      window.addEventListener('popstate', page)
-    }
-
-    function configure(key, val) {
-      CONFIG[key] = val
-    }
-
-    const functions = {
-      page: page,
-      trigger: trigger,
-      trackPushState: trackPushState,
-      configure: configure
-    }
-
-    const queue = window.plausible.q || []
-
-    window.plausible = function() {
-      var args = [].slice.call(arguments);
-      var funcName = args.shift();
-      functions[funcName].apply(this, args);
-    };
-
-    for (var i = 0; i < queue.length; i++) {
-      window.plausible.apply(this, queue[i])
-    }
-  } catch (e) {
-    new Image().src = plausibleHost + '/api/error?message=' +  encodeURIComponent(e.message);
-  }
-})(window, BASE_URL);
diff --git a/assets/js/plausible.js b/assets/js/plausible.js
deleted file mode 100644
index af031e150e097f05b6840d251671b62055fe29db..0000000000000000000000000000000000000000
--- a/assets/js/plausible.js
+++ /dev/null
@@ -1,115 +0,0 @@
-(function(window, plausibleHost){
-  'use strict';
-
-  try {
-    const scriptEl = window.document.querySelector('[src*="' + plausibleHost +'"]')
-    const domainAttr = scriptEl && scriptEl.getAttribute('data-domain')
-    const trackAcquisitionAttr = scriptEl && scriptEl.getAttribute('data-track-acquisition')
-
-    const CONFIG = {
-      domain: domainAttr || window.location.hostname,
-      trackAcquisition: typeof(trackAcquisitionAttr) === 'string'
-    }
-
-    function setCookie(name,value) {
-      var date = new Date();
-      date.setTime(date.getTime() + (3*365*24*60*60*1000)); // 3 YEARS
-      var expires = "; expires=" + date.toUTCString();
-      document.cookie = name + "=" + (value || "")  + expires + "; samesite=strict; path=/";
-    }
-
-    function getCookie(name) {
-      let matches = document.cookie.match(new RegExp(
-        "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
-      ));
-      return matches ? decodeURIComponent(matches[1]) : null;
-    }
-
-    function ignore(reason) {
-      console.warn('[Plausible] Ignoring event because ' + reason);
-    }
-
-    function getUrl() {
-      return window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.search;
-    }
-
-    function getSourceFromQueryParam() {
-      const result = window.location.search.match(/[?&](ref|source|utm_source)=([^?&]+)/);
-      return result ? result[2] : null
-    }
-
-    function getUserData() {
-      var userData = JSON.parse(getCookie('plausible_user'))
-
-      if (userData) {
-        return {
-          initial_referrer: userData.initial_referrer && decodeURIComponent(userData.initial_referrer),
-          initial_source: userData.initial_source && decodeURIComponent(userData.initial_source)
-        }
-      } else {
-        userData = {
-          initial_referrer: window.document.referrer || null,
-          initial_source: getSourceFromQueryParam(),
-        }
-
-        setCookie('plausible_user', JSON.stringify({
-          initial_referrer: userData.initial_referrer && encodeURIComponent(userData.initial_referrer),
-          initial_source: userData.initial_source && encodeURIComponent(userData.initial_source),
-        }))
-
-        return userData
-      }
-    }
-
-    function trigger(eventName, options) {
-      if (/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(window.location.hostname)) return ignore('website is running locally');
-      if (window.location.protocol === 'file:') return ignore('website is running locally');
-      if (window.document.visibilityState === 'prerender') return ignore('document is prerendering');
-
-      var payload = CONFIG['trackAcquisition'] ? getUserData() : {}
-      payload.name = eventName
-      payload.url = getUrl()
-      payload.domain = CONFIG['domain']
-      payload.referrer = window.document.referrer || null
-      payload.source = getSourceFromQueryParam()
-      payload.user_agent = window.navigator.userAgent
-      payload.screen_width = window.innerWidth
-
-      var request = new XMLHttpRequest();
-      request.open('POST', plausibleHost + '/api/event', true);
-      request.setRequestHeader('Content-Type', 'text/plain');
-
-      request.send(JSON.stringify(payload));
-
-      request.onreadystatechange = function() {
-        if (request.readyState == XMLHttpRequest.DONE) {
-          options && options.callback && options.callback()
-        }
-      }
-    }
-
-    function page() {
-      trigger('pageview')
-    }
-
-    var his = window.history
-    if (his.pushState) {
-      var originalPushState = his['pushState']
-      his.pushState = function() {
-        originalPushState.apply(this, arguments)
-        page();
-      }
-      window.addEventListener('popstate', page)
-    }
-
-    const queue = (window.plausible && window.plausible.q) || []
-    window.plausible = trigger
-    for (var i = 0; i < queue.length; i++) {
-      trigger.apply(this, queue[i])
-    }
-
-    page()
-  } catch (e) {
-    new Image().src = plausibleHost + '/api/error?message=' +  encodeURIComponent(e.message);
-  }
-})(window, BASE_URL);
diff --git a/assets/webpack.config.js b/assets/webpack.config.js
index ae97513a862d47813be407253a72d9bf4f380c38..ca6df08b9bd67585c282ace790f934d09a9f8f92 100644
--- a/assets/webpack.config.js
+++ b/assets/webpack.config.js
@@ -15,10 +15,7 @@ module.exports = (env, options) => ({
   },
   entry: {
       'app': ['./js/app.js'],
-      'dashboard': ['./js/dashboard/mount.js'],
-      'p': ['./js/p.js'],
-      'analytics': ['./js/plausible.js'],
-      'plausible': ['./js/plausible.js']
+      'dashboard': ['./js/dashboard/mount.js']
   },
   output: {
     filename: '[name].js',
diff --git a/compile b/compile
index 2258d133e35ec1708bd12fbb8e38d0226b41275b..0919aa7f2fba88d7dfb6404187affdcc8212565d 100644
--- a/compile
+++ b/compile
@@ -1,4 +1,5 @@
 cd $phoenix_dir
 NODE_ENV=production npm --prefix ./assets run deploy
+npm --prefix ./tracker install && npm --prefix ./tracker run deploy
 mix "${phoenix_ex}.digest"
 mix "${phoenix_ex}.digest.clean"
diff --git a/tracker/compile.js b/tracker/compile.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b53dacd2e535b4137a3e971a1df7098ae3216fc
--- /dev/null
+++ b/tracker/compile.js
@@ -0,0 +1,23 @@
+const uglify = require("uglify-js");
+const fs = require('fs')
+const path = require('path')
+
+const scheme = process.env.SCHEME || "http"
+const host = process.env.HOST || "localhost"
+const baseUrl = scheme + "://" + host
+
+function relPath(segment) {
+  return path.join(__dirname, segment)
+}
+
+function compilefile(input, output) {
+  const code = fs.readFileSync(input)
+    .toString()
+    .replace('BASE_URL', "'" + baseUrl + "'")
+  const result = uglify.minify(code)
+  fs.writeFileSync(output, result.code)
+}
+
+compilefile(relPath('src/plausible.js'), relPath('../priv/static/js/plausible.js'))
+compilefile(relPath('src/p.js'), relPath('../priv/static/js/p.js'))
+fs.copyFileSync(relPath('../priv/static/js/plausible.js'), relPath('../priv/static/js/analytics.js'))
diff --git a/tracker/package-lock.json b/tracker/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..ff852c51907c9d26731f85fc9db8607a75846696
--- /dev/null
+++ b/tracker/package-lock.json
@@ -0,0 +1,19 @@
+{
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+    },
+    "uglify-js": {
+      "version": "3.9.4",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.4.tgz",
+      "integrity": "sha512-8RZBJq5smLOa7KslsNsVcSH+KOXf1uDU8yqLeNuVKwmT0T3FA0ZoXlinQfRad7SDcbZZRZE4ov+2v71EnxNyCA==",
+      "requires": {
+        "commander": "~2.20.3"
+      }
+    }
+  }
+}
diff --git a/tracker/package.json b/tracker/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..93e1c6214539766ec3ee483aa6a30dba1c155005
--- /dev/null
+++ b/tracker/package.json
@@ -0,0 +1,9 @@
+{
+  "scripts": {
+    "deploy": "node compile.js"
+  },
+  "license": "MIT",
+  "dependencies": {
+    "uglify-js": "^3.9.4"
+  }
+}
diff --git a/tracker/src/p.js b/tracker/src/p.js
new file mode 100644
index 0000000000000000000000000000000000000000..459904734df8bcb8a41e1ba78233badd4b693025
--- /dev/null
+++ b/tracker/src/p.js
@@ -0,0 +1,127 @@
+(function(window, plausibleHost){
+  'use strict';
+
+  function setCookie(name,value) {
+    var date = new Date();
+    date.setTime(date.getTime() + (3*365*24*60*60*1000)); // 3 YEARS
+    var expires = "; expires=" + date.toUTCString();
+    document.cookie = name + "=" + (value || "")  + expires + "; samesite=strict; path=/";
+  }
+
+  function getCookie(name) {
+    var matches = document.cookie.match(new RegExp(
+      "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
+    ));
+    return matches ? decodeURIComponent(matches[1]) : null;
+  }
+
+  function ignore(reason) {
+    console.warn('[Plausible] Ignoring event because ' + reason);
+  }
+
+  function getUrl() {
+    return window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.search;
+  }
+
+  function getSourceFromQueryParam() {
+    var result = window.location.search.match(/[?&](ref|source|utm_source)=([^?&]+)/);
+    return result ? result[2] : null
+  }
+
+  function getUserData() {
+    var userData = JSON.parse(getCookie('plausible_user'))
+
+    if (userData) {
+      return {
+        initial_referrer: userData.initial_referrer && decodeURIComponent(userData.initial_referrer),
+        initial_source: userData.initial_source && decodeURIComponent(userData.initial_source)
+      }
+    } else {
+      userData = {
+        initial_referrer: window.document.referrer || null,
+        initial_source: getSourceFromQueryParam(),
+      }
+
+      setCookie('plausible_user', JSON.stringify({
+        initial_referrer: userData.initial_referrer && encodeURIComponent(userData.initial_referrer),
+        initial_source: userData.initial_source && encodeURIComponent(userData.initial_source),
+      }))
+
+      return userData
+    }
+  }
+
+  function trigger(eventName, options) {
+    if (/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(window.location.hostname)) return ignore('website is running locally');
+    if (window.location.protocol === 'file:') return ignore('website is running locally');
+    if (window.document.visibilityState === 'prerender') return ignore('document is prerendering');
+
+    var payload = CONFIG['trackAcquisition'] ? getUserData() : {}
+    payload.name = eventName
+    payload.url = getUrl()
+    payload.domain = CONFIG['domain']
+    payload.referrer = window.document.referrer || null
+    payload.source = getSourceFromQueryParam()
+    payload.user_agent = window.navigator.userAgent
+    payload.screen_width = window.innerWidth
+
+    var request = new XMLHttpRequest();
+    request.open('POST', plausibleHost + '/api/event', true);
+    request.setRequestHeader('Content-Type', 'text/plain');
+
+    request.send(JSON.stringify(payload));
+
+    request.onreadystatechange = function() {
+      if (request.readyState == XMLHttpRequest.DONE) {
+        options && options.callback && options.callback()
+      }
+    }
+  }
+
+  function page(options) {
+    trigger('pageview', options)
+  }
+
+  function trackPushState() {
+    var his = window.history
+    if (his.pushState) {
+      var originalFn = his['pushState']
+      his.pushState = function() {
+        originalFn.apply(this, arguments)
+        page();
+      }
+    }
+    window.addEventListener('popstate', page)
+  }
+
+  function configure(key, val) {
+    CONFIG[key] = val
+  }
+
+  try {
+    var CONFIG = {
+      domain: window.location.hostname
+    }
+
+    var functions = {
+      page: page,
+      trigger: trigger,
+      trackPushState: trackPushState,
+      configure: configure
+    }
+
+    var queue = window.plausible.q || []
+
+    window.plausible = function() {
+      var args = [].slice.call(arguments);
+      var funcName = args.shift();
+      functions[funcName].apply(this, args);
+    };
+
+    for (var i = 0; i < queue.length; i++) {
+      window.plausible.apply(this, queue[i])
+    }
+  } catch (e) {
+    new Image().src = plausibleHost + '/api/error?message=' +  encodeURIComponent(e.message);
+  }
+})(window, "https://plausible.io");
diff --git a/tracker/src/plausible.js b/tracker/src/plausible.js
new file mode 100644
index 0000000000000000000000000000000000000000..bdeded53daff3e8dc7fe49ec863436c754d8fbe3
--- /dev/null
+++ b/tracker/src/plausible.js
@@ -0,0 +1,75 @@
+(function(window, plausibleHost){
+  'use strict';
+
+  var location = window.location
+  var document = window.document
+
+  var scriptEl = document.querySelector('[src*="' + plausibleHost +'"]')
+  var domainAttr = scriptEl && scriptEl.getAttribute('data-domain')
+  var CONFIG = {domain: domainAttr || location.hostname}
+
+  function ignore(reason) {
+    console.warn('[Plausible] Ignore event: ' + reason);
+  }
+
+  function getUrl() {
+    return location.protocol + '//' + location.hostname + location.pathname + location.search;
+  }
+
+  function getSourceFromQueryParam() {
+    var result = location.search.match(/[?&](ref|source|utm_source)=([^?&]+)/);
+    return result ? result[2] : null
+  }
+
+  function trigger(eventName, options) {
+    if (/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(location.hostname) || location.protocol === 'file:') return ignore('running locally');
+    if (document.visibilityState === 'prerender') return ignore('prerendering');
+
+    var payload = {}
+    payload.name = eventName
+    payload.url = getUrl()
+    payload.domain = CONFIG['domain']
+    payload.referrer = document.referrer || null
+    payload.source = getSourceFromQueryParam()
+    payload.user_agent = window.navigator.userAgent
+    payload.screen_width = window.innerWidth
+
+    var request = new XMLHttpRequest();
+    request.open('POST', plausibleHost + '/api/event', true);
+    request.setRequestHeader('Content-Type', 'text/plain');
+
+    request.send(JSON.stringify(payload));
+
+    request.onreadystatechange = function() {
+      if (request.readyState == 4) {
+        options && options.callback && options.callback()
+      }
+    }
+  }
+
+  function page() {
+    trigger('pageview')
+  }
+
+  try {
+    var his = window.history
+    if (his.pushState) {
+      var originalPushState = his['pushState']
+      his.pushState = function() {
+        originalPushState.apply(this, arguments)
+        page();
+      }
+      window.addEventListener('popstate', page)
+    }
+
+    var queue = (window.plausible && window.plausible.q) || []
+    window.plausible = trigger
+    for (var i = 0; i < queue.length; i++) {
+      trigger.apply(this, queue[i])
+    }
+
+    page()
+  } catch (e) {
+    new Image().src = plausibleHost + '/api/error?message=' +  encodeURIComponent(e.message);
+  }
+})(window, BASE_URL);