diff --git a/.gitignore b/.gitignore index e895f15ed..7030c1061 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 1b63605bb..c5ce01fe1 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 201d3817d..000000000 --- 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 af031e150..000000000 --- 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 ae97513a8..ca6df08b9 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 2258d133e..0919aa7f2 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 000000000..6b53dacd2 --- /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 000000000..ff852c519 --- /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 000000000..93e1c6214 --- /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 000000000..459904734 --- /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 000000000..bdeded53d --- /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);