Jacob O'Bryant
03/25/2023, 5:52 AMLogan Rios
03/27/2023, 5:23 PMJacob O'Bryant
03/27/2023, 5:39 PMquery-events
function which takes stuff like that from the database and then returns a collection of "event" documents in the format that the dashboard code expects. e.g. I have a :user/joined-at key on user documents, so in part of the function I load all the users and return something like {:user #uuid "...", :type :signup, :timestamp #inst "..."}
for each one. The app also already records your reading history, which is what I use to count someone as active or not, so I didn't need to add anything to the db for that. For other apps you might need to add stuff though--basically you need to decide what would make a user count as "active" on a particular day, and then figure out if you need to start recording more data to be able to query for that.
For page views (both on the landing page and inside the app) I have a bit of javascript that takes any query params from the url (so it'll include stuff like ref
, utm_source
etc), inserts a few extra things like document.referrer
, and passes it to an endpoint:
{
let params = new URLSearchParams(window.location.search);
if (document.referrer) {
params.set('referrer_url', document.referrer);
}
let landing = document.querySelector('input[name="landing"]');
if (landing) {
params.set('landing', landing.value);
}
params.set('path', window.location.pathname);
params.set('inner-width', window.innerWidth);
params.set('inner-height', window.innerHeight);
fetch('/page-view?' + params.toString());
}
(landing
is for A/B tests, which I've actually stopped doing since I don't get enough traffic for them to be significant)Jacob O'Bryant
03/27/2023, 5:41 PM(defn page-view [{:keys [headers
session
query-params
com.yakread/tracking-cookie]
:as sys}]
(biff/submit-tx sys
[(merge (biff/assoc-some
{:db/doc-type :event
:event/type :page-view
:event/timestamp :db/now
:event/cookie-uid tracking-cookie}
:event/ip (headers "x-real-ip")
:event/user (session :uid)
:event/user-agent (headers "user-agent"))
(update-keys query-params #(keyword "event.params" %)))])
{:status 204})
middleware:
(defn wrap-tracking-cookie [handler]
(fn [{:keys [cookies] :as req}]
(let [new-cookie (random-uuid)
old-cookie (biff/catchall
(java.util.UUID/fromString
(get-in cookies ["uid" :value])))]
(cond-> (handler (assoc req :com.yakread/tracking-cookie (or old-cookie new-cookie)))
(not old-cookie) (assoc-in [:cookies "uid"]
{:path "/"
:max-age (* 60 60 24 365 10)
:same-site :lax
:value (str new-cookie)})))))
Jacob O'Bryant
03/27/2023, 5:44 PM:event/id :uuid
:event (doc {:required [[:xt/id :event/id]
[:event/type [:enum
:page-view
:signup
:subscribe
:navigate]]
[:event/timestamp inst?]
[:event/cookie-uid :uuid]]
:optional [[:event/ip :string]
[:event/user :user/id]
[:event/variant :string]
[:event/user-agent :string]
[:event.page-view/path :string]
[:event.page-view/referrer :string]
[:event.subscribe/opml :string]
[:event.subscribe/feeds [:vector :string]]
[:event.navigate/page :keyword]
[:event.navigate/inner-width number?]
[:event.navigate/display-mode :keyword]]
:wildcards {'event.params any?}})
(doc is an alias for com.biffweb/doc-schema
)
I use cookies to be able to count unique visits (i.e. in the dashboard code, the cookie id is what gets used as the user ID--unless the user signed up, in which case I match the cookie ID for any page views to any cookie IDs for signup events, and use the user document's ID). that also was helpful for fraud detection in The Sample when I was doing affiliate advertising (pay people $x per subscriber they send, via https://swapstack.co), though I've since solved that problem by just not doing affiliate advertising anymore, so probably could get by without cookiesJacob O'Bryant
03/27/2023, 5:45 PMLogan Rios
03/27/2023, 5:55 PMJacob O'Bryant
03/27/2023, 5:56 PMJacob O'Bryant
03/27/2023, 6:04 PMJacob O'Bryant
03/27/2023, 6:07 PMquery-events
implementation--just added itLogan Rios
03/27/2023, 6:09 PMJacob O'Bryant
03/27/2023, 6:11 PMLogan Rios
03/27/2023, 6:12 PMJacob O'Bryant
03/27/2023, 6:17 PMJacob O'Bryant
03/27/2023, 6:18 PMJacob O'Bryant
03/27/2023, 6:18 PMLogan Rios
03/27/2023, 6:20 PMJacob O'Bryant
03/27/2023, 6:22 PMJacob O'Bryant
03/27/2023, 6:22 PMLogan Rios
03/27/2023, 6:22 PMLogan Rios
03/27/2023, 6:23 PM