Skip to content

Commit

Permalink
feat: add analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
ghivert committed Sep 4, 2024
1 parent 8b9ed95 commit 1a46e28
Show file tree
Hide file tree
Showing 12 changed files with 293 additions and 27 deletions.
23 changes: 22 additions & 1 deletion apps/backend/src/backend/postgres/queries.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import backend/data/hex_user.{type HexUser}
import backend/error
import backend/gleam/context
import birl.{type Time}
import birl/duration
import gleam/bool
import gleam/dict.{type Dict}
import gleam/dynamic
import gleam/hexpm
import gleam/int
import gleam/io
import gleam/json
import gleam/list
import gleam/option.{type Option, None, Some}
Expand Down Expand Up @@ -157,6 +157,27 @@ fn get_current_package_owners(db: pgo.Connection, package_id: Int) {
|> result.map_error(error.DatabaseError)
}

pub fn get_total_searches(db: pgo.Connection) {
"SELECT SUM(occurences) FROM search_analytics"
|> pgo.execute(db, [], dynamic.element(0, dynamic.int))
|> result.map(fn(r) { r.rows })
|> result.map_error(error.DatabaseError)
}

pub fn get_total_signatures(db: pgo.Connection) {
"SELECT COUNT(*) FROM package_type_fun_signature"
|> pgo.execute(db, [], dynamic.element(0, dynamic.int))
|> result.map(fn(r) { r.rows })
|> result.map_error(error.DatabaseError)
}

pub fn get_total_packages(db: pgo.Connection) {
"SELECT COUNT(*) FROM package"
|> pgo.execute(db, [], dynamic.element(0, dynamic.int))
|> result.map(fn(r) { r.rows })
|> result.map_error(error.DatabaseError)
}

fn add_new_package_owners(
db: pgo.Connection,
owners: List(HexUser),
Expand Down
32 changes: 25 additions & 7 deletions apps/backend/src/backend/router.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import gleam/int
import gleam/json
import gleam/list
import gleam/option
import gleam/pair
import gleam/result
import gleam/string_builder
import tasks/hex as syncing
Expand Down Expand Up @@ -143,14 +144,31 @@ pub fn handle_get(req: Request, ctx: Context) {
})
|> result.unwrap(wisp.internal_server_error())
["analytics"] -> {
queries.get_timeseries_count(ctx.db)
{
use timeseries <- result.try(queries.get_timeseries_count(ctx.db))
use total <- result.try(queries.get_total_searches(ctx.db))
use signatures <- result.try(queries.get_total_signatures(ctx.db))
use packages <- result.try(queries.get_total_packages(ctx.db))
let total = list.first(total) |> result.unwrap(0)
let signatures = list.first(signatures) |> result.unwrap(0)
let packages = list.first(packages) |> result.unwrap(0)
Ok(#(timeseries, total, signatures, packages))
}
|> result.map(fn(content) {
json.array(content, fn(row) {
json.object([
#("count", json.int(row.0)),
#("date", json.string(birl.to_iso8601(row.1))),
])
})
let #(timeseries, total, signatures, packages) = content
json.object([
#("total", json.int(total)),
#("signatures", json.int(signatures)),
#("packages", json.int(packages)),
#("timeseries", {
json.array(timeseries, fn(row) {
json.object([
#("count", json.int(row.0)),
#("date", json.string(birl.to_iso8601(row.1))),
])
})
}),
])
|> json.to_string_builder
|> wisp.json_response(200)
})
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@chouqueth/gleam": "^1.4.1",
"@gleam-lang/highlight.js-gleam": "^1.5.0",
"@sentry/browser": "^8.0.0",
"chart.js": "^4.4.4",
"dompurify": "^3.1.4",
"highlight.js": "^11.9.0",
"marked": "^12.0.2",
Expand Down
69 changes: 69 additions & 0 deletions apps/frontend/src/chart.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Chart } from 'chart.js/auto'

export class LineChart extends HTMLElement {
static observedAttributes = ['datasets']

#shadow
#canvas
dataset

constructor() {
super()
this.#shadow = this.attachShadow({ mode: 'open' })
}

connectedCallback() {
this.#render()
}

#render() {
const labels = this.datasets.labels.toArray()
const data = this.datasets.data.toArray()
const wrapper = document.createElement('div')
wrapper.style.position = 'relative'
// wrapper.style.maxWidth = '400px'
// wrapper.style.maxHeight = '150px'
this.#canvas = document.createElement('canvas')
wrapper.appendChild(this.#canvas)
this.#shadow.appendChild(wrapper)

new Chart(this.#canvas, {
type: 'line',
data: {
labels,
datasets: [
{
data,
borderColor: '#8c3a96',
fill: false,
tension: 0.4,
},
],
},
options: {
responsive: true,
animation: false,
events: [],
plugins: {
legend: {
display: false,
},
},
scales: {
x: { display: true, title: { display: true } },
y: { display: true, title: { display: true, text: 'Value' } },
},
},
})
}

// Lifecycle functions.
disconnectedCallback() {}
adoptedCallback() {}

attributeChangedCallback() {}

static register() {
customElements.define('line-chart', LineChart)
}
}
30 changes: 25 additions & 5 deletions apps/frontend/src/data/model.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ pub type Model {
show_old_packages: Bool,
show_documentation_search: Bool,
show_vector_search: Bool,
analytics: List(#(Int, birl.Time)),
total_searches: Int,
total_signatures: Int,
total_packages: Int,
timeseries: List(#(Int, birl.Time)),
)
}

Expand All @@ -64,7 +67,10 @@ pub fn init() {
show_old_packages: False,
show_documentation_search: False,
show_vector_search: False,
analytics: [],
total_searches: 0,
total_signatures: 0,
total_packages: 0,
timeseries: [],
)
}

Expand Down Expand Up @@ -96,8 +102,19 @@ pub fn update_input(model: Model, content: String) {
Model(..model, input: content)
}

pub fn update_analytics(model: Model, analytics: List(#(Int, birl.Time))) {
Model(..model, analytics: analytics)
pub fn update_analytics(
model: Model,
analytics: #(Int, Int, Int, List(#(Int, birl.Time))),
) {
let #(total_searches, total_signatures, total_packages, timeseries) =
analytics
Model(
..model,
timeseries:,
total_searches:,
total_signatures:,
total_packages:,
)
}

pub fn search_key(key key: String, model model: Model) {
Expand Down Expand Up @@ -331,7 +348,10 @@ pub fn reset(model: Model) {
show_old_packages: False,
show_documentation_search: False,
show_vector_search: False,
analytics: [],
timeseries: [],
total_searches: 0,
total_signatures: 0,
total_packages: 0,
)
}

Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/data/msg.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub type Msg {
Reset
ScrollTo(String)
OnEscape
Analytics(Result(List(#(Int, birl.Time)), http.HttpError))
Analytics(Result(#(Int, Int, Int, List(#(Int, birl.Time))), http.HttpError))
OnRouteChange(router.Route)
OnCheckFilter(Filter, Bool)
}
29 changes: 18 additions & 11 deletions apps/frontend/src/frontend.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import frontend/view/body/search_result as sr
import gleam/bool
import gleam/dict
import gleam/dynamic
import gleam/io
import gleam/option.{None, Some}
import gleam/pair
import gleam/result
Expand Down Expand Up @@ -93,17 +92,25 @@ fn init(_) {
|> update.add_effect(
msg.Analytics
|> http.expect_json(
dynamic.list(dynamic.decode2(
pair.new,
dynamic.field("count", dynamic.int),
dynamic.field("date", fn(dyn) {
dynamic.string(dyn)
|> result.then(fn(t) {
birl.parse(t)
|> result.replace_error([])
})
dynamic.decode4(
fn(a, b, c, d) { #(a, b, c, d) },
dynamic.field("total", dynamic.int),
dynamic.field("signatures", dynamic.int),
dynamic.field("packages", dynamic.int),
dynamic.field("timeseries", {
dynamic.list(dynamic.decode2(
pair.new,
dynamic.field("count", dynamic.int),
dynamic.field("date", fn(dyn) {
dynamic.string(dyn)
|> result.then(fn(t) {
birl.parse(t)
|> result.replace_error([])
})
}),
))
}),
)),
),
_,
)
|> http.get(config.api_endpoint() <> "/analytics", _),
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import gleamHljs from '@gleam-lang/highlight.js-gleam'
import hljs from 'highlight.js/lib/core'
import plaintext from 'highlight.js/lib/languages/plaintext'
// @ts-ignore
import { LineChart } from './chart.mjs'
// @ts-ignore
import { main } from './frontend.gleam'
import './stylesheets/all.css'
import './stylesheets/hljs-theme.css'
import './stylesheets/main.css'
import './stylesheets/normalize.css'

LineChart.register()
// @ts-ignore
Element.prototype._attachShadow = Element.prototype.attachShadow
Element.prototype.attachShadow = function () {
Expand Down
73 changes: 72 additions & 1 deletion apps/frontend/src/frontend/view/body/body.gleam
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import birl
import data/model.{type Model}
import data/msg
import data/search_result
Expand All @@ -8,8 +9,13 @@ import frontend/strings as frontend_strings
import frontend/view/search_input/search_input
import gleam/bool
import gleam/dict
import gleam/float
import gleam/int
import gleam/io
import gleam/list
import gleam/result
import gleam/string
import line_chart
import lustre/attribute as a
import lustre/element as el
import lustre/element/html as h
Expand Down Expand Up @@ -220,12 +226,77 @@ fn sidebar_link(href href: String, title title: String, icon icon) {
])
}

fn format_huge_number(number: Int) {
let number = int.to_float(number)
let g = number /. 1_000_000_000.0
let m = number /. 1_000_000.0
let k = number /. 1000.0
case number {
_ if g >. 1.0 -> float.to_string(g) |> string.slice(0, 5) <> " G"
_ if m >. 1.0 -> float.to_string(m) |> string.slice(0, 5) <> " M"
_ if k >. 1.0 -> float.to_string(k) |> string.slice(0, 5) <> " K"
_ -> float.round(number) |> int.to_string
}
}

pub fn body(model: Model) {
case model.route {
router.Home -> h.main([a.class("main")], [view_search_input(model)])
router.Trending -> h.main([a.class("main")], [view_trending(model)])
router.Analytics ->
el.fragment([sidebar(model), h.main([a.class("main")], [])])
el.fragment([
sidebar(model),
h.main([a.class("main")], [
h.div(
[a.class("items-wrapper"), a.style([#("padding-left", "24px")])],
[
h.div([a.class("matches-titles")], [
h.div([a.class("matches-title")], [h.text("Global analytics")]),
]),
h.div([a.class("analytics-box-wrapper")], [
h.div([a.class("analytics-box")], [
h.div([a.class("analytics-title")], [
h.text("Number of searches"),
]),
h.text(format_huge_number(model.total_searches)),
]),
h.div([a.class("analytics-box")], [
h.div([a.class("analytics-title")], [
h.text("Number of signatures indexed"),
]),
h.text(format_huge_number(model.total_signatures)),
]),
h.div([a.class("analytics-box")], [
h.div([a.class("analytics-title")], [
h.text("Number of packages indexed"),
]),
h.text(format_huge_number(model.total_packages)),
]),
]),
h.div([a.class("matches-titles")], [
h.div([a.class("matches-title")], [h.text("Last 30 days")]),
]),
h.div([a.style([#("width", "auto"), #("height", "500px")])], [
case model.timeseries {
[] -> el.none()
data -> {
line_chart.line_chart({
use line_chart.Dataset(dates, value), #(count, date) <- list.fold(
data,
line_chart.Dataset([], []),
)
line_chart.Dataset([birl.to_iso8601(date), ..dates], [
count,
..value
])
})
}
},
]),
],
),
]),
])
router.Search(_) -> {
let key = model.search_key(model.submitted_input, model)
el.fragment([
Expand Down
Loading

0 comments on commit 1a46e28

Please sign in to comment.