Skip to content

Commit f61bcb3

Browse files
committed
feat: add improved analytics
1 parent 25fa187 commit f61bcb3

File tree

11 files changed

+340
-62
lines changed

11 files changed

+340
-62
lines changed

apps/backend/src/backend/postgres/queries.gleam

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import gleam/dict.{type Dict}
88
import gleam/dynamic
99
import gleam/hexpm
1010
import gleam/int
11-
import gleam/io
1211
import gleam/json
1312
import gleam/list
1413
import gleam/option.{type Option, None, Some}
@@ -72,6 +71,36 @@ pub fn upsert_search_analytics(db: pgo.Connection, query: String) {
7271
})
7372
}
7473

74+
pub fn select_more_popular_packages(db: pgo.Connection) {
75+
let decoder =
76+
dynamic.tuple4(
77+
dynamic.string,
78+
dynamic.string,
79+
dynamic.int,
80+
dynamic.optional(dynamic.int),
81+
)
82+
use ranked <- result.try({
83+
"SELECT name, repository, rank, (popularity -> 'github')::int
84+
FROM package
85+
ORDER BY rank DESC
86+
LIMIT 22"
87+
|> pgo.execute(db, [], decoder)
88+
|> result.map(fn(r) { r.rows })
89+
|> result.map_error(error.DatabaseError)
90+
})
91+
use popular <- result.try({
92+
"SELECT name, repository, rank, (popularity -> 'github')::int
93+
FROM package
94+
WHERE popularity -> 'github' IS NOT NULL
95+
ORDER BY popularity -> 'github' DESC
96+
LIMIT 23"
97+
|> pgo.execute(db, [], decoder)
98+
|> result.map(fn(r) { r.rows })
99+
|> result.map_error(error.DatabaseError)
100+
})
101+
Ok(#(ranked, popular))
102+
}
103+
75104
pub fn select_last_day_search_analytics(db: pgo.Connection) {
76105
let #(date, _) = birl.to_erlang_universal_datetime(birl.now())
77106
let now = birl.from_erlang_universal_datetime(#(date, #(0, 0, 0)))
@@ -113,7 +142,9 @@ pub fn get_timeseries_count(db: pgo.Connection) {
113142
(SELECT att.occurences
114143
FROM analytics_timeseries att
115144
WHERE att.date < at.date
116-
AND att.query = at.query),
145+
AND att.query = at.query
146+
ORDER BY date DESC
147+
LIMIT 1),
117148
0)
118149
) searches,
119150
at.date date

apps/backend/src/backend/router.gleam

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import gleam/int
1212
import gleam/json
1313
import gleam/list
1414
import gleam/option
15-
import gleam/pair
1615
import gleam/result
1716
import gleam/string_builder
1817
import tasks/hex as syncing
@@ -126,6 +125,16 @@ fn search(query: String, ctx: Context) {
126125
])
127126
}
128127

128+
fn encode_package(package: #(String, String, Int, option.Option(Int))) {
129+
let #(name, repository, rank, popularity) = package
130+
json.object([
131+
#("name", json.string(name)),
132+
#("repository", json.string(repository)),
133+
#("rank", json.int(rank)),
134+
#("popularity", json.nullable(popularity, json.int)),
135+
])
136+
}
137+
129138
pub fn handle_get(req: Request, ctx: Context) {
130139
case wisp.path_segments(req) {
131140
["healthcheck"] -> wisp.ok()
@@ -149,17 +158,23 @@ pub fn handle_get(req: Request, ctx: Context) {
149158
use total <- result.try(queries.get_total_searches(ctx.db))
150159
use signatures <- result.try(queries.get_total_signatures(ctx.db))
151160
use packages <- result.try(queries.get_total_packages(ctx.db))
161+
use #(ranked, popular) <- result.try({
162+
queries.select_more_popular_packages(ctx.db)
163+
})
152164
let total = list.first(total) |> result.unwrap(0)
153165
let signatures = list.first(signatures) |> result.unwrap(0)
154166
let packages = list.first(packages) |> result.unwrap(0)
155-
Ok(#(timeseries, total, signatures, packages))
167+
Ok(#(timeseries, total, signatures, packages, ranked, popular))
156168
}
157169
|> result.map(fn(content) {
158-
let #(timeseries, total, signatures, packages) = content
170+
let #(timeseries, total, signatures, packages, ranked, popular) =
171+
content
159172
json.object([
160173
#("total", json.int(total)),
161174
#("signatures", json.int(signatures)),
162175
#("packages", json.int(packages)),
176+
#("ranked", json.array(ranked, encode_package)),
177+
#("popular", json.array(popular, encode_package)),
163178
#("timeseries", {
164179
json.array(timeseries, fn(row) {
165180
json.object([
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Chart } from 'chart.js/auto'
2+
3+
export class BarChart extends HTMLElement {
4+
static observedAttributes = ['datasets']
5+
6+
#shadow
7+
#canvas
8+
dataset
9+
10+
constructor() {
11+
super()
12+
this.#shadow = this.attachShadow({ mode: 'open' })
13+
}
14+
15+
connectedCallback() {
16+
this.#render()
17+
}
18+
19+
#render() {
20+
const labels = this.datasets.labels.toArray()
21+
const data = this.datasets.data.toArray()
22+
const color = this.color
23+
const wrapper = document.createElement('div')
24+
wrapper.style.position = 'relative'
25+
wrapper.style.maxWidth = '850px'
26+
// wrapper.style.padding = '12px'
27+
// wrapper.style.maxHeight = '150px'
28+
this.#canvas = document.createElement('canvas')
29+
wrapper.appendChild(this.#canvas)
30+
this.#shadow.appendChild(wrapper)
31+
Chart.defaults.font.family = 'Lexend'
32+
new Chart(this.#canvas, {
33+
type: 'bar',
34+
data: {
35+
labels,
36+
datasets: [
37+
{
38+
data,
39+
borderColor: `${color}aa`,
40+
backgroundColor: `${color}22`,
41+
borderRadius: 5,
42+
},
43+
],
44+
},
45+
options: {
46+
aspectRatio: 1,
47+
indexAxis: 'y',
48+
responsive: true,
49+
animation: false,
50+
events: [],
51+
layout: {
52+
padding: {
53+
right: 12,
54+
},
55+
},
56+
plugins: {
57+
legend: {
58+
display: false,
59+
},
60+
},
61+
elements: {
62+
bar: {
63+
borderWidth: 2,
64+
barPercentage: 0.5,
65+
barThickness: 6,
66+
maxBarThickness: 8,
67+
minBarLength: 2,
68+
},
69+
},
70+
scales: {
71+
x: {
72+
display: true,
73+
grid: { drawTicks: false, display: true },
74+
ticks: { padding: 0, align: 'inner', padding: 5 },
75+
},
76+
y: {
77+
display: true,
78+
grid: { drawTicks: false, display: true },
79+
ticks: {
80+
padding: 5,
81+
mirror: true,
82+
includeBounds: false,
83+
backdropPadding: 0,
84+
},
85+
},
86+
},
87+
},
88+
})
89+
}
90+
91+
// Lifecycle functions.
92+
disconnectedCallback() {}
93+
adoptedCallback() {}
94+
95+
attributeChangedCallback() {}
96+
97+
static register() {
98+
customElements.define('bar-chart', BarChart)
99+
}
100+
}

apps/frontend/src/line_chart.gleam renamed to apps/frontend/src/chart.gleam

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import gleam/string
21
import lustre/attribute
32
import lustre/element
43

@@ -14,3 +13,13 @@ pub fn line_chart(datasets: Dataset) {
1413
[],
1514
)
1615
}
16+
17+
pub fn bar_chart(color: String, datasets: Dataset) {
18+
let datasets = attribute.property("datasets", datasets)
19+
let color = attribute.property("color", color)
20+
element.element(
21+
"bar-chart",
22+
[attribute.style([#("display", "block")]), datasets, color],
23+
[],
24+
)
25+
}

apps/frontend/src/data/model.gleam

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ pub type Model {
4141
total_signatures: Int,
4242
total_packages: Int,
4343
timeseries: List(#(Int, birl.Time)),
44+
ranked: List(msg.Package),
45+
popular: List(msg.Package),
4446
)
4547
}
4648

@@ -71,6 +73,8 @@ pub fn init() {
7173
total_signatures: 0,
7274
total_packages: 0,
7375
timeseries: [],
76+
ranked: [],
77+
popular: [],
7478
)
7579
}
7680

@@ -102,18 +106,15 @@ pub fn update_input(model: Model, content: String) {
102106
Model(..model, input: content)
103107
}
104108

105-
pub fn update_analytics(
106-
model: Model,
107-
analytics: #(Int, Int, Int, List(#(Int, birl.Time))),
108-
) {
109-
let #(total_searches, total_signatures, total_packages, timeseries) =
110-
analytics
109+
pub fn update_analytics(model: Model, analytics: msg.Analytics) {
111110
Model(
112111
..model,
113-
timeseries:,
114-
total_searches:,
115-
total_signatures:,
116-
total_packages:,
112+
timeseries: analytics.timeseries,
113+
total_searches: analytics.total_searches,
114+
total_signatures: analytics.total_signatures,
115+
total_packages: analytics.total_indexed,
116+
ranked: analytics.ranked,
117+
popular: analytics.popular,
117118
)
118119
}
119120

@@ -348,10 +349,12 @@ pub fn reset(model: Model) {
348349
show_old_packages: False,
349350
show_documentation_search: False,
350351
show_vector_search: False,
351-
timeseries: [],
352-
total_searches: 0,
353-
total_signatures: 0,
354-
total_packages: 0,
352+
timeseries: model.timeseries,
353+
total_searches: model.total_searches,
354+
total_signatures: model.total_signatures,
355+
total_packages: model.total_packages,
356+
ranked: model.ranked,
357+
popular: model.popular,
355358
)
356359
}
357360

apps/frontend/src/data/msg.gleam

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import birl
22
import data/package
33
import data/search_result.{type SearchResults}
44
import frontend/router
5+
import gleam/option
56
import lustre_http as http
67
import plinth/browser/event.{type Event}
78

@@ -15,6 +16,26 @@ pub type Filter {
1516
VectorSearch
1617
}
1718

19+
pub type Package {
20+
Package(
21+
name: String,
22+
repository: String,
23+
rank: Int,
24+
popularity: option.Option(Int),
25+
)
26+
}
27+
28+
pub type Analytics {
29+
Analytics(
30+
total_searches: Int,
31+
total_signatures: Int,
32+
total_indexed: Int,
33+
timeseries: List(#(Int, birl.Time)),
34+
ranked: List(Package),
35+
popular: List(Package),
36+
)
37+
}
38+
1839
pub type Msg {
1940
None
2041
OnSearchFocus(event: Event)
@@ -26,7 +47,7 @@ pub type Msg {
2647
Reset
2748
ScrollTo(String)
2849
OnEscape
29-
Analytics(Result(#(Int, Int, Int, List(#(Int, birl.Time))), http.HttpError))
50+
OnAnalytics(Result(Analytics, http.HttpError))
3051
OnRouteChange(router.Route)
3152
OnCheckFilter(Filter, Bool)
3253
}

apps/frontend/src/frontend.gleam

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ pub fn main() {
7474
|> lustre.start("#app", Nil)
7575
}
7676

77+
fn decode_package(dyn) {
78+
dynamic.decode4(
79+
msg.Package,
80+
dynamic.field("name", dynamic.string),
81+
dynamic.field("repository", dynamic.string),
82+
dynamic.field("rank", dynamic.int),
83+
dynamic.field("popularity", dynamic.optional(dynamic.int)),
84+
)(dyn)
85+
}
86+
7787
fn init(_) {
7888
let initial =
7989
modem.initial_uri()
@@ -90,10 +100,10 @@ fn init(_) {
90100
|> http.get(config.api_endpoint() <> "/trendings", _),
91101
)
92102
|> update.add_effect(
93-
msg.Analytics
103+
msg.OnAnalytics
94104
|> http.expect_json(
95-
dynamic.decode4(
96-
fn(a, b, c, d) { #(a, b, c, d) },
105+
dynamic.decode6(
106+
msg.Analytics,
97107
dynamic.field("total", dynamic.int),
98108
dynamic.field("signatures", dynamic.int),
99109
dynamic.field("packages", dynamic.int),
@@ -110,6 +120,8 @@ fn init(_) {
110120
}),
111121
))
112122
}),
123+
dynamic.field("ranked", dynamic.list(decode_package)),
124+
dynamic.field("popular", dynamic.list(decode_package)),
113125
),
114126
_,
115127
)
@@ -142,7 +154,7 @@ fn update(model: Model, msg: Msg) {
142154
handle_search_results(model, input, search_results)
143155
msg.OnCheckFilter(filter, value) ->
144156
handle_oncheck_filter(model, filter, value)
145-
msg.Analytics(analytics) -> {
157+
msg.OnAnalytics(analytics) -> {
146158
case analytics {
147159
Error(_) -> #(model, effect.none())
148160
Ok(analytics) ->

0 commit comments

Comments
 (0)