Skip to content

Commit da8b381

Browse files
Merge pull request #135 from modulr-software/feat/analytics-endpoints
analytics endpoints
2 parents f3089e8 + b7a9fb0 commit da8b381

File tree

10 files changed

+193
-42
lines changed

10 files changed

+193
-42
lines changed

src/source/middleware/auth/core.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
(res/status 401)))))
2929

3030
(defn wrap-auth-user-type
31-
"returns an unauthorized response if the user's type is not the required user type (provider | distributor | admin)"
31+
"returns an unauthorized response if the user's type is not the required user type (creator | distributor | admin)"
3232
[handler & {:keys [required-type]}]
3333
(fn [request]
3434
(let [ds (db.util/conn :master)
@@ -38,7 +38,7 @@
3838
(:type))]
3939
(cond
4040
(not (some? required-type)) (handler request)
41-
(and (= user-type (name :admin)) (= user-type expected-type)) (handler request)
41+
(and (= user-type (name required-type)) (= user-type expected-type)) (handler request)
4242
:else (->
4343
(res/response {:message "Unauthorized"})
4444
(res/status 403))))))
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
(ns source.routes.analytics.bundle.posts.-id-.views
2+
(:require [ring.util.response :as res]
3+
[source.services.analytics.interface :as analytics]
4+
[source.services.interface :as services]))
5+
6+
(defn post
7+
{:summary "Explicitly insert a view event for the post with the given id for the purpose of analytics"
8+
:parameters {:query [:map [:uuid :string]]
9+
:path [:map [:id {:title "id"
10+
:description "post id"} :int]]}
11+
:responses {200 {:body [:map [:message :string]]}}}
12+
13+
[{:keys [ds bundle-id path-params] :as _request}]
14+
(let [post (services/incoming-post ds {:id (:id path-params)})]
15+
(analytics/insert-post-view! ds post bundle-id)
16+
(res/response {:message "Successfully inserted view event"})))
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
(ns source.routes.analytics.creator.deltas
2+
(:require [clojure.walk :as w]
3+
[ring.util.response :as res]
4+
[source.services.analytics.interface :as analytics]))
5+
6+
(defn get
7+
{:summary "Returns the percentage of growth in impressions, clicks and views per week, over the given time period. Optionally filtered by feed."
8+
:parameters {:query [:map
9+
[:mindate :string]
10+
[:maxdate :string]
11+
[:feed {:optional true} [:maybe :int]]]}
12+
:responses {200 {:body [:vector
13+
[:map
14+
[:week :string]
15+
[:impressions :int]
16+
[:clicks :int]
17+
[:views :int]]]}}}
18+
19+
[{:keys [ds user query-params] :as _request}]
20+
(let [{:keys [mindate maxdate feed]} (w/keywordize-keys query-params)]
21+
(res/response (analytics/weekly-growth-averages ds mindate maxdate {:creator-id (:id user)
22+
:feed-id feed}))))

src/source/routes/analytics/creator/general.clj

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
[clojure.walk :as w]))
55

66
(defn get
7-
{:summary "Gets the number of impressions, clicks and views per day over the given time period. Optionally filtered by feed."
7+
{:summary "Gets the number of impressions, clicks and views per day for a creator over the given time period. Optionally filtered by feed."
88
:parameters {:query [:map
99
[:mindate :string]
1010
[:maxdate :string]
11-
[:creator :int]
1211
[:feed {:optional true} [:maybe :int]]]}
1312
:responses {200 {:body [:vector
1413
[:map
@@ -17,7 +16,7 @@
1716
[:clicks :int]
1817
[:views :int]]]}}}
1918

20-
[{:keys [ds query-params] :as _request}]
21-
(let [{:keys [mindate maxdate creator feed]} (w/keywordize-keys query-params)]
22-
(res/response (analytics/interval-statistics-query ds :daily mindate maxdate {:creator-id creator
19+
[{:keys [ds user query-params] :as _request}]
20+
(let [{:keys [mindate maxdate feed]} (w/keywordize-keys query-params)]
21+
(res/response (analytics/interval-statistics-query ds :daily mindate maxdate {:creator-id (:id user)
2322
:feed-id feed}))))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
(ns source.routes.analytics.creator.top
2+
(:require [source.services.analytics.interface :as analytics]
3+
[ring.util.response :as res]
4+
[clojure.walk :as w]
5+
[clojure.set :as set]))
6+
7+
(defn get
8+
{:summary "Get the top n records with the highest number of impressions, clicks and views, in terms of the given top field. Optionally filtered by content type."
9+
:parameters {:query [:map
10+
[:n :int]
11+
[:mindate :string]
12+
[:maxdate :string]
13+
[:top [:enum "post" "bundle"]]
14+
[:contenttype {:optional true} [:maybe :int]]]}
15+
:responses {200 {:body [:vector
16+
[:map
17+
[:top :string]
18+
[:impressions :int]
19+
[:clicks :int]
20+
[:views :int]]]}}}
21+
22+
[{:keys [ds user query-params] :as _request}]
23+
(let [{:keys [n mindate maxdate top contenttype]} (w/keywordize-keys query-params)
24+
top-field (if (= top "post") :post-id :bundle-id)
25+
results (analytics/top-statistics-query ds mindate maxdate n top-field {:creator-id (:id user)
26+
:content-type-id contenttype})]
27+
(res/response (mapv (fn [result]
28+
(set/rename-keys result {top-field :top})) results))))
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
(ns source.routes.analytics.distributor.general
2+
(:require [source.services.analytics.interface :as analytics]
3+
[ring.util.response :as res]
4+
[clojure.walk :as w]))
5+
6+
(defn get
7+
{:summary "Gets the number of impressions, clicks and views per day for a distributor over the given time period. Optionally filtered by bundle."
8+
:parameters {:query [:map
9+
[:mindate :string]
10+
[:maxdate :string]
11+
[:bundle {:optional true} [:maybe :int]]]}
12+
:responses {200 {:body [:vector
13+
[:map
14+
[:day :string]
15+
[:impressions :int]
16+
[:clicks :int]
17+
[:views :int]]]}}}
18+
19+
[{:keys [ds user query-params] :as _request}]
20+
(let [{:keys [mindate maxdate bundle]} (w/keywordize-keys query-params)]
21+
(res/response (analytics/interval-statistics-query ds :daily mindate maxdate {:distributor-id (:id user)
22+
:bundle-id bundle}))))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
(ns source.routes.analytics.distributor.top
2+
(:require [source.services.analytics.interface :as analytics]
3+
[ring.util.response :as res]
4+
[clojure.walk :as w]
5+
[clojure.set :as set]))
6+
7+
(defn get
8+
{:summary "Get the top n records with the highest number of impressions, clicks and views, in terms of the given top field. Optionally filtered by content type."
9+
:parameters {:query [:map
10+
[:n :int]
11+
[:mindate :string]
12+
[:maxdate :string]
13+
[:top [:enum "post" "feed"]]
14+
[:contenttype {:optional true} [:maybe :int]]]}
15+
:responses {200 {:body [:vector
16+
[:map
17+
[:top :string]
18+
[:impressions :int]
19+
[:clicks :int]
20+
[:views :int]]]}}}
21+
22+
[{:keys [ds user query-params] :as _request}]
23+
(let [{:keys [n mindate maxdate top contenttype]} (w/keywordize-keys query-params)
24+
top-field (if (= top "post") :post-id :feed-id)
25+
results (analytics/top-statistics-query ds mindate maxdate n top-field {:distributor-id (:id user)
26+
:content-type-id contenttype})]
27+
(res/response (mapv (fn [result]
28+
(set/rename-keys result {top-field :top})) results))))

src/source/routes/reitit.clj

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
[source.routes.feed :as feed]
4242
[source.routes.feed-categories :as feed-categories]
4343
[source.routes.analytics.creator.general :as analytics-creator-general]
44+
[source.routes.analytics.creator.deltas :as analytics-creator-deltas]
45+
[source.routes.analytics.creator.top :as analytics-creator-top]
46+
[source.routes.analytics.distributor.general :as analytics-distributor-general]
47+
[source.routes.analytics.distributor.top :as analytics-distributor-top]
48+
[source.routes.analytics.bundle.posts.-id-.views :as analytics-bundle-posts-id-views]
4449
[source.routes.integrations :as integrations]
4550
[source.routes.integration :as integration]
4651
[source.routes.integration-key :as integration-key]
@@ -218,12 +223,20 @@
218223
:post feed-categories/post})]]]
219224

220225
["/analytics"
221-
["/creator"
226+
["/creator" {:middleware [[mw/apply-auth {:required-type :creator}]]}
222227
["/general" (route {:get analytics-creator-general/get})]
223-
["/deltas"]
224-
["/top"]]
225-
["/bundle"]
226-
["admin"]]
228+
["/deltas" (route {:get analytics-creator-deltas/get})]
229+
["/top" (route {:get analytics-creator-top/get})]]
230+
["/distributor" {:middleware [[mw/apply-auth {:required-type :distributor}]]}
231+
["/general" (route {:get analytics-distributor-general/get})]
232+
["/top" (route {:get analytics-distributor-top/get})]]
233+
["/bundle" {:middleware [[mw/apply-bundle]]}
234+
["/posts"
235+
["/:id"
236+
["/views" (route {:post analytics-bundle-posts-id-views/post})]]]]
237+
["admin" {:middleware [[mw/apply-auth {:required-type :admin}]]}
238+
["/general"]
239+
["/top"]]]
227240

228241
["/bundle" {:middleware [[mw/apply-bundle]]
229242
:tags #{"bundles"}}

src/source/services/analytics/core.clj

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
(defn metric-query
99
"Generic select query function for returning analytics data from the events table"
10-
[ds {:keys [select order-by group-by metric feed-id post-id content-type-id bundle-id creator-id distributor-id min-date max-date category-ids ret]}]
10+
[ds {:keys [select order-by group-by limit metric feed-id post-id content-type-id bundle-id creator-id distributor-id min-date max-date category-ids where ret]}]
1111
(let [clauses (cond-> {}
1212
(some? metric) (hsql/where [:= :event metric])
1313
(some? feed-id) (hsql/where [:= :feed-id feed-id])
@@ -16,28 +16,31 @@
1616
(some? bundle-id) (hsql/where [:= :bundle-id bundle-id])
1717
(some? creator-id) (hsql/where [:= :creator-id creator-id])
1818
(some? distributor-id) (hsql/where [:= :distributor-id distributor-id])
19+
(some? where) (merge where)
1920
(and (some? min-date) (nil? max-date)) (hsql/where [:>= :timestamp min-date])
2021
(and (some? max-date) (nil? min-date)) (hsql/where [:<= :timestamp max-date])
2122
(and (some? min-date) (some? max-date)) (hsql/where [:between :timestamp min-date max-date])
2223
(some? select) (merge select)
24+
(nil? select) (merge {:select [[[:count :*] :total]]})
25+
(some? limit) (hsql/limit limit)
2326
(some? order-by) (merge order-by)
2427
(some? group-by) (merge group-by)
2528
(seq category-ids) (-> (hsql/join [:event-categories :ec] [:= :events.id :ec.event-id])
2629
(hsql/where [:in :ec.category-id category-ids])))]
2730
(hon/execute!
2831
ds
29-
(merge {:select [[[:count :*] :total]]
30-
:from [:events]}
32+
(merge {:from [:events]}
3133
clauses)
3234
{:ret (if ret ret :*)})))
3335

3436
(defn statistics-query
35-
"returns the number of impressions, clicks and views, filtered by any other arguments accepted by metric-query"
36-
[ds opts]
37+
"Returns the number of impressions, clicks and views, filtered by any other arguments accepted by metric-query.
38+
If ret is not given, returns a single record."
39+
[ds {:keys [ret] :as opts}]
3740
(metric-query ds (merge {:select (hsql/select [(hsql/filter :%count.* (hsql/where := :event "impression")) :impressions]
3841
[(hsql/filter :%count.* (hsql/where := :event "click")) :clicks]
3942
[(hsql/filter :%count.* (hsql/where := :event "view")) :views])
40-
:ret :1}
43+
:ret (if ret ret :1)}
4144
opts)))
4245

4346
(defn interval-statistics-query
@@ -69,9 +72,25 @@
6972
:order-by (hsql/order-by column)}
7073
opts))))
7174

75+
(defn top-statistics-query
76+
"Returns the top n of the given top field in order of the number of their impressions, clicks and views within the given time period"
77+
[ds min-date max-date n top-field opts]
78+
(metric-query ds (merge {:select (hsql/select top-field
79+
[(hsql/filter :%count.* (hsql/where := :event "impression")) :impressions]
80+
[(hsql/filter :%count.* (hsql/where := :event "click")) :clicks]
81+
[(hsql/filter :%count.* (hsql/where := :event "view")) :views])
82+
:min-date min-date
83+
:max-date max-date
84+
:where (hsql/where [:!= top-field nil])
85+
:group-by (hsql/group-by top-field)
86+
:order-by (hsql/order-by [:impressions :desc] [:clicks :desc] [:views :desc])
87+
:limit n
88+
:ret :*}
89+
opts)))
90+
7291
(defn weekly-growth-averages
7392
"Returns the percentage of growth in impressions, clicks and views per week, over the given time period.
74-
Uses the first week as a basis for comparison, not included in results.
93+
Uses the first week as a basis for comparison.
7594
Can be filtered by any other arguments accepted by metric-query."
7695
[ds min-date max-date opts]
7796
(let [weeks (interval-statistics-query ds :weekly min-date max-date opts)
@@ -171,7 +190,7 @@
171190
:bundle-id bundle-id
172191
:distributor-id (:user-id bundle)}) feeds)
173192
events' (insert-event! ds {:data events
174-
:ret :*})]
193+
:ret :*})]
175194
(insert-feed-event-categories! ds events' feeds)))
176195

177196
(defn insert-post-impressions!
@@ -189,7 +208,7 @@
189208
:bundle-id bundle-id
190209
:distributor-id (:user-id bundle)}) posts)
191210
events' (insert-event! ds {:data events
192-
:ret :*})]
211+
:ret :*})]
193212
(insert-post-event-categories! ds events' posts)))
194213

195214
(defn insert-feed-click!
@@ -205,7 +224,7 @@
205224
:bundle-id bundle-id
206225
:distributor-id (:user-id bundle)}
207226
event' (insert-event! ds {:data event
208-
:ret :*})]
227+
:ret :*})]
209228
(insert-feed-event-categories! ds event' feed)))
210229

211230
(defn insert-post-click!
@@ -222,7 +241,7 @@
222241
:bundle-id bundle-id
223242
:distributor-id (:user-id bundle)}
224243
event' (insert-event! ds {:data event
225-
:ret :*})]
244+
:ret :*})]
226245
(insert-post-event-categories! ds event' post)))
227246

228247
(defn insert-post-view!
@@ -239,7 +258,7 @@
239258
:bundle-id bundle-id
240259
:distributor-id (:user-id bundle)}
241260
event' (insert-event! ds {:data event
242-
:ret :*})]
261+
:ret :*})]
243262
(insert-post-event-categories! ds event' post)))
244263

245264
(comment
@@ -290,10 +309,7 @@
290309
(time (metric-query ds {:min-date "2025-11-25 15:00:00"
291310
:feed-id "1"}))
292311

293-
(time (statistics-query ds {:content-type 1
294-
:creator-id 1
295-
:bundle-id 1
296-
:ret :1}))
312+
(time (statistics-query ds {:ret :*}))
297313

298314
(time (hon/find ds {:tname :events
299315
:where [:< :id 500]
@@ -304,14 +320,14 @@
304320
:data {:timestamp (str "2025-11-22" " 13:00:00")}
305321
:ret :*}))
306322

307-
(time (hon/execute! ds (- (hsql/select [[:date :timestamp] :day]
308-
[(hsql/filter :%count.* (hsql/where := :event "impression")) :impressions]
309-
[(hsql/filter :%count.* (hsql/where := :event "click")) :clicks]
310-
[(hsql/filter :%count.* (hsql/where := :event "view")) :views])
311-
(hsql/from :events)
312-
(hsql/where [:between :timestamp "2025-11-17 00:00:00" "2025-11-24 23:59:59"])
313-
(hsql/group-by :day)
314-
(hsql/order-by :day)) {:ret :*}))
323+
(time (hon/execute! ds (-> (hsql/select [[:date :timestamp] :day]
324+
[(hsql/filter :%count.* (hsql/where := :event "impression")) :impressions]
325+
[(hsql/filter :%count.* (hsql/where := :event "click")) :clicks]
326+
[(hsql/filter :%count.* (hsql/where := :event "view")) :views])
327+
(hsql/from :events)
328+
(hsql/where [:between :timestamp "2025-11-17 00:00:00" "2025-11-24 23:59:59"])
329+
(hsql/group-by :day)
330+
(hsql/order-by :day)) {:ret :*}))
315331

316332
(time (interval-statistics-query ds :daily "2025-11-17" "2025-11-24" {:feed-id 4}))
317333

@@ -356,12 +372,14 @@
356372
[(hsql/filter :%count.* (hsql/where := :event "view")) :views])))
357373

358374
(time (insert-event! ds {:data {:timestamp (util/get-utc-timestamp-string)
359-
:event "impression"
360-
:feed-id 1
361-
:content-type-id 1
362-
:creator-id 1
363-
:bundle-id 1
364-
:distributor-id 1}}))
375+
:event "impression"
376+
:feed-id 1
377+
:content-type-id 1
378+
:creator-id 1
379+
:bundle-id 1
380+
:distributor-id 1}}))
381+
382+
(time (top-statistics-query ds "2025-11-17" "2025-11-24" 10 :post-id {}))
365383

366384
())
367385

src/source/services/analytics/interface.clj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
[ds min-date max-date opts]
2727
(core/weekly-growth-averages ds min-date max-date opts))
2828

29+
(defn top-statistics-query
30+
"Returns the top n of the given top field in order of the number of their impressions, clicks and views within the given time period."
31+
[ds min-date max-date n top-field opts]
32+
(core/top-statistics-query ds min-date max-date n top-field opts))
33+
2934
(defn average-engagement
3035
"Returns the average engagement (average clicks and views) over the given time period.
3136
Can be filtered by any other arguments accepted by metric-query."

0 commit comments

Comments
 (0)