From 132bbcbe0db1ee52227b6d49c195366dce83ac47 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 20 Jul 2020 01:54:21 -0700 Subject: [PATCH] Refactored session and collect process. --- lib/db.js | 26 ++--- lib/session.js | 49 ++++++++++ lib/utils.js | 70 +++----------- pages/api/collect.js | 33 ++++--- pages/api/session.js | 51 ---------- public/umami.js | 2 +- scripts/umami/index.js | 213 ++++++++++++++++++----------------------- 7 files changed, 192 insertions(+), 252 deletions(-) create mode 100644 lib/session.js delete mode 100644 pages/api/session.js diff --git a/lib/db.js b/lib/db.js index 57518b18..7abbe9df 100644 --- a/lib/db.js +++ b/lib/db.js @@ -13,37 +13,39 @@ export async function runQuery(query) { }); } -export async function getWebsite(website_id) { +export async function getWebsite(website_uuid) { return runQuery( prisma.website.findOne({ where: { - website_uuid: website_id, + website_uuid, }, }), ); } -export async function createSession(website_id, session_id, data) { - await runQuery( +export async function createSession(website_id, data) { + return runQuery( prisma.session.create({ data: { - session_uuid: session_id, website: { connect: { - website_uuid: website_id, + website_id, }, }, ...data, }, + select: { + session_id: true, + }, }), ); } -export async function getSession(session_id) { +export async function getSession(session_uuid) { return runQuery( prisma.session.findOne({ where: { - session_uuid: session_id, + session_uuid, }, }), ); @@ -55,12 +57,12 @@ export async function savePageView(website_id, session_id, url, referrer) { data: { website: { connect: { - website_uuid: website_id, + website_id, }, }, session: { connect: { - session_uuid: session_id, + session_id, }, }, url, @@ -76,12 +78,12 @@ export async function saveEvent(website_id, session_id, url, event_type, event_v data: { website: { connect: { - website_uuid: website_id, + website_id, }, }, session: { connect: { - session_uuid: session_id, + session_id, }, }, url, diff --git a/lib/session.js b/lib/session.js new file mode 100644 index 00000000..daafffd5 --- /dev/null +++ b/lib/session.js @@ -0,0 +1,49 @@ +import { getWebsite, getSession, createSession } from 'lib/db'; +import { getCountry, getDevice, getIpAddress, hash, isValidSession } from 'lib/utils'; + +export default async function checkSession(req) { + const { payload } = req.body; + const { session } = payload; + + if (isValidSession(session)) { + return session; + } + + const ip = getIpAddress(req); + const { userAgent, browser, os } = getDevice(req); + const country = await getCountry(req, ip); + const { website: website_uuid, hostname, screen, language } = payload; + + if (website_uuid) { + const website = await getWebsite(website_uuid); + + if (website) { + const { website_id } = website; + const session_uuid = hash(`${website_id}${hostname}${ip}${userAgent}${os}`); + + let session = await getSession(session_uuid); + + if (!session) { + session = await createSession(website_id, { + session_uuid, + hostname, + browser, + os, + screen, + language, + country, + }); + } + + const { session_id } = session; + + return [ + website_id, + website_uuid, + session_id, + session_uuid, + hash(website_id, website_uuid, session_id, session_uuid), + ].join(':'); + } + } +} diff --git a/lib/utils.js b/lib/utils.js index 52f76160..86069443 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -12,8 +12,8 @@ export function md5(s) { return crypto.createHash('md5').update(s).digest('hex'); } -export function hash(s) { - return uuid(s, md5(process.env.HASH_SALT)); +export function hash(...args) { + return uuid(args.join(''), md5(process.env.HASH_SALT)); } export function validHash(s) { @@ -60,61 +60,19 @@ export async function getCountry(req, ip) { return result.country.iso_code; } -export async function parseSessionRequest(req) { - if (req.method === 'POST') { - const ip = getIpAddress(req); - const { website_id, hostname, screen, language } = req.body; - const { userAgent, browser, os } = getDevice(req); - const country = await getCountry(req, ip); - const session_id = hash(`${website_id}${hostname}${ip}${userAgent}${os}`); - - return { - website_id, - session_id, - hostname, - browser, - os, - screen, - language, - country, - }; - } - - return {}; +export function parseSession(session) { + const [website_id, website_uuid, session_id, session_uuid, sig] = (session || '').split(':'); + return { + website_id: parseInt(website_id), + website_uuid, + session_id: parseInt(session_id), + session_uuid, + sig, + }; } -export function parseCollectRequest(req) { - if (req.method === 'POST') { - const { type, payload } = req.body; +export function isValidSession(session) { + const { website_id, website_uuid, session_id, session_uuid, sig } = parseSession(session); - if (payload.session) { - const { - url, - referrer, - event_type, - event_value, - session: { website_id, session_id, time, hash: validationHash }, - } = payload; - - if ( - validHash(website_id) && - validHash(session_id) && - validHash(validationHash) && - hash(`${website_id}${session_id}${time}`) === validationHash - ) { - return { - success: 1, - type, - website_id, - session_id, - url, - referrer, - event_type, - event_value, - }; - } - } - } - - return { success: 0 }; + return hash(website_id, website_uuid, session_id, session_uuid) === sig; } diff --git a/pages/api/collect.js b/pages/api/collect.js index b1573673..1e12c083 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.js @@ -1,25 +1,30 @@ -import { parseCollectRequest } from 'lib/utils'; +import { parseSession } from 'lib/utils'; import { savePageView, saveEvent } from 'lib/db'; import { allowPost } from 'lib/middleware'; +import checkSession from 'lib/session'; export default async (req, res) => { await allowPost(req, res); - const values = parseCollectRequest(req); + const session = await checkSession(req); - if (values.success) { - const { type, website_id, session_id, url, referrer, event_type, event_value } = values; + const { website_id, session_id } = parseSession(session); + const { type, payload } = req.body; + let ok = 1; - if (type === 'pageview') { - await savePageView(website_id, session_id, url, referrer).catch(() => { - values.success = 0; - }); - } else if (type === 'event') { - await saveEvent(website_id, session_id, url, event_type, event_value).catch(() => { - values.success = 0; - }); - } + if (type === 'pageview') { + const { url, referrer } = payload; + await savePageView(website_id, session_id, url, referrer).catch(e => { + ok = 0; + throw e; + }); + } else if (type === 'event') { + const { url, event_type, event_value } = payload; + await saveEvent(website_id, session_id, url, event_type, event_value).catch(() => { + ok = 0; + throw e; + }); } - res.status(200).json({ success: values.success }); + res.status(200).json({ ok, session }); }; diff --git a/pages/api/session.js b/pages/api/session.js deleted file mode 100644 index 801dca85..00000000 --- a/pages/api/session.js +++ /dev/null @@ -1,51 +0,0 @@ -import { getWebsite, getSession, createSession } from 'lib/db'; -import { hash, parseSessionRequest } from 'lib/utils'; -import { allowPost } from 'lib/middleware'; - -export default async (req, res) => { - await allowPost(req, res); - - let result = { success: 0 }; - - const { - website_id, - session_id, - hostname, - browser, - os, - screen, - language, - country, - } = await parseSessionRequest(req); - - if (website_id && session_id) { - const website = await getWebsite(website_id); - - if (website) { - const session = await getSession(session_id); - const time = Date.now(); - - if (!session) { - await createSession(website_id, session_id, { - hostname, - browser, - os, - screen, - language, - country, - }); - } - - result = { - ...result, - success: 1, - session_id, - website_id, - time, - hash: hash(`${website_id}${session_id}${time}`), - }; - } - } - - res.status(200).json(result); -}; diff --git a/public/umami.js b/public/umami.js index 492d02aa..d18ffc28 100644 --- a/public/umami.js +++ b/public/umami.js @@ -1 +1 @@ -!function(){"use strict";function e(e){var n=this.constructor;return this.then((function(t){return n.resolve(e()).then((function(){return t}))}),(function(t){return n.resolve(e()).then((function(){return n.reject(t)}))}))}var n=setTimeout;function t(e){return Boolean(e&&void 0!==e.length)}function r(){}function o(e){if(!(this instanceof o))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],f(e,this)}function i(e,n){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,o._immediateFn((function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null!==t){var r;try{r=t(e._value)}catch(e){return void s(n.promise,e)}u(n.promise,r)}else(1===e._state?u:s)(n.promise,e._value)}))):e._deferreds.push(n)}function u(e,n){try{if(n===e)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var t=n.then;if(n instanceof o)return e._state=3,e._value=n,void a(e);if("function"==typeof t)return void f((r=t,i=n,function(){r.apply(i,arguments)}),e)}e._state=1,e._value=n,a(e)}catch(n){s(e,n)}var r,i}function s(e,n){e._state=2,e._value=n,a(e)}function a(e){2===e._state&&0===e._deferreds.length&&o._immediateFn((function(){e._handled||o._unhandledRejectionFn(e._value)}));for(var n=0,t=e._deferreds.length;n { + const { + screen: { width, height }, + navigator: { language }, + location: { hostname, pathname, search }, + localStorage: store, + document, + history, + } = window; -/* Load script */ + const script = document.querySelector('script[data-website-id]'); + const website = script && script.getAttribute('data-website-id'); + const hostUrl = new URL(script.src).origin; + const screen = `${width}x${height}`; + const listeners = []; -const script = document.querySelector('script[data-website-id]'); + let currentUrl = `${pathname}${search}`; + let currentRef = document.referrer; -if (script) { - const website_id = script.getAttribute('data-website-id'); + /* Helper methods */ - if (website_id) { - const sessionKey = 'umami.session'; - const hostUrl = new URL(script.src).origin; - const screen = `${width}x${height}`; - let currentUrl = `${pathname}${search}`; - let currenrRef = document.referrer; - const listeners = []; + const post = (url, params) => + fetch(url, { + method: 'post', + cache: 'no-cache', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }).then(res => res.json()); - /* Helper methods */ - - const post = (url, params) => - fetch(url, { - method: 'post', - cache: 'no-cache', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(params), - }).then(res => res.json()); - - const createSession = data => - post(`${hostUrl}/api/session`, data).then(({ success, ...session }) => { - if (success) { - store.setItem(sessionKey, JSON.stringify(session)); - return success; - } - }); - - const getSession = () => JSON.parse(store.getItem(sessionKey)); - - const getSessionData = url => ({ website_id, hostname, url, screen, language }); - - const pageView = (url, referrer) => - post(`${hostUrl}/api/collect`, { - type: 'pageview', - payload: { url, referrer, session: getSession() }, - }).then(({ success }) => { - if (!success) { - store.removeItem(sessionKey); - } - return success; - }); - - const trackEvent = (url, event_type, event_value) => - post(`${hostUrl}/api/collect`, { - type: 'event', - payload: { url, event_type, event_value, session: getSession() }, - }); - - const execute = (url, referrer) => { - const data = getSessionData(url); - - if (!store.getItem(sessionKey)) { - createSession(data).then(success => success && pageView(url, referrer)); - } else { - pageView(url, referrer).then( - success => - !success && createSession(data).then(success => success && pageView(url, referrer)), - ); - } + const collect = (type, params) => { + const payload = { + session: store.getItem(sessionKey), + url: currentUrl, + referrer: currentRef, + website, + hostname, + screen, + language, }; - /* Handle push state */ - - const handlePush = (state, title, url) => { - removeEvents(); - currenrRef = currentUrl; - currentUrl = url; - execute(currentUrl, currenrRef); - setTimeout(loadEvents, 300); - }; - - const hook = (type, cb) => { - const orig = history[type]; - return (state, title, url) => { - const args = [state, title, url]; - cb.apply(null, args); - return orig.apply(history, args); - }; - }; - - history.pushState = hook('pushState', handlePush); - history.replaceState = hook('replaceState', handlePush); - - /* Handle events */ - - const removeEvents = () => { - listeners.forEach(([element, type, listener]) => { - element.removeEventListener(type, listener, true); + if (params) { + Object.keys(params).forEach(key => { + payload[key] = params[key]; }); - listeners.length = 0; - }; + } - const loadEvents = () => { - document.querySelectorAll("[class*='umami--']").forEach(element => { - element.className.split(' ').forEach(className => { - if (/^umami--/.test(className)) { - const [, type, value] = className.split('--'); - if (type && value) { - const listener = () => trackEvent(currentUrl, type, value); - listeners.push([element, type, listener]); - element.addEventListener(type, listener, true); - } + return post(`${hostUrl}/api/collect`, { + type, + payload, + }).then(({ ok, session }) => ok && session && store.setItem(sessionKey, session)); + }; + + const pageView = () => collect('pageview').then(() => setTimeout(loadEvents, 300)); + + const pageEvent = (event_type, event_value) => collect('event', { event_type, event_value }); + + /* Handle history */ + + const handlePush = (state, title, url) => { + removeEvents(); + currentRef = currentUrl; + currentUrl = url; + pageView(); + }; + + const hook = (type, cb) => { + const orig = history[type]; + return (state, title, url) => { + const args = [state, title, url]; + cb.apply(null, args); + return orig.apply(history, args); + }; + }; + + history.pushState = hook('pushState', handlePush); + history.replaceState = hook('replaceState', handlePush); + + /* Handle events */ + + const removeEvents = () => { + listeners.forEach(([element, type, listener]) => { + element.removeEventListener(type, listener, true); + }); + listeners.length = 0; + }; + + const loadEvents = () => { + document.querySelectorAll("[class*='umami--']").forEach(element => { + element.className.split(' ').forEach(className => { + if (/^umami--/.test(className)) { + const [, type, value] = className.split('--'); + if (type && value) { + const listener = () => pageEvent(type, value); + listeners.push([element, type, listener]); + element.addEventListener(type, listener, true); } - }); + } }); - }; + }); + }; - /* Start */ + /* Start */ - execute(currentUrl, currenrRef); - loadEvents(); - } -} + pageView(); +})(window, 'umami.session');